Agnic
Partner Program

Partner-Branded Checkout

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.

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.


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:

PartyOwns
YouOpen the deep link; listen for completion; refetch balance
AgnicRenders 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:

FieldWhat it controlsExample
Display nameWordmark in the Checkout header ("Add credit to your ___")PixelAI
Accent colorPrimary CTA + selected preset ring (6-digit hex)#7c3aed

Configure both in the dashboard:

  1. Go to app.agnic.aiOAuth Clients
  2. Click your client → Checkout settings
  3. Set Display name and Accent colorSave

Changes are live on the next page load.


URL format

https://app.agnic.ai/topup
  ?client_id={YOUR_CLIENT_ID}
  &return_url={URL_ENCODED_RETURN}
  &amount={CENTS}            # optional
ParamRequiredNotes
client_idYour app_… client id. Must be approved and not revoked.
return_urlAbsolute URL, URL-encoded. Must match one of the client's registered redirect_uris.
amountPreset (in cents) to pre-select. If matches a package exactly, that chip is highlighted.

Reference implementation

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:

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:

typeWhen
agnic:topup_completePayment succeeded; credit is applied
agnic:topup_cancelledUser 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.

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.

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:

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:

ParamValuesNotes
topupsuccess | cancelledAlways present on mobile return; never present on desktop popup close
session_idopaque stringOnly 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.

"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

ConcernHow 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 appCard data never touches your domain or ours at the page level. You are out of PCI scope.
3DS / Strong Customer AuthenticationHandled end-to-end by Checkout. If a full-page challenge is required, the user returns via return_url.
postMessage spoofing from another tabYou validate ev.origin === "https://app.agnic.ai" in your listener. (See the code above.)
Replay / double-creditHandled 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

SymptomLikely causeFix
"This checkout link is invalid."client_id doesn't exist or is mistypedConfirm the env var; check the client at app.agnic.ai
"This app isn't approved yet."OAuth client is still in reviewWait 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 clientAdd the origin to redirect_uris in the dashboard
"This app's access has been revoked."OAuth client has been revoked by AgnicContact support
postMessage never firesListener not installed, or ev.origin check too strictConfirm origin is exactly https://app.agnic.ai; log all messages
Mobile redirect returns without ?topup=…User hit back button before completionTreat absence as "not paid"; fetchBalance is still safe to call
Accent color doesn't applyHex isn't 6 digits (e.g. #fff)Use 6-digit form (#ffffff); 3-digit is rejected
Popup blocked on desktopwindow.open called outside a user gestureOnly 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.

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.

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.