TwelveAI logoTwelveAI
Documentation

Build conversational banking on TwelveAI.

Send a customer message to one endpoint. TwelveAI classifies intent, calls the right tools grounded in real data, gates money-moving actions behind confirmation, and returns a structured result. Base URL https://api.twelveai.app.

Overview

A request flows: authenticate with your API key; the router classifies intent and narrows the tool set to that agent; the act loop calls tools and feeds results back; the policy layer decides whether a money-moving action runs, must be confirmed, escalated, or blocked; and a grounded response returns with the reply, tool calls, structured output, and usage. Reads answer directly. Writes pause for confirmation.

Two ideas do most of the work. (1) Grounding: the model may only state facts a tool returned this turn. (2) The gate: a write is never executed until your policies allow it and the user confirms.

Authentication

Every request carries your key in the x-api-key header. Each workspace has a live key (sk_live_…) and a sandbox key (sk_test_…). The workspace is derived from the key, so a key only ever touches its own data. Keep keys server-side.

x-api-key: sk_live_xxxxxxxxxxxxxxxx   # live
x-api-key: sk_test_xxxxxxxxxxxxxxxx   # sandbox

Quickstart

Authenticate and send your first message. Pick your language, in every one you send a message and receive a grounded, structured result.

POST /v1/chat
curl https://api.twelveai.app/v1/chat \
  -H "x-api-key: sk_live_xxxxxxxxxxxxxxxx" \
  -H "content-type: application/json" \
  -d '{ "message": "What is my balance?", "customerId": "cust_123" }'

Making a request

POST /v1/chat

The core endpoint. Send a message and, to tie the turn to one of your users and apply their tier, a customerId. Everything else is optional. The response is grounded and structured.

Request
curl https://api.twelveai.app/v1/chat \
  -H "x-api-key: sk_live_xxxxxxxxxxxxxxxx" \
  -H "content-type: application/json" \
  -d '{ "message": "What is my balance?", "customerId": "cust_123" }'
200 Response
{
  "ok": true,
  "sandbox": false,
  "message": "I've prepared a transfer of ₦50,000 to JANE DOE. Confirm to send.",
  "intent": "transfer",
  "toolCalls": [
    { "name": "resolve_account",
      "arguments": { "account_number": "0123456789", "bank_code": "058" },
      "result": { "ok": true, "data": { "account_name": "JANE DOE" } } }
  ],
  "pendingConfirmation": { "tool": "transfer_funds", "arguments": { "amount": 50000 } },
  "escalated": false,
  "autonomy": "pending",
  "usage": { "inputTokens": 812, "outputTokens": 96 },
  "billing": { "charged": 3.1, "balanceNgn": 4820.5, "lowBalance": false },
  "latencyMs": 2140
}

Request fields

FieldTypeDescription
messagestringThe user's message. Required unless resuming with a continuation + toolResults.
customerIdstringYour customer id for this end-user (alias: userId). Applies their tier caps.
tierstringSync the customer's tier on this turn (last-write-wins).
namestringCustomer name. Stored on the customer only if provided.
emailstringCustomer email. Stored on the customer only if provided.
channelstringOrigin channel, e.g. "whatsapp", "mobile", "voice".
intentstringForce a specific agent instead of auto-routing.
confirmedbooleanResend true (with the continuation) to execute a gated write.
metadataobjectArbitrary key/values stored with the turn and echoed back.
continuationstringToken from the previous turn: continue, or (with toolResults) resume.
toolResultsarrayResults for paused client-side tools when resuming.
sandboxbooleanRun this turn in sandbox: tools return sample data, nothing real is called.

Response fields

FieldTypeDescription
okbooleanWhether the turn succeeded.
messagestring|nullThe grounded, user-facing reply. Present even when pending or escalated.
intentstring|nullThe classified agent/intent.
toolCallsarrayExecuted tools: { name, arguments, result }. unconfigured:true = no endpoint attached.
pendingConfirmationobject|nullA gated write awaiting confirmed:true: { tool, arguments }.
pendingToolCallsarray|nullClient-side tools you must run, then resume with toolResults.
escalatedbooleanTrue when a policy routed the write for human review (nothing executed).
policystring|nullName of the policy rule that gated/blocked/escalated, if any.
autonomystring|nullHow it cleared: auto, full, confirmed, pending, escalated, blocked.
continuationstring|nullToken to send back next turn. It changes every turn.
usageobject{ inputTokens, outputTokens }.
billingobject|null{ charged, balanceNgn, lowBalance }. Null in sandbox.

Status codes & errors

Every endpoint returns JSON with an ok boolean. On failure, ok is false and error is a readable string.

200OKThe turn ran. It may still hold a pendingConfirmation, an escalation, or a tool with unconfigured:true.
400Bad requestMissing/invalid input, e.g. no message and no continuation.
401UnauthorizedMissing or invalid x-api-key.
402Payment requiredPrepaid billing balance empty. Top up, then retry.
404Not foundUnknown resource, e.g. a customer that does not exist.
429Rate limitedToo many requests for this key. Back off and retry.
500Server errorUnexpected. Retry idempotent reads; never blind-retry a write without a continuation.

Confirmations (gated writes)

POST /v1/chat

Money-moving actions never execute on the first call. TwelveAI returns pendingConfirmation (the exact tool + arguments) and a continuation. You collect the user's approval (PIN, OTP, biometric), then resend with confirmed: true and that continuation.

// 1) first call returns a pending write, nothing moved yet:
let res = await twelve.post("/v1/chat", {
  message: "Send 50,000 to 0123456789 at GTBank",
  customerId: "cust_123",
});
// res.data.pendingConfirmation = { tool: "transfer_funds", arguments: {…} }

// 2) after the user approves (PIN/OTP), resend with confirmed: true:
res = await twelve.post("/v1/chat", {
  continuation: res.data.continuation,
  message: "yes",
  confirmed: true,
});

Multi-turn & continuation

The continuation is a short, opaque token. We keep the recent conversation server-side under it, and it expires about an hour after the last turn. It is not a fixed session id: every turn returns a new token, so always send back the most recent one.

We retain only the last 10 messages (customer and AI combined) plus system context for the token's lifetime, not the whole history. Live figures (a balance, a status) are always re-fetched via tools, so answers never go stale. Pass your own history array if you need longer memory.

Streaming (SSE)

POST /v1/chat/stream

Streams the reply over Server-Sent Events: token events as it is generated, then a done event carrying the same structured result. Resume a paused turn on /v1/chat, not the stream.

const res = await twelve.post("/v1/chat/stream", {
  message: "What's my balance?",
  customerId: "cust_123",
}, { responseType: "stream" });

res.data.on("data", (c) => process.stdout.write(c.toString()));
// events: "token" (each fragment), then "done" (the structured result)

Client-side execution

Can't give us network access to a system? Declare a tool as client-executed (no binding). TwelveAI pauses and returns pendingToolCalls plus a continuation; you run it inside your perimeter and resume with the result. We never hold your keys.

// A client-executed tool has no binding. TwelveAI pauses:
// res.data.pendingToolCalls = [{ id, name, arguments }]
// run it in your perimeter, then resume:
await twelve.post("/v1/chat", {
  continuation: res.data.continuation,
  toolResults: [{ id: "call_1", result: { balance: 904500 } }],
});

Metadata

POST /v1/chat

Attach a metadata object to any request. It is stored with the turn and surfaced in the Requests explorer. Content retention (input, response, metadata, tool calls) is always on: TwelveAI uses recent conversation content to keep context.

await twelve.post("/v1/chat", {
  message: "Buy 500 airtime for 08012345678",
  customerId: "cust_123",
  metadata: { sessionId: "sess_88", device: "ios" },
});

Sandbox

Use your sk_test_… key, or pass sandbox: true on a turn, to run against stubbed tool responses. Every tool returns its sample response, so you can build and test the whole conversation before wiring a real API. Sandbox turns run the model (so they count tokens) but never touch real systems.

Customers

GET / POST /v1/customers

A customer is one of your end-users, keyed by an opaque id you choose. We store no personal data by default: just the id, a tier, and daily usage. You may also send a name and email and we store them, but nothing is collected unless you provide it. A customer is created automatically the first time you pass its id on a chat.

Customer
{
  "ok": true,
  "customer": {
    "externalId": "cust_123",
    "tier": "premium",
    "effectiveTier": "premium",
    "name": "Ada Okafor",
    "email": "ada@example.com",
    "today": { "count": 0, "sum": 0 },
    "cap": { "maxDailySumNgn": 500000, "maxPerTxnNgn": 200000, "breachAction": "escalate" }
  }
}

Use GET /v1/customers for the list (with usage vs cap), GET /v1/customers/:id for one (with tier history), and GET /v1/customers/:id/events for that customer's chats.

Tiers & daily caps

Tiers map to daily caps (a daily total, a per-transfer max, and transfers/day). Define them under Customers → Manage tiers. When a customer would exceed a cap, TwelveAI blocks or escalates it, per the tier's setting. You are the source of truth for a tier; we never infer it.

Sync on a chat
await twelve.post("/v1/chat", {
  customerId: "cust_123",
  tier: "premium",   // syncs the tier and applies its caps to THIS turn
  message: "Send 50,000 to 0123456789 at GTBank",
});
Every tier move is recorded (visible on the customer's page) and, if you subscribe, emitted as a customer.tier_changed webhook.

Policies & autonomy

Before any write runs, one deterministic policy layer decides the outcome, in order: (1) your custom rules, (2) tier caps, (3) built-in guardrails, (4) the agent's autonomy. The first terminal decision wins. Nothing is model-decided.

FieldTypeDescription
auto_approveactionThe write runs without extra confirmation (within limits).
require_confirmationactionThe write pauses for the user to confirm (the default gate).
escalateactionThe write is routed to your human queue for review, not executed.
blockactionThe write is refused with a message.
feeactionA transaction fee is attached to the write.

Autonomy levels (per agent) are the fallback gate: suggest, confirm, auto (acts under an amount + velocity limit), and full. Configure rules under Policy, autonomy under Agents & Intents.

Escalation

When a policy escalates a write, TwelveAI pauses it, tags the turn escalated: true with the matching policy, tells the user it is under review, and fires the action.escalated webhook. Approving is just resending with confirmed: true. Escalated turns are flagged and filterable in the Requests explorer.

Response
{
  "ok": true,
  "message": "This needs a quick review before it can go through. I've flagged it.",
  "intent": "transfer",
  "toolCalls": [],
  "pendingConfirmation": { "tool": "transfer_funds", "arguments": { "amount": 900000 } },
  "escalated": true,
  "policy": "High-value review",
  "autonomy": "escalated"
}

Guardrails

Guardrails are simple workspace-wide limits set in the console: a maximum transfer amount, blocked recipient accounts, and a tiered transaction-fee schedule. They run through the same policy layer, so there is one enforcement path.

Beneficiaries

Client mode (default): we hold only an opaque reference and your systems keep the account details, so we store no payee PII, and the beneficiary tools are handed to your systems to run (client execution). Managed mode: we host the store so find/save/list run against our data, which also powers the new_beneficiary policy signal.

Tools & endpoints

PUT / POST /v1/tools/:name

Capability tools (get_balance, buy_airtime) call your endpoints. Attach one per tool in the console under Agents & Intents → Manage tools, or via PUT /v1/tools/:name. Use {customer_id} for the end-user and {arg} for a tool argument.

Attach endpoint
curl -X PUT https://api.twelveai.app/v1/tools/get_balance \
  -H "x-api-key: sk_live_xxxxxxxxxxxxxxxx" \
  -H "content-type: application/json" \
  -d '{
    "method": "GET",
    "url": "https://api.yourbank.com/v1/customers/{customer_id}/balance",
    "auth": { "type": "bearer", "token": "YOUR_API_TOKEN" }
  }'
Test it
// Confirm an endpoint returns 200 before it goes near a customer:
await twelve.post("/v1/tools/get_balance/test", {
  method: "GET",
  url: "https://api.yourbank.com/v1/customers/{customer_id}/balance",
});
// { ok: true, status: 200, statusText: "OK", sample: {…} }

When a tool returns, our AI reads the JSON and turns it into a grounded reply, never restating raw JSON or inventing values.

Transfers are handed off, never executed by us. transfer_funds comes back in pendingToolCalls with the verified details; you collect the PIN, initiate on your rails, and resume with the result. Policies still block/escalate first.
In live mode a capability tool with no endpoint returns { ok:false, unconfigured:true } (we never fabricate data). In sandbox it returns its sample response.

Tool Manifest

Declare your own HTTP tools with a manifest entry: a name, a JSON-Schema for parameters, and a binding (method + URL with {customer_id} and {arg} placeholders). TwelveAI calls it, applies the gate for writes, and grounds the answer. Set a workspace Tool base URL so bindings can use relative paths.

Manifest entry
{
  "name": "get_statement",
  "description": "Get a summary of the user's recent transactions.",
  "sideEffect": "read",
  "parameters": {
    "type": "object",
    "properties": { "count": { "type": "integer", "minimum": 1, "maximum": 50 } }
  },
  "binding": {
    "method": "GET",
    "url": "https://api.yourbank.com/v1/customers/{customer_id}/transactions",
    "resultPath": "data"
  },
  "auth": { "type": "bearer", "token": "YOUR_API_TOKEN" }
}

Billing & usage

GET /v1/usage

Billing is per token, split into input and output, shown on every turn. Fund a prepaid NGN billing balance; a turn is blocked with 402 when empty. Set a low-balance threshold to be emailed before you run dry.

Related reads: GET /v1/usage/analytics (daily series), GET /v1/usage/events (request traces), and GET /v1/billing (billing balance + transactions).

Webhooks

Set a webhook URL and subscribe to events under Webhooks (or PUT /v1/settings). We POST a JSON body with the event name, its fields, and an at timestamp; delivery is queued and retried. Set your own webhookSecret and we send it verbatim in the x-webhook-secret header so you can verify a call is really from us.

// set your signing secret once:
await twelve.put("/v1/settings", { webhookUrl: "https://you.com/hooks", webhookSecret: "whsec_…" });

// verify on your endpoint (Express):
app.post("/hooks", (req, res) => {
  if (req.get("x-webhook-secret") !== process.env.TWELVE_WEBHOOK_SECRET) return res.status(401).end();
  const { event, ...data } = req.body; // e.g. "action.executed"
  res.sendStatus(200);
});

Events: customer.created, customer.tier_changed, customer.blocked, beneficiary.saved, action.confirmation_required, action.escalated, action.blocked, action.executed, ai.response.completed, billing.low_balance.

Versioning

GET /v1/version

The API is versioned in the path (/v1). We add fields without breaking existing ones; a published endpoint's behavior does not change in place. GET /v1/version returns the current version and recent releases, and every chat response carries an engine-version header.

Using the dashboard

The console is where you configure and observe everything above. Sign in with your workspace email and password; the browser talks to the API through a server proxy so your key never reaches the browser.

1

Fund your billing balance. Open Billing & Costs and top up. Live turns need a positive balance.

2

Connect your tools. Under Agents & Intents, open an agent, click Manage tools, and attach the endpoint for each capability tool. Hit Test to confirm a 200.

3

Set limits. Define tiers and caps under Customers, and opt into rules under Policy.

4

Test in the Playground. Chat as a customer. Toggle Sandbox for sample data; turn it off to hit your live endpoints.

5

Watch it run. Every turn appears in Requests; usage rolls up under Analytics.

Endpoint reference

POST/v1/chatA grounded turn (start, continue, or resume).
POST/v1/chat/streamStreaming turn (SSE).
GET/v1/versionVersion + changelog.
GET/v1/usageToken usage summary.
GET/v1/usage/eventsRecent request traces.
GET / POST/v1/customersList or create/update customers.
GET / PATCH/v1/customers/:idFetch or update one customer.
GET/v1/customers/:id/eventsOne customer's chats.
GET/v1/billingBilling balance + transactions.
GET/v1/keysReveal your live + sandbox keys.
GET / PUT/v1/settingsAlerts, retention, webhook, secret, tiers, policies.
GET / PUT/v1/tools/:nameAttach or update a tool's endpoint.
POST/v1/tools/:name/testCall a tool's endpoint and report the HTTP status.

Manage keys, endpoints, agents, and billing in the dashboard, or explore live in the Playground.

Try it without writing code first.