---
title: Partner-Branded Checkout
description: Let your users top up their Agnic balance without leaving your brand — one deep link, no PCI scope, no payment infrastructure.
---

When a user in your app runs out of credit, you need to get them funded and back to generating revenue in under 30 seconds. **Partner-Branded Checkout** is that path — and every top-up driven through your integration is attributed to your Partner ID and accrues commission.

It's a hosted page that:

- Wears **your wordmark and accent color**
- Opens as a **desktop popup** or **mobile full-page redirect**
- Handles card collection, 3DS, and fraud scoring for you
- Signals your app the moment the payment clears, so your UI updates without a reload

You write ~40 lines of frontend code. Agnic owns payments, PCI scope, and reconciliation.

<Callout type="info">
**Why this matters for your earnings.** Every top-up driven through your `client_id` is tagged to your Partner ID. All downstream API usage the user funds from that credit accrues commission to you at your Partner tier. Wiring up Checkout is the fastest way to turn a user's "I'm out of credit" moment into attributed revenue.
</Callout>

---

## The flow

```
┌────────────────┐                                          ┌────────────────┐
│                │  1. user clicks "Add Funds"              │                │
│  Your app      │─────────────────────────────────────────▶│  popup /       │
│                │                                          │  redirect      │
└────────┬───────┘                                          └────────┬───────┘
         │                                                           │
         │                                      2. Agnic Checkout loads
         │                                      (your brand, preset chips)
         │                                                           │
         │                                      3. user pays securely
         │                                                           │
         │     4. 'agnic:topup_complete' postMessage / redirect     │
         │◀──────────────────────────────────────────────────────────┘
         │
         │     5. your app refetches balance → UI updates
         ▼
  user keeps using your product
```

Responsibilities:

| Party      | Owns                                                          |
|------------|---------------------------------------------------------------|
| **You**    | Open the deep link; listen for completion; refetch balance    |
| **Agnic**  | Renders the branded page; collects payment; credits the user  |

You never touch card data. You never run webhook handlers.

---

## Step 1 — Brand your Checkout page

Two fields drive the user-facing surface:

| Field           | What it controls                                                 | Example      |
|-----------------|------------------------------------------------------------------|--------------|
| **Display name**| Wordmark in the Checkout header ("Add credit to your ___")       | `PixelAI`    |
| **Accent color**| Primary CTA + selected preset ring (6-digit hex)                 | `#7c3aed`    |

Configure both in the dashboard:

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

Changes are live on the next page load.

---

## Step 2 — Open the Checkout deep link

### URL format

```
https://app.agnic.ai/topup
  ?client_id={YOUR_CLIENT_ID}
  &return_url={URL_ENCODED_RETURN}
  &amount={CENTS}            # optional
```

| 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`.**       |
| `amount`      | ✗        | Preset (in cents) to pre-select. If matches a package exactly, that chip is highlighted.        |

### Reference implementation

```tsx
const handleTopUp = () => {
  const returnUrl = `${window.location.origin}/your-page`;
  const base = process.env.NEXT_PUBLIC_AGNIC_TOPUP_URL || "https://app.agnic.ai/topup";
  const clientId = process.env.NEXT_PUBLIC_AGNIC_OAUTH_CLIENT_ID!;
  const url = `${base}?client_id=${encodeURIComponent(clientId)}&return_url=${encodeURIComponent(returnUrl)}`;

  // Popup on desktop, full redirect on mobile (narrow viewport).
  const isNarrow = typeof window !== "undefined" && window.innerWidth < 640;
  if (isNarrow) {
    window.location.href = url;
    return;
  }

  const width = 480;
  const height = 720;
  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-topup", `width=${width},height=${height},left=${left},top=${top},popup=yes`);
};
```

The 640px breakpoint matters: mobile browsers commonly block `window.open`. Anything narrower than a laptop gets a full-page redirect instead of a popup.

---

## Step 3 — Handle the return

Two callbacks fire when the user finishes. Listen for **both** — desktop uses the first, mobile uses the second.

### Desktop: `postMessage`

The popup emits a `window.postMessage` to `window.opener` the moment the payment clears. Install a listener once:

```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:topup_complete") {
      fetchBalance();
      toast.success("Top-up successful — your balance is updated.");
    }
    if (ev.data.type === "agnic:topup_cancelled") {
      // user closed the popup without paying — no-op, or show a prompt
    }
  };
  window.addEventListener("message", onMessage);
  return () => window.removeEventListener("message", onMessage);
}, []);
```

The event types Agnic emits:

| `type`                     | When                                              |
|----------------------------|---------------------------------------------------|
| `agnic:topup_complete`     | Payment succeeded; credit is applied              |
| `agnic:topup_cancelled`    | User closed the popup before paying               |

The popup closes itself ~2 seconds after a successful payment, so your `postMessage` handler is the fast path — refetch the balance there, don't wait for the window to close.

<Callout type="info">
**Always check `ev.origin`.** It should be `https://app.agnic.ai`. This prevents a malicious tab in the same browser from spoofing a completion event.
</Callout>

### Mobile: `?topup=success` on the return URL

When Checkout ran as a full-page redirect, there's no opener to post to. Instead, it bounces the user back to your `return_url` with `?topup=success` appended:

```tsx
useEffect(() => {
  if (typeof window === "undefined") return;
  const params = new URLSearchParams(window.location.search);
  if (params.get("topup") === "success") {
    fetchBalance();
    toast.success("Top-up successful — your balance is updated.");
    // Clean the URL so a refresh doesn't re-toast.
    params.delete("topup");
    params.delete("session_id");
    const qs = params.toString();
    window.history.replaceState({}, "", window.location.pathname + (qs ? `?${qs}` : ""));
  }
}, []);
```

Query parameters you may receive on `return_url`:

| Param         | Values                    | Notes                                                                |
|---------------|---------------------------|----------------------------------------------------------------------|
| `topup`       | `success` \| `cancelled`  | Always present on mobile return; never present on desktop popup close|
| `session_id`  | opaque string             | Only on `success`. Useful if you want to correlate with your own logs|

---

## Complete reference component

Everything in one place — three effects + one handler. Drop this into any React app you already have Agnic auth in.

```tsx
"use client";

import { useEffect, useState, useCallback } from "react";
import { toast } from "sonner";

export default function YourPage() {
  const [balance, setBalance] = useState<number>();

  const fetchBalance = useCallback(async () => {
    const res = await fetch("https://api.agnic.ai/api/balance", {
      headers: { Authorization: `Bearer ${userAccessToken}` },
    });
    const data = await res.json();
    if (data.balance !== undefined) setBalance(data.balance);
  }, []);

  useEffect(() => { fetchBalance(); }, [fetchBalance]);

  // 1. Listen for the desktop popup's success message.
  useEffect(() => {
    const onMessage = (ev: MessageEvent) => {
      if (ev.origin !== "https://app.agnic.ai") return;
      if (ev.data?.type === "agnic:topup_complete") {
        fetchBalance();
        toast.success("Top-up successful.");
      }
    };
    window.addEventListener("message", onMessage);
    return () => window.removeEventListener("message", onMessage);
  }, [fetchBalance]);

  // 2. Handle the mobile redirect return.
  useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    if (params.get("topup") === "success") {
      fetchBalance();
      toast.success("Top-up successful.");
      params.delete("topup"); params.delete("session_id");
      const qs = params.toString();
      window.history.replaceState({}, "", window.location.pathname + (qs ? `?${qs}` : ""));
    }
  }, [fetchBalance]);

  // 3. Open Checkout.
  const handleTopUp = () => {
    const returnUrl = `${window.location.origin}/your-page`;
    const base = process.env.NEXT_PUBLIC_AGNIC_TOPUP_URL!;
    const clientId = process.env.NEXT_PUBLIC_AGNIC_OAUTH_CLIENT_ID!;
    const url = `${base}?client_id=${encodeURIComponent(clientId)}&return_url=${encodeURIComponent(returnUrl)}`;

    if (window.innerWidth < 640) {
      window.location.href = url;
      return;
    }
    const w = 480, h = 720;
    const left = Math.round(window.screenX + (window.outerWidth - w) / 2);
    const top = Math.round(window.screenY + (window.outerHeight - h) / 2);
    window.open(url, "agnic-topup", `width=${w},height=${h},left=${left},top=${top},popup=yes`);
  };

  return (
    <button onClick={handleTopUp}>
      Add funds {balance !== undefined && `(balance: $${balance.toFixed(2)})`}
    </button>
  );
}
```

That's the whole integration. ~50 lines, zero backend changes on your side.

---

## Security model — what this means for you

| Concern                                   | How Checkout 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 data never touches your domain or ours at the page level. You are out of PCI scope.           |
| 3DS / Strong Customer Authentication      | Handled end-to-end by Checkout. If a full-page challenge is required, the user returns via `return_url`. |
| `postMessage` spoofing from another tab   | **You** validate `ev.origin === "https://app.agnic.ai"` in your listener. (See the code above.)   |
| Replay / double-credit                    | Handled by Agnic; you can trust that a `topup_complete` corresponds to a real captured payment.    |

---

## Testing

### Manual smoke-test checklist

- Desktop Chrome: popup opens, centered, 480×720
- Desktop Safari: popup opens (Safari ≥ 14 allows popups in response to a click)
- iPhone Safari: falls through to `window.location.href`, full-page redirect
- Android Chrome: same full-page behavior under 640 px
- `return_url` not registered → error page renders, no payment session created
- Click ✕ in header → popup closes, `agnic:topup_cancelled` fires
- Successful payment on desktop → `agnic:topup_complete` fires, popup closes ~2 s later
- Successful payment on mobile → redirect to `return_url?topup=success&session_id=…`

---

## Troubleshooting

| Symptom                                      | Likely cause                                                             | Fix                                                                   |
|----------------------------------------------|--------------------------------------------------------------------------|-----------------------------------------------------------------------|
| "This checkout 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` on the client    | Add the origin to `redirect_uris` in the dashboard                    |
| "This app's access has been revoked."        | OAuth client has been revoked by Agnic                                   | Contact [support](mailto:support@agnic.ai)                            |
| `postMessage` never fires                    | Listener not installed, or `ev.origin` check too strict                  | Confirm origin is exactly `https://app.agnic.ai`; log all messages    |
| Mobile redirect returns without `?topup=…`   | User hit back button before completion                                   | Treat absence as "not paid"; `fetchBalance` is still safe to call     |
| Accent color doesn't apply                   | Hex isn't 6 digits (e.g. `#fff`)                                         | Use 6-digit form (`#ffffff`); 3-digit is rejected                     |
| Popup blocked on desktop                     | `window.open` called outside a user gesture                              | Only call it from a click handler, never from `setTimeout` or async   |

---

## FAQ

**Can I host my own Checkout UI?**
Not today. Hosting Checkout on Agnic is what keeps you out of PCI scope and out of payment regulatory obligations. If you have a use case the hosted page can't cover, [talk to us](mailto:partners@agnic.ai).

**Can I pre-fill an amount?**
Yes — pass `?amount={CENTS}`. It must match a package amount exactly, otherwise the "Popular" preset is auto-selected.

**Can I change the top-up packages for my users?**
Not partner-by-partner. Packages are global today. If your product needs custom denominations, [talk to us](mailto:partners@agnic.ai).

**Does this work for recurring billing?**
No — this is one-time credit. Recurring is on the roadmap. For now, design around credits.

**What happens if my OAuth client is revoked mid-flow?**
In-flight payments already started complete normally and users still receive credit. Only new Checkout sessions are blocked — the page begins rendering an access-revoked error.

---

<Cards>
  <Card title="OAuth 2.0 integration" href="/docs/authentication/oauth2">
    The same `client_id` powers Sign in with Agnic and Checkout
  </Card>
  <Card title="Partner Program overview" href="/docs/partner-program">
    Revenue share, tiers, and the legal model
  </Card>
  <Card title="Manage OAuth clients" href="https://app.agnic.ai/oauth-clients">
    Create clients, register redirect URIs, set Checkout branding
  </Card>
  <Card title="Track your earnings" href="https://app.agnic.ai/earnings">
    Real-time accrued commission and payout history
  </Card>
</Cards>
