API reference
Every endpoint, in the open, with request samples in cURL, Node, Python, PHP, Go, Ruby and Java. REST over HTTPS, JSON in and out, amounts in kobo. Base URL: https://api.twelveai.app
Getting started
Authenticate every request with a bearer API key. Keys are mode-scoped: sk_test_… for test and sk_live_… for live. Never expose a secret key in client-side code. Connect platforms use an OAuth access token (tw_at_…) on the same endpoints, limited to the granted scopes.
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxEvery endpoint below includes a ready-to-run request in your language. Switch tabs on the Request panel.
Payments
/api/business/v1/payments/initializeInitialize a payment
Starts a payment and returns a branded checkout_url to redirect your customer to.
curl -X POST https://api.twelveai.app/api/business/v1/payments/initialize \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "email": "customer@example.com", "amount": 50000, "reference": "order-12345" }'
{ "success": true, "data": { "reference": "order-12345", "checkout_url": "https://checkout.twelveai.app/c/order-12345", "authorization_url": "https://checkout.paystack.com/…", "amount": 50000, "currency": "NGN" } }
/api/business/v1/payments/verify/:referenceVerify a payment
Re-checks a payment. Use when a webhook was missed. Idempotent.
curl "https://api.twelveai.app/api/business/v1/payments/verify/order-12345" \ -H "Authorization: Bearer sk_test_..."
{ "success": true, "data": { "reference": "order-12345", "status": "successful", "amount": 50000, "fee": 750, "net": 49250, "currency": "NGN", "paid_at": "2026-06-12T12:34:56Z", "customer": { "email": "customer@example.com", "name": null }, "metadata": { "order_id": "12345" } } }
/api/business/v1/paymentsList payments
Cursor-paginated, newest first, scoped to the mode of the key you send.
curl "https://api.twelveai.app/api/business/v1/payments?status=successful&limit=50" \ -H "Authorization: Bearer sk_live_..."
{ "success": true, "data": { "payments": [ { "reference": "order-12345", "amount": 50000, "fee": 750, "net": 49250, "currency": "NGN", "status": "successful", "settlement_status": "settled", "customer_email": "customer@example.com", "paid_at": "2026-06-12T12:34:56Z", "created_at": "2026-06-12T12:30:00Z" } ], "next_cursor": null } }
/api/business/v1/saved-cardsList saved cards
Reusable card authorizations captured on successful payments. Use the authorization_code to re-charge.
curl "https://api.twelveai.app/api/business/v1/saved-cards?email=customer@example.com" \ -H "Authorization: Bearer sk_live_..."
{ "success": true, "data": { "saved_cards": [ { "authorization_code": "AUTH_xxxxxxxx", "customer_email": "customer@example.com", "brand": "visa", "last4": "4081", "exp_month": "08", "exp_year": "2029", "bank": "GTBank", "reusable": true } ], "next_cursor": null } }
/api/business/v1/payments/charge-authorizationCharge a saved card
Synchronous re-charge against a saved authorization_code. Live mode only.
curl -X POST https://api.twelveai.app/api/business/v1/payments/charge-authorization \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "authorization_code": "AUTH_xxxxxxxx", "email": "customer@example.com", "amount": 50000 }'
{ "success": true, "message": "Charged.", "data": { "reference": "subscription-may-2026", "status": "successful", "amount": 50000, "net": 49250, "paid_at": "2026-05-01T08:00:00Z" } }
Balance & money
/api/business/v1/balanceGet balance
Wallet balances (kobo), verification status, and this month's live usage.
curl "https://api.twelveai.app/api/business/v1/balance" \ -H "Authorization: Bearer sk_live_..."
{ "success": true, "data": { "available_balance": 4925000, "pending_balance": 150000, "currency": "NGN", "verification_status": "verified", "monthly_cap": null, "monthly_used": 12500000 } }
/api/business/v1/transactionsList transactions
Unified money-movement ledger (live): card payments + incoming bank transfers + payouts, newest first. Connect scope: transactions:read.
curl "https://api.twelveai.app/api/business/v1/transactions?limit=50" \ -H "Authorization: Bearer sk_live_..."
{ "success": true, "data": { "transactions": [ { "kind": "payment", "direction": "credit", "reference": "order-12345", "amount_minor": 50000, "fee_minor": 750, "currency": "NGN", "status": "successful", "counterparty": "customer@example.com", "created_at": "2026-06-12T12:34:56Z" } ], "next_offset": 50 } }
/api/business/v1/customersList customers
Your customers, aggregated by email across payments. Connect scope: customers:read.
curl "https://api.twelveai.app/api/business/v1/customers?limit=50" \ -H "Authorization: Bearer sk_live_..."
{ "success": true, "data": { "customers": [ { "email": "customer@example.com", "payments": 3, "total_minor": 150000, "last_seen": "2026-06-12T12:34:56Z" } ] } }
/api/business/v1/transfersCreate a payout
Withdraw available balance to the account's saved settlement account. Live only. Connect scope: payouts:write. GET the same path lists payouts.
curl -X POST https://api.twelveai.app/api/business/v1/transfers \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "amount": 25000 }'
{ "success": true, "message": "Payout submitted. Status will update when the bank confirms.", "data": { "payout": { "reference": "TWAI_PO_…", "status": "processing", "amount": 2500000, "currency": "NGN", "destination": { "bank_name": "GTBank", "account_number": "0123456789", "account_name": "Greyark Technologies LLC" }, "created_at": "2026-06-12T12:34:56Z" } } }
Webhooks
/api/business/v1/webhooksConfigure webhooks
Sets (or replaces) the URL we POST events to for this mode. signing_secret is returned once.
curl -X POST https://api.twelveai.app/api/business/v1/webhooks \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "url": "https://yourdomain.com/webhooks/twelve" }'
{ "success": true, "message": "Save the signing_secret - it will not be shown again.", "data": { "id": 12, "mode": "live", "url": "https://yourdomain.com/webhooks/twelve", "signing_secret": "whsec_…", "enabled": true } }
Connect (OAuth)
/oauth/tokenExchange / refresh token
Client-authenticated (no bearer). Exchange the auth code for an access token, or refresh an existing one.
curl -X POST https://api.twelveai.app/oauth/token \ -H "Content-Type: application/json" \ -d '{ "grant_type": "authorization_code", "client_id": "twc_...", "client_secret": "twcs_...", "code": "tw_ac_...", "redirect_uri": "https://yourapp.com/twelveai/callback" }'
{ "success": true, "access_token": "tw_at_...", "refresh_token": "tw_rt_...", "token_type": "Bearer", "scope": "payments:write payments:read", "mode": "live" }
/oauth/sandbox/connectSandbox connect
Client-authenticated. Returns an instant TEST access token connected to a sandbox account, so you can test your on-behalf integration without the interactive consent flow. Test mode only.
curl -X POST https://api.twelveai.app/oauth/sandbox/connect \ -H "Content-Type: application/json" \ -d '{ "client_id": "twc_...", "client_secret": "twcs_..." }'
{ "success": true, "access_token": "tw_at_...", "refresh_token": "tw_rt_...", "token_type": "Bearer", "scope": "payments:write payments:read", "mode": "test", "connected_account_id": 2 }
Connecting a business: popup & mobile
You do not have to full-page redirect. The authorize step works three ways, pick what fits your app. All of them end with a one-time code you exchange server-side at POST /oauth/token.
Web — popup (no redirect)
Drop in connect.js and call TwelveConnect.open(). It opens a popup and hands the code back via postMessage, so your page never navigates away.
<script src="https://business.twelveai.app/connect.js"></script> <script> TwelveConnect.open({ clientId: "twc_...", redirectUri: "https://yourapp.com/twelveai/callback", // must be registered scope: "payments:write payments:read", mode: "live", onSuccess: async ({ code }) => { // Send code to YOUR server, then exchange it with your client_secret. await fetch("/api/twelveai/exchange", { method: "POST", body: JSON.stringify({ code }) }) }, onError: (err) => console.error(err), onClose: () => {}, // user closed the popup }) </script>
Mobile app — in-app browser + deep link
Register a custom-scheme redirect URI (e.g. myapp://twelveai/callback), open the same authorize URL in an in-app browser, and capture the code from the deep link the consent screen redirects to. Then exchange it from your server.
import * as WebBrowser from "expo-web-browser" const redirectUri = "myapp://twelveai/callback" // registered on your app const authorizeUrl = "https://business.twelveai.app/oauth/authorize" + "?client_id=twc_..." + "&redirect_uri=" + encodeURIComponent(redirectUri) + "&scope=" + encodeURIComponent("payments:write payments:read") + "&mode=live" const result = await WebBrowser.openAuthSessionAsync(authorizeUrl, redirectUri) if (result.type === "success") { const code = new URL(result.url).searchParams.get("code") // POST code to your server → exchange at /oauth/token with your client_secret }
Use PKCE (code_challenge) for mobile, since public clients cannot hold a secret. Universal/App Links (an https redirect that opens your app) work too.
Connect scopes
A connection's granted scopes are the ceiling, enforced on every request. Out-of-scope calls return 403 insufficient_scope.
payments:readGET /payments · GET /payments/verify/:referencepayments:writePOST /payments/initialize · POST /payments/charge-authorizationtransactions:readGET /transactionsbalance:readGET /balancecustomers:readGET /customers · GET /saved-cardspayouts:writePOST /transfers · GET /transfers (live only)Webhook events & signatures
We POST events to your configured URL whenever a payment changes state. Verify the signature against the raw request body before trusting a payload.
charge.successA payment succeeded.charge.failedA payment attempt failed.refund.processedA refund completed.x-twelveai-signature— HMAC-SHA512 of the raw body, hex.x-twelveai-timestamp— Unix seconds. Reject if > 5 minutes old.x-twelveai-event— event name.x-twelveai-event-id— unique delivery id for dedup.
Retry policy: 1m → 5m → 30m → 2h → 12h → 24h → 48h. After 20 consecutive non-2xx responses we auto-disable the endpoint; re-save the URL to re-enable. Acknowledge with a 2xx within 10 seconds and do your work asynchronously.
Errors & rate limits
Errors return a non-2xx status with a JSON body. Common statuses:
400Bad request — a required field is missing or invalid.401Unauthenticated — bad, missing, or revoked key/token.403Forbidden — verification required, or a Connect token missing a scope.404Not found — no such resource on this account.429Too many requests — you are being rate limited; back off and retry.{ "success": false, "message": "A valid customer email is required." }
Get your API keys
Create an account, grab your test keys, and make your first call in minutes.