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
/api/v1/invoicesMint 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)
/api/v1/invoices/:idPublic, unauthenticated status endpoint (safe to poll from the browser): returns the same invoice object with live status, txHash and paidAt.
Payment links
Payment links are reusable, no-code products (create them in Dashboard → Buttons & Links). Each visit to /l/<slug> shows a branded page; paying mints a fresh invoice. You can also mint programmatically:
/api/v1/links/:slug// open CORS — callable from any storefront frontend
{
"amount": "49", // only for open-amount links
"reference": "cart-8812", // optional
"redirectUrl": "https://..." // optional
}
→ { "ok": true, "data": { "checkoutUrl": "...", ... } }Your public storefront at /m/<handle> lists all active links — a link-in-bio page for sellers without a website.
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
