Partner-Branded Add Card
Let your users vault a payment card with Agnic without leaving your brand — one deep link, zero PCI scope for you.
Agent purchases on the card rail need a vaulted card. Partner-Branded Add Card is the hosted page that gets one on file: your user clicks "Add a card" in your app, a page wearing your wordmark and accent color opens, and the card goes straight into VGS — Agnic's PCI-certified vault. Neither your app nor Agnic's frontend ever sees the card number.
It works exactly like Partner-Branded Checkout:
- Wears your wordmark and accent color (same dashboard settings)
- Opens as a desktop popup or mobile full-page redirect
- Signals your app the moment the card is vaulted —
postMessageon desktop,?card=successon mobile
Card entry is rendered by VGS-hosted iframes. The card number, expiry, and CVC never exist in your page, your domain, or even Agnic's JavaScript — only opaque aliases flow to the API. Your app stays fully out of PCI scope (SAQ-A on Agnic's side).
The flow
┌────────────────┐ ┌────────────────┐
│ │ 1. user clicks "Add a card" │ │
│ Your app │─────────────────────────────────────────▶│ popup / │
│ │ │ redirect │
└────────┬───────┘ └────────┬───────┘
│ │
│ 2. Agnic Add Card loads
│ (your brand, VGS iframes)
│ │
│ 3. user enters card → VGS vault
│ │
│ 4. 'agnic:card_added' postMessage / redirect │
│◀──────────────────────────────────────────────────────────┘
│
│ 5. your app updates its UI ("card on file ✓")
▼
user's agents can now pay on the card railResponsibilities:
| Party | Owns |
|---|---|
| You | Open the deep link; listen for completion |
| Agnic | Renders the branded page; VGS card collection; vaulting |
You never touch card data. There is nothing card-shaped to store on your side.
Step 1 — Brand the page
Uses the same two dashboard fields as Checkout — if you've already set them up, you're done:
- Go to app.agnic.ai → OAuth Clients
- Click your client → Checkout settings
- Set Display name and Accent color → Save
Step 2 — Open the deep link
URL format
https://app.agnic.ai/partner/cards/new
?client_id={YOUR_CLIENT_ID}
&return_url={URL_ENCODED_RETURN}| Param | Required | Notes |
|---|---|---|
client_id | ✓ | Your app_… client id. Must be approved and not revoked. |
return_url | ✓ | Absolute URL, URL-encoded. Must match one of the client's registered redirect_uris. |
Reference implementation
const handleAddCard = () => {
const returnUrl = `${window.location.origin}/your-page`;
const clientId = process.env.NEXT_PUBLIC_AGNIC_OAUTH_CLIENT_ID!;
const url = `https://app.agnic.ai/partner/cards/new?client_id=${encodeURIComponent(clientId)}&return_url=${encodeURIComponent(returnUrl)}`;
// Popup on desktop, full redirect on mobile (narrow viewport).
if (window.innerWidth < 640) {
window.location.href = url;
return;
}
const width = 480; // keep ≥ 480 — the expiry/CVC grid needs the room
const height = 760;
const left = Math.max(0, Math.round(window.screenX + (window.outerWidth - width) / 2));
const top = Math.max(0, Math.round(window.screenY + (window.outerHeight - height) / 2));
window.open(url, "agnic-add-card", `width=${width},height=${height},left=${left},top=${top},popup=yes`);
};Open the popup at least 480 px wide. The card form renders expiry and CVC side by side; narrower popups cramp the VGS iframes.
Step 3 — Handle the return
Listen for both callbacks — desktop uses postMessage, mobile uses the redirect params.
Desktop: postMessage
useEffect(() => {
const onMessage = (ev: MessageEvent) => {
// Origin check: only trust messages from Agnic.
if (ev.origin !== "https://app.agnic.ai") return;
if (!ev.data || typeof ev.data !== "object") return;
if (ev.data.type === "agnic:card_added") {
// ev.data.last4: string, ev.data.brand: string | null — display only
toast.success(`Card •••• ${ev.data.last4} added.`);
refreshCardStatus();
}
if (ev.data.type === "agnic:card_cancelled") {
// user closed the popup without adding a card — no-op
}
};
window.addEventListener("message", onMessage);
return () => window.removeEventListener("message", onMessage);
}, []);type | Payload | When |
|---|---|---|
agnic:card_added | last4: string, brand: string|null | Card vaulted successfully |
agnic:card_cancelled | — | User closed the popup before finishing |
The payload is display data only — no card identifiers or vault aliases are ever exposed.
Mobile: ?card=success on the return URL
useEffect(() => {
const params = new URLSearchParams(window.location.search);
if (params.get("card") === "success") {
toast.success("Card added.");
refreshCardStatus();
params.delete("card");
const qs = params.toString();
window.history.replaceState({}, "", window.location.pathname + (qs ? `?${qs}` : ""));
}
}, []);| Param | Values | Notes |
|---|---|---|
card | success | cancelled | Appended to your return_url on mobile |
Security model — what this means for you
| Concern | How Add Card handles it |
|---|---|
| Arbitrary redirect (open-redirect abuse) | return_url must match a redirect_uri on your OAuth client. Rejected otherwise. |
| PCI scope for your app | Card fields are VGS-hosted iframes. The PAN/CVC never exists in your page or Agnic's JS. |
postMessage spoofing from another tab | You validate ev.origin === "https://app.agnic.ai" in your listener. |
| Who can add a card | The user signs in to their own Agnic account inside the flow; cards are strictly user-scoped. |
| Sensitive data in callbacks | Only last4 + brand are emitted — never aliases, card ids, or tokens. |
Testing
Manual smoke-test checklist
- Desktop Chrome: popup opens 480×760, VGS card fields render
- Add a test card → success screen →
agnic:card_addedfires withlast4→ popup closes ~2 s later - Click ✕ in header →
agnic:card_cancelledfires, popup closes - Mobile (viewport < 640 px): full-page redirect; success returns
return_url?card=success return_urlnot registered → error page renders, no card form shown- Signed-out user → inline sign-in inside the flow, then the card form
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| "This link is invalid." | client_id doesn't exist or is mistyped | Confirm the env var; check the client at app.agnic.ai |
| "This app isn't approved yet." | OAuth client is still in review | Wait for approval (typically 1 business day) |
| "The return URL isn't registered for this app." | return_url doesn't prefix-match any redirect_uri | Add the origin to redirect_uris in the dashboard |
| Card fields don't render | Popup too narrow, or VGS script blocked by an extension | Open ≥ 480 px wide; test without ad blockers |
postMessage never fires | Listener not installed, or origin check too strict | Origin is exactly https://app.agnic.ai; log all messages |
| Popup blocked on desktop | window.open called outside a user gesture | Only call it from a click handler |
FAQ
Can I embed this in an iframe instead of a popup?
No. The page ships X-Frame-Options: DENY deliberately — a card-entry surface must never render inside another site's frame (clickjacking), and the user's Agnic session isn't available inside third-party iframes anyway. The popup gives the same "never left your app" feel with none of those problems.
Can I read the card details my user added?
No. You get last4 and brand for display, nothing else. Cards belong to the user's Agnic account, not to your app.
Does adding a card earn commission? The card itself doesn't, but agent purchases it enables are attributed to your Partner ID when they flow through your integration.