---
title: Partner-Branded Add Card
description: 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](/docs/partner-program/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 — `postMessage` on desktop, `?card=success` on mobile

<Callout type="info">
**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).
</Callout>

---

## 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 rail
```

Responsibilities:

| 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:

1. Go to [app.agnic.ai](https://app.agnic.ai) → **OAuth Clients**
2. Click your client → **Checkout settings**
3. 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

```tsx
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`);
};
```

<Callout type="info">
**Open the popup at least 480 px wide.** The card form renders expiry and CVC side by side; narrower popups cramp the VGS iframes.
</Callout>

---

## Step 3 — Handle the return

Listen for **both** callbacks — desktop uses `postMessage`, mobile uses the redirect params.

### Desktop: `postMessage`

```tsx
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

```tsx
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_added` fires with `last4` → popup closes ~2 s later
- Click ✕ in header → `agnic:card_cancelled` fires, popup closes
- Mobile (viewport < 640 px): full-page redirect; success returns `return_url?card=success`
- `return_url` not 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.

---

<Cards>
  <Card title="Partner-Branded Checkout" href="/docs/partner-program/checkout">
    Same pattern for topping up credit
  </Card>
  <Card title="Partner-Branded Delegation" href="/docs/partner-program/delegation">
    Let users authorize agent spending with a passkey
  </Card>
  <Card title="OAuth 2.0 integration" href="/docs/authentication/oauth2">
    The same `client_id` powers Sign in with Agnic and hosted flows
  </Card>
  <Card title="Manage OAuth clients" href="https://app.agnic.ai/oauth-clients">
    Create clients, register redirect URIs, set branding
  </Card>
</Cards>
