Start here
Overview
The REENG API connects your bank's own systems to your AI voice agent. Everything a call produces arrives as a ticket, ranked by priority, with a response deadline. Your systems can read and update tickets, receive events the moment they happen, keep the agent's knowledge up to date, and ask the agent to call a customer back.
With your connector, the agent can also verify a caller against your own records and answer about their own account, such as whether a transfer went through, or block their card after they say yes. You decide who is verified and exactly what the agent may say. REENG keeps none of your answers.
| Name | Type | Description |
|---|---|---|
Base URL | https://api.reeng-ai.dev/v1 | |
Format | JSON in and out, UTF-8. Times are ISO 8601 in UTC. | |
Who can use it | Banks on REENG, for now. Your Admins manage keys, webhooks and the connector on API & Webhooks in the dashboard. |
Five minutes
Quick start
- Create a key. In the dashboard, open API & Webhooks and press Create key. Copy it straight away: it's shown once.
- List your open tickets, most urgent first.
curl https://api.reeng-ai.dev/v1/tickets?status=open \
-H "Authorization: Bearer rk_live_..."- Add a webhook on the same page, so new tickets reach you during the call instead of on your next poll. Then press Send a test.
Keys and tokens
Authentication
Send your API key as a bearer token on every request. Each key belongs to one bank and sees only its tickets, calls and files.
Authorization: Bearer rk_live_9xQ2...- One key per system, named for what uses it, so you can revoke one without the others. Up to 10 at a time.
- Shown once. REENG keeps only a hash. If a key is lost, revoke it and create another.
- IP allowlists. Limit a key to your servers' addresses or ranges (up to 20). From anywhere else it's refused with 403.
OAuth 2.0 client credentials
If your security team prefers short-lived tokens, exchange a key for a token that lasts an hour. Your client_id is shown next to the key in the dashboard.
POST/v1/oauth/token
curl -X POST https://api.reeng-ai.dev/v1/oauth/token \
-H "Content-Type: application/json" \
-d '{"grant_type": "client_credentials", "client_id": "key_14", "client_secret": "rk_live_9xQ2..."}'{ "access_token": "eyJhbGciOi...", "token_type": "Bearer", "expires_in": 3600 }Use the token exactly like a key: Authorization: Bearer eyJhbGciOi.... It stops working when it expires or when its key is revoked.
How requests behave
Conventions
Idempotency
Send an Idempotency-Key header with any update or callback request. Repeating the same key within 24 hours returns the first result instead of doing it twice, so a retry after a timeout is always safe.
Idempotency-Key: 4f1c2a7e-ticket-1042-closePaging
List tickets with order=updated to page through everything that changed, newest first. Each response carries a next_cursor; pass it back as cursor until it's null.
curl "https://api.reeng-ai.dev/v1/tickets?status=all&order=updated&limit=100&cursor=MjAyNi0xMS0xMFQwOToxNDowMyswMDowMHwxMDQy" \
-H "Authorization: Bearer rk_live_..."Core resource
Tickets
A ticket is everything one call produced that your team has to act on: the caller, what they asked for, its priority, its status and its deadline. A call that produced several requests (a message and a branch appointment, say) is one ticket with several requests.
{
"id": 1042,
"status": "new",
"priority": "urgent",
"priority_label": "P1 Critical",
"created_at": "2026-11-10T09:14:03+00:00",
"updated_at": "2026-11-10T09:14:03+00:00",
"caller": { "name": "Musa Ibrahim", "phone": "+2348095551002", "email": null, "address": null },
"requests": [
{
"id": 1042, "type": "callback", "label": "Callback", "priority": "urgent",
"item": null, "quantity": 1, "total": 0, "currency": null,
"details": "Three card debits overnight in Lagos he didn't make. Card blocked by USSD."
}
],
"team_note": null,
"assigned_to": null,
"due_at": "2026-11-10T09:29:03+00:00",
"overdue": false,
"call": { "id": 88213, "summary": "Caller reported unauthorised POS debits and blocked his card." }
}| Name | Type | Description |
|---|---|---|
status | string | new, contacted, done or cancelled. |
priority | string | urgent, high, normal or low. See Priorities and deadlines. |
priority_label | string | Your own name for that level, such as P1 Critical. |
requests[].type | string | callback, booking, appointment, consultation, visit, order, delivery, reservation or viewing. |
due_at | string | When it must be handled by, from its priority and your deadlines. |
overdue | boolean | Still open after due_at. |
assigned_to | string | Who on your team owns it. Free text, up to 120 characters. |
call | object | The call it came from, with its summary. Null for a missed call with no conversation. |
GET/v1/tickets
| Name | Type | Description |
|---|---|---|
status | string | open (default: new and contacted), closed (done and cancelled), all, or one status. |
priority | string | Only this priority. |
assigned_to | string | Only tickets assigned to exactly this name. |
overdue | boolean | true for overdue ones only, false for the rest. |
updated_since | datetime | Only tickets changed after this time. |
order | string | priority (default) or updated (newest change first, pages with cursor). |
cursor | string | From the previous page's next_cursor. |
limit | integer | Up to 200. Default 100. |
{ "tickets": [ { "id": 1042, "priority": "urgent", "...": "..." } ], "count": 37, "next_cursor": null }GET/v1/tickets/{id}
PATCH/v1/tickets/{id}
| Name | Type | Description |
|---|---|---|
status | string | new, contacted, done or cancelled. |
priority | string | urgent, high, normal or low. Starts a new deadline. |
team_note | string | Your team's note, up to 2,000 characters. Empty clears it. |
assigned_to | string | Who owns it. Empty unassigns. |
curl -X PATCH https://api.reeng-ai.dev/v1/tickets/1042 \
-H "Authorization: Bearer rk_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 1042-fraud-desk" \
-d '{"status": "contacted", "assigned_to": "Ada (fraud desk)", "team_note": "Card replaced, dispute opened"}'The response is the updated ticket, and a ticket.updated event goes to your webhook.
What comes first
Priorities and deadlines
The agent sets each ticket's priority from what happened on the call. Your team can change it at any time.
| Name | Type | Description |
|---|---|---|
urgent | 15 min | Money, safety or security at risk right now: fraud, an unauthorised debit, a lost or stolen card, a scam in progress. Sent to your webhook during the call. |
high | 4 hours | The caller is stuck until you act: money taken but not received, a restricted account, an angry or repeat caller, a missed call. |
normal | 1 day | Everything else: questions, appointments, general messages. |
low | 3 days | Only your team sets this. |
Use your own names and times. On API & Webhooks, rename the four levels to match your service policy (P1 to P4, say) and set each one's deadline. Your names appear on Follow-ups and in every ticket as priority_label; the keys above never change, so your integration keeps working when a name does.
The times shown are the default deadlines. A ticket still open past its deadline is marked overdue, and a ticket.overdue event is sent once (checked every minute). Changing a ticket's priority starts a new deadline.
Every conversation
Calls
GET/v1/calls
| Name | Type | Description |
|---|---|---|
since | datetime | Only calls that ended after this time. |
limit | integer | Up to 200. Default 50. |
GET/v1/calls/{id}
{
"id": 88213,
"call_sid": "CA9f2c...",
"direction": "inbound",
"caller": "+2348095551002",
"started_at": "2026-11-10T09:11:40+00:00",
"ended_at": "2026-11-10T09:14:03+00:00",
"duration_seconds": 143,
"summary": "Caller reported unauthorised POS debits and blocked his card.",
"sentiment": "Negative",
"outcome": null,
"ticket_ids": [1042],
"transcript": [ { "role": "agent", "text": "Thank you for calling Kestrel Trust Bank..." }, { "role": "user", "text": "..." } ]
}What the agent knows
Knowledge base
Keep the agent's documents current from your own systems: your fees schedule, branch list or rates, the moment they change. The same rules apply as in the dashboard: .txt, .md, .pdf, .csv or .xlsx, up to 10 MB each.
GET/v1/knowledge
filename, size_kb, type, modified.PUT/v1/knowledge
file.curl -X PUT https://api.reeng-ai.dev/v1/knowledge \
-H "Authorization: Bearer rk_live_..." \
-F "file=@fees-and-charges.pdf"DELETE/v1/knowledge/{filename}
The agent rings them
Outbound callbacks
Ask the agent to call a customer back, for example once a dispute is resolved. It calls only the caller behind one of your tickets, from your REENG number, and opens with your reason: “Hello, this is Kestrel Trust Bank calling you back about your card dispute. Is now a good time to talk?” From there it's an ordinary call.
POST/v1/tickets/{id}/call
| Name | Type | Description |
|---|---|---|
purposerequired | string | What the call is about, in a few words the agent can say. 3 to 200 characters. |
curl -X POST https://api.reeng-ai.dev/v1/tickets/1042/call \
-H "Authorization: Bearer rk_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 1042-callback-1" \
-d '{"purpose": "your card dispute"}'{ "id": 311, "status": "calling", "call_sid": "CA41d0...", "to": "+2348095551002" }- Needs an active plan and a REENG number. A forwarded-only number can't place calls (409).
- Up to 3 callbacks a day for one ticket, and 30 an hour in total (429).
- The minutes count like any real call. When the customer doesn't answer, is busy or rejects it, you get
outbound_call.failed; when they answer, the usualcall.startedandcall.ended.
Pushed to you
Webhooks
Add one https address on API & Webhooks and choose its events. Each event is a POST with a JSON body.
| Name | Type | Description |
|---|---|---|
ticket.created | ticket | A new ticket. Urgent ones are sent during the call; the rest when the call ends. |
ticket.updated | ticket | Changed by your team or through the API. |
ticket.overdue | ticket | Still open past its deadline. Sent once. |
call.started | object | A call began: call_sid, direction, caller, started_at. |
call.ended | call | A call ended, with its summary (and transcript, if you share them). |
outbound_call.failed | object | A callback wasn't answered: id, ticket_id, to, purpose, reason. |
ticket.test | ticket | From Send a test. Always sent, whatever you subscribe to. |
{
"id": "6f2a9a3e-1c1e-4c1f-9d6b-0b9a5d1e2c77",
"event": "ticket.created",
"created_at": "2026-11-10T09:14:04.218Z",
"data": { "id": 1042, "priority": "urgent", "...": "the ticket" }
}| Name | Type | Description |
|---|---|---|
X-REENG-Event | string | The event name. |
X-REENG-Delivery | string | The event id. A redelivery keeps it, so you can ignore one you've already stored. |
X-REENG-Timestamp | string | Unix seconds when it was signed. |
X-REENG-Signature | string | v1= then the signature. Two, comma-separated, while you rotate your secret. |
- Answer with any 2xx once you've stored it, within 10 seconds. Redirects aren't followed.
- Retries: up to three tries, straight away, after 30 seconds and after 5 minutes.
- Delivery log: every event of the last 7 days, with its result, is on API & Webhooks, where you can send any of them again.
- Order isn't guaranteed. Use
updated_atto keep the newest state.
Trust, but check
Verifying webhooks
Every request is signed with your secret: an HMAC-SHA256 of the timestamp, a full stop and the raw body. Check it before you trust anything in the body, and reject requests more than 5 minutes old.
import hashlib, hmac, time
def is_from_reeng(secrets, timestamp, raw_body, signature_header):
# Reject anything older than 5 minutes: a replayed request.
if abs(time.time() - int(timestamp)) > 300:
return False
signed = timestamp.encode() + b"." + raw_body
for secret in secrets: # your current secret, plus the old one while rotating
want = "v1=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
if any(hmac.compare_digest(want, s.strip()) for s in signature_header.split(",")):
return True
return Falseimport crypto from "node:crypto";
export function isFromReeng(secrets, timestamp, rawBody, signatureHeader) {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const sent = signatureHeader.split(",").map((s) => s.trim());
return secrets.some((secret) => {
const want = "v1=" + crypto.createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest("hex");
return sent.some((s) => s.length === want.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(want)));
});
}Verified account help
Your connector
Your connector is an https address on your side. During a call the agent asks it to verify the caller, then to look things up or act, as you allow. Your system makes every decision; the agent only reads out what you return.
- The caller asks about their account. The agent asks your chosen security questions, one at a time.
- REENG sends a
verifyrequest with their answers and the number they're calling from. You reply verified or not. - Once verified, the agent can send
lookuprequests for what you allowed, and read yoursaytext aloud. - For an action such as blocking a card, the agent reads it back, waits for a clear yes, then sends
action.
Set it up on API & Webhooks: the address, 1 to 3 security questions, and what a verified caller can ask about and have done. Saving gives the connector its own signing secret; requests are signed exactly like webhooks.
| Name | Type | Description |
|---|---|---|
full_name | Their full name. | |
date_of_birth | Their date of birth, as they say it. | |
account_number | Their full account number. | |
account_last4 | The last four digits of their account number. | |
card_last4 | The last four digits of their card. | |
registered_phone | The phone number on the account. |
| Name | Type | Description |
|---|---|---|
balance | lookup | Their account balance. |
recent_transactions | lookup | Their recent transactions. |
transfer_status | lookup | Whether a transfer went through. The agent passes what the caller said in details. |
card_status | lookup | Their card's status. |
loan_status | lookup | Their loan balance and next repayment. |
block_card | action | Block their card. |
freeze_account | action | Stop money leaving the account. |
Requests you receive
{
"type": "verify",
"call_id": "CA9f2c...",
"is_test": false,
"caller_phone": "+2348095551002",
"answers": { "date_of_birth": "4 May 1990", "account_last4": "4471" }
}{ "verified": true, "customer_ref": "CUST-77", "say": "" }
// or, when the details don't match:
{ "verified": false, "say": "Those details didn't match our records." }{
"type": "lookup", "call_id": "CA9f2c...", "is_test": false,
"customer_ref": "CUST-77", "lookup": "transfer_status", "details": "Monday, 45,000 naira to Riverbend Bank"
}{ "say": "Your transfer of forty-five thousand naira on Monday was reversed to your account this morning." }{ "type": "action", "call_id": "CA9f2c...", "is_test": false, "customer_ref": "CUST-77", "action": "block_card" }{ "done": true, "say": "Your card ending 1234 is now blocked. You can order a new one in the app." }A ping request ({"type": "ping"}) is sent when you press Test it; answer it with any JSON object.
| Name | Type | Description |
|---|---|---|
Speed | Answer within 4 seconds: a caller is waiting. Otherwise the agent apologises and takes a message. | |
Replies | A 2xx with a JSON object. say is read aloud as written, up to 400 characters: write it as you'd say it. | |
Verification | Three tries per call, then the agent stops. A verification lasts for that caller on that call only, up to 30 minutes. | |
customer_ref | Your own reference for the verified customer. It's sent back with every lookup and action on that call. | |
is_test | true for test calls from the dashboard. Use a sandbox, or refuse. |
When things go wrong
Errors and limits
Errors return a status code and a JSON body you can show to a person: { "detail": "Ticket not found" }.
| Name | Type | Description |
|---|---|---|
400 | Something in the request isn't valid. detail says what. | |
401 | Missing, wrong, expired or revoked key or token. | |
402 | An outbound callback needs an active plan. | |
403 | Not available to this account, or this key can't be used from this address. | |
404 | Not found, or not yours. | |
409 | Can't be done in the current state, such as a callback without a caller number. | |
429 | Too many requests. Wait, then retry. | |
502 | A provider REENG relies on didn't respond. Retry with the same Idempotency-Key. |
| Name | Type | Description |
|---|---|---|
API requests | 120 a minute per key. | |
Token requests | 30 a minute per address. | |
Callbacks | 3 a day per ticket, 30 an hour. | |
Keys | 10 at a time. | |
Allowlist | 20 addresses or ranges per key. |
Stability
Versioning
Everything here is version 1. We add fields and events without notice, so ignore what you don't recognise. Anything that would break your integration comes as a new version, announced in advance, with the old one kept running alongside.
Last updated 10 November 2026.