TwelveAI logoTwelveAIConversational Banking
Docs/API Reference

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.

Authentication header
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxx

Every endpoint below includes a ready-to-run request in your language. Switch tabs on the Request panel.

Payments

POST/api/business/v1/payments/initialize

Initialize a payment

Starts a payment and returns a branded checkout_url to redirect your customer to.

ParameterDescription
email*Customer email.
amount*Amount in kobo. Min 100.
referenceYour order id. Auto-generated if omitted.
callback_urlWhere the customer is redirected after payment. Defaults to your dashboard payment page.
customer_nameStored on the payment.
customer_phoneStored on the payment.
metadataEchoed back on verify + webhook.
application_fee_percentConnect only: your platform cut as a percentage of the charge (e.g. 2.5). Defaults to your app’s configured %. Capped at net.
application_fee_bpsConnect only: the same cut in basis points (250 = 2.5%). Ignored if application_fee_percent is set.
application_fee_capConnect only: cap the % fee at this max amount in kobo (e.g. 10000 = ₦100). Defaults to your app’s configured cap. 0 = uncapped.
Request
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"
}'
Response
{
  "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"
  }
}
Redirect the customer to checkout_url (our branded page), or use authorization_url to skip straight to the processor.
GET/api/business/v1/payments/verify/:reference

Verify a payment

Re-checks a payment. Use when a webhook was missed. Idempotent.

Request
curl "https://api.twelveai.app/api/business/v1/payments/verify/order-12345" \
  -H "Authorization: Bearer sk_test_..."
Response
{
  "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" }
  }
}
GET/api/business/v1/payments

List payments

Cursor-paginated, newest first, scoped to the mode of the key you send.

ParameterDescription
statusFilter: successful | pending | failed.
limitDefault 50, max 200.
cursorPrevious response's next_cursor.
Request
curl "https://api.twelveai.app/api/business/v1/payments?status=successful&limit=50" \
  -H "Authorization: Bearer sk_live_..."
Response
{
  "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
  }
}
GET/api/business/v1/saved-cards

List saved cards

Reusable card authorizations captured on successful payments. Use the authorization_code to re-charge.

ParameterDescription
emailFilter to one customer.
limitDefault 50, max 200.
cursorPagination cursor.
Request
curl "https://api.twelveai.app/api/business/v1/saved-cards?email=customer@example.com" \
  -H "Authorization: Bearer sk_live_..."
Response
{
  "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
  }
}
Treat authorization codes as secrets — anyone holding one can charge that card through your account. Store them server-side only.
POST/api/business/v1/payments/charge-authorization

Charge a saved card

Synchronous re-charge against a saved authorization_code. Live mode only.

ParameterDescription
authorization_code*From GET /saved-cards.
email*The card owner’s email.
amount*Amount in kobo.
referenceYour reference for this charge.
metadataEchoed back on verify + webhook.
Request
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
}'
Response
{
  "success": true,
  "message": "Charged.",
  "data": {
    "reference": "subscription-may-2026",
    "status": "successful",
    "amount": 50000,
    "net": 49250,
    "paid_at": "2026-05-01T08:00:00Z"
  }
}
Live mode only — saved-card re-charges always move real money. There is no test-mode charge-authorization.

Balance & money

GET/api/business/v1/balance

Get balance

Wallet balances (kobo), verification status, and this month's live usage.

Request
curl "https://api.twelveai.app/api/business/v1/balance" \
  -H "Authorization: Bearer sk_live_..."
Response
{
  "success": true,
  "data": {
    "available_balance": 4925000,
    "pending_balance": 150000,
    "currency": "NGN",
    "verification_status": "verified",
    "monthly_cap": null,
    "monthly_used": 12500000
  }
}
GET/api/business/v1/transactions

List transactions

Unified money-movement ledger (live): card payments + incoming bank transfers + payouts, newest first. Connect scope: transactions:read.

ParameterDescription
limitDefault 50, max 200.
offsetPagination offset.
Request
curl "https://api.twelveai.app/api/business/v1/transactions?limit=50" \
  -H "Authorization: Bearer sk_live_..."
Response
{
  "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
  }
}
kind is payment | collection | payout; direction is credit | debit. Paginate with next_offset.
GET/api/business/v1/customers

List customers

Your customers, aggregated by email across payments. Connect scope: customers:read.

ParameterDescription
qFilter by email (substring).
limitDefault 50, max 200.
Request
curl "https://api.twelveai.app/api/business/v1/customers?limit=50" \
  -H "Authorization: Bearer sk_live_..."
Response
{
  "success": true,
  "data": {
    "customers": [
      {
        "email": "customer@example.com",
        "payments": 3,
        "total_minor": 150000,
        "last_seen": "2026-06-12T12:34:56Z"
      }
    ]
  }
}
POST/api/business/v1/transfers

Create a payout

Withdraw available balance to the account's saved settlement account. Live only. Connect scope: payouts:write. GET the same path lists payouts.

ParameterDescription
amount*Amount in NGN (major units), min 5,000.
memoOptional note on the payout.
Request
curl -X POST https://api.twelveai.app/api/business/v1/transfers \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
  "amount": 25000
}'
Response
{
  "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"
    }
  }
}
Live only, and always to the saved settlement account — a Connect platform can never route funds to an arbitrary destination. Min ₦5,000; the unverified monthly cap still applies.

Webhooks

POST/api/business/v1/webhooks

Configure webhooks

Sets (or replaces) the URL we POST events to for this mode. signing_secret is returned once.

ParameterDescription
url*Your https endpoint.
Request
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"
}'
Response
{
  "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)

GET/oauth/authorize

Authorize (browser redirect)

Send the business you want to connect to this URL in their browser. On approval we redirect back to your redirect_uri with a one-time code and your metadata.

ParameterDescription
client_id*Your platform app id.
redirect_uri*Must exactly match a registered URI.
scope*Space-separated scopes.
metadataEchoed back verbatim; use it to correlate / for CSRF. May be a JSON object.
modetest | live (live needs app approval). Default live.
code_challengeOptional PKCE (S256).
Request
GET https://business.twelveai.app/oauth/authorize
  ?client_id=twc_...
  &redirect_uri=https://yourapp.com/twelveai/callback
  &scope=payments:write payments:read
  &metadata=<opaque, echoed back to you>
  &mode=live
  &code_challenge=...&code_challenge_method=S256
Response
# On approval we redirect the browser to:
https://yourapp.com/twelveai/callback?code=tw_ac_...&metadata=...
POST/oauth/token

Exchange / refresh token

Client-authenticated (no bearer). Exchange the auth code for an access token, or refresh an existing one.

ParameterDescription
grant_type*authorization_code | refresh_token
client_id*Your platform app id.
client_secret*Your platform secret.
codeFor authorization_code.
redirect_uriMust match the authorize request.
code_verifierFor PKCE.
refresh_tokenFor refresh_token.
Request
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"
}'
Response
{
  "success": true,
  "access_token": "tw_at_...",
  "refresh_token": "tw_rt_...",
  "token_type": "Bearer",
  "scope": "payments:write payments:read",
  "mode": "live"
}
Then call any /api/business/v1/* endpoint with Authorization: Bearer tw_at_…, limited to the granted scopes.
POST/oauth/sandbox/connect

Sandbox 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.

ParameterDescription
client_id*Your platform app id.
client_secret*Your platform secret.
scopeOptional subset of your app scopes. Defaults to all.
Request
curl -X POST https://api.twelveai.app/oauth/sandbox/connect \
  -H "Content-Type: application/json" \
  -d '{
  "client_id": "twc_...",
  "client_secret": "twcs_..."
}'
Response
{
  "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.

Web popup
<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.

React Native / Expo
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/:reference
payments:writePOST /payments/initialize · POST /payments/charge-authorization
transactions:readGET /transactions
balance:readGET /balance
customers:readGET /customers · GET /saved-cards
payouts: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.
Error shape
{ "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.