API Documentation

Accept non-custodial USDT payments on BNB Smart Chain. One REST call mints a hosted checkout; funds settle straight to your wallet with a flat 0.5% fee split on-chain.

Quickstart

Create an invoice and redirect your customer to the returned checkout URL:

curl -X POST https://afa-pay.samadi-capitaltm.workers.dev/api/v1/invoices \
  -H "Content-Type: application/json" \
  -H "x-api-key: afa_live_..." \
  -d '{
    "chainId": 56,
    "token": "USDT",
    "amount": "25.00",
    "method": "WALLET",
    "reference": "order-1042",
    "redirectUrl": "https://yourshop.com/thanks"
  }'
{
  "ok": true,
  "data": {
    "id": "inv_9fc9e5a2e57096fcbf1c0d4be810b9aa",
    "status": "PENDING",
    "checkoutUrl": "https://afa-pay.samadi-capitaltm.workers.dev/pay/inv_9fc9...",
    "forwarderAddress": "0x62412Ab7...",   // per-invoice QR deposit address
    "amountWei": "25000000000000000000",
    "payoutAddress": "0xYourWallet...",
    "feeBps": 50,
    "expiresAt": "2026-07-18T16:01:05.477Z"
  }
}

The hosted checkout offers both wallet-connect and an exchange-style QR deposit. When the payment settles on-chain you receive a signed invoice.paid webhook, and the customer is redirected to your redirectUrl.

Authentication

Create an API key in Dashboard → Developers and send it with every request:

x-api-key: afa_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Keys are shown once at creation and stored hashed. Use test mode keys for staging; revoke any key instantly from the dashboard.

Invoices

POST/api/v1/invoices

Mint a single-use invoice. Body fields:

chainId      56                        // BNB Smart Chain
token        "USDT"                    // launch scope: USDT (BEP-20)
amount       "25.00"                   // human decimal string
method       "WALLET" | "QR"           // checkout default tab
reference?   "order-1042"              // your own order id (shown on receipt)
redirectUrl? "https://..."             // browser redirect after payment
ttlMinutes?  30                        // expiry (5–1440)
GET/api/v1/invoices/:id

Public, unauthenticated status endpoint (safe to poll from the browser): returns the same invoice object with live status, txHash and paidAt.

Widget & buttons

Drop-in checkout for any website — no backend required:

<script src="https://afa-pay.samadi-capitaltm.workers.dev/widget.js" defer></script>

<!-- reusable button created in the dashboard -->
<button data-afa-button="btn_xxxxxxxx">Pay with AFA.CO</button>

<!-- or open a specific invoice you minted server-side -->
<a data-afa-checkout="inv_xxxxxxxx">Complete payment</a>

The widget opens the hosted checkout in an overlay iframe and emits afaPay postMessage events (status, paid) to the host page. For security the checkout never broadcasts these events to "*": it targets only your origin. Either load it with ?embedOrigin=https://your-site.com, or complete the handshake — the iframe posts { afaPayReady: true } on load, and your page replies with { afaPayInit: true } so the checkout learns (and pins) your exact origin.

Webhooks

Add an endpoint in Dashboard → Developers; you receive a signing secret (shown once). Events are POSTed as JSON:

POST https://yourshop.com/afa-webhook
AFA-Event: invoice.paid
AFA-Signature: t=1789040000,v1=8f2a…c41

{
  "event": "invoice.paid",
  "data": { /* invoice object, same shape as GET /invoices/:id */ },
  "createdAt": "2026-07-18T15:58:09.000Z"
}

Verify the signature — HMAC-SHA256 over `${t}.${rawBody}`:

import crypto from "node:crypto";

function verify(rawBody, header, secret, toleranceSec = 300) {
  const { t, v1 } = Object.fromEntries(header.split(",").map(p => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false;
  const expected = crypto.createHmac("sha256", secret)
    .update(`${t}.${rawBody}`).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

Events: invoice.paid · invoice.underpaid. Deliveries retry with backoff until acknowledged with a 2xx.

Statuses & errors

PENDING    awaiting payment
DETECTED   funds seen at the QR deposit address (partial or unconfirmed)
PAID       settled on-chain — 99.5% in your wallet
UNDERPAID  received less than the invoice amount
EXPIRED    TTL passed without payment (late payments are still reconciled for 24h)

All errors share one shape:

{ "ok": false, "error": { "message": "No payout wallet set for BNB Smart Chain" } }

Every paid invoice has a public receipt at /receipt/<id> — an immutable, on-chain-verifiable proof you can share with customers.

Questions? afa.developersteam@gmail.com