Skip to content

REENG API · v1

Build on your AI voice agent.

Tickets with priorities and deadlines, signed events as calls happen, and verified answers from your own systems, for banking first.

GET /v1/tickets?status=open

# Open tickets, most urgent first
{
  "tickets": [{
    "id": 1042,
    "priority": "urgent",
    "status": "new",
    "due_at": "09:29 UTC",
    "details": "Card debits he didn't make"
  }],
  "count": 1
}
On this page
  1. Overview
  2. Quick start
  3. Authentication
  4. Conventions
  5. Tickets
  6. Priorities and deadlines
  7. Calls
  8. Knowledge base
  9. Outbound callbacks
  10. Webhooks
  11. Verifying webhooks
  12. Your connector
  13. Errors and limits
  14. Versioning

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.

At a glance
NameTypeDescription
Base URLhttps://api.reeng-ai.dev/v1
FormatJSON in and out, UTF-8. Times are ISO 8601 in UTC.
Who can use itBanks on REENG, for now. Your Admins manage keys, webhooks and the connector on API & Webhooks in the dashboard.

Five minutes

Quick start

  1. Create a key. In the dashboard, open API & Webhooks and press Create key. Copy it straight away: it's shown once.
  2. List your open tickets, most urgent first.
Request
curl https://api.reeng-ai.dev/v1/tickets?status=open \
  -H "Authorization: Bearer rk_live_..."
  1. 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.

Header
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

Request
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..."}'
Response
{ "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.

Header
Idempotency-Key: 4f1c2a7e-ticket-1042-close

Paging

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.

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

The ticket object
{
  "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." }
}
Fields
NameTypeDescription
statusstringnew, contacted, done or cancelled.
prioritystringurgent, high, normal or low. See Priorities and deadlines.
priority_labelstringYour own name for that level, such as P1 Critical.
requests[].typestringcallback, booking, appointment, consultation, visit, order, delivery, reservation or viewing.
due_atstringWhen it must be handled by, from its priority and your deadlines.
overduebooleanStill open after due_at.
assigned_tostringWho on your team owns it. Free text, up to 120 characters.
callobjectThe call it came from, with its summary. Null for a missed call with no conversation.

GET/v1/tickets

List tickets. By default the open ones, most urgent first.
Parameters
NameTypeDescription
statusstringopen (default: new and contacted), closed (done and cancelled), all, or one status.
prioritystringOnly this priority.
assigned_tostringOnly tickets assigned to exactly this name.
overduebooleantrue for overdue ones only, false for the rest.
updated_sincedatetimeOnly tickets changed after this time.
orderstringpriority (default) or updated (newest change first, pages with cursor).
cursorstringFrom the previous page's next_cursor.
limitintegerUp to 200. Default 100.
Response
{ "tickets": [ { "id": 1042, "priority": "urgent", "...": "..." } ], "count": 37, "next_cursor": null }

GET/v1/tickets/{id}

One ticket. 404 if it isn't yours.

PATCH/v1/tickets/{id}

The same changes your team makes on the Follow-ups page. Send only what changes.
Body
NameTypeDescription
statusstringnew, contacted, done or cancelled.
prioritystringurgent, high, normal or low. Starts a new deadline.
team_notestringYour team's note, up to 2,000 characters. Empty clears it.
assigned_tostringWho owns it. Empty unassigns.
Request
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.

Priorities
NameTypeDescription
urgent15 minMoney, 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.
high4 hoursThe caller is stuck until you act: money taken but not received, a restricted account, an angry or repeat caller, a missed call.
normal1 dayEverything else: questions, appointments, general messages.
low3 daysOnly 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

Your real calls, newest first. Test calls are never included.
Parameters
NameTypeDescription
sincedatetimeOnly calls that ended after this time.
limitintegerUp to 200. Default 50.

GET/v1/calls/{id}

One call.
The call object
{
  "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

Your files: filename, size_kb, type, modified.

PUT/v1/knowledge

Add a file, or replace the one with the same name. Multipart, field file.
Request
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}

Remove a file. The agent stops using it straight away.

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

Body
NameTypeDescription
purposerequiredstringWhat the call is about, in a few words the agent can say. 3 to 200 characters.
Request
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"}'
Response
{ "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 usual call.started and call.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.

Events
NameTypeDescription
ticket.createdticketA new ticket. Urgent ones are sent during the call; the rest when the call ends.
ticket.updatedticketChanged by your team or through the API.
ticket.overdueticketStill open past its deadline. Sent once.
call.startedobjectA call began: call_sid, direction, caller, started_at.
call.endedcallA call ended, with its summary (and transcript, if you share them).
outbound_call.failedobjectA callback wasn't answered: id, ticket_id, to, purpose, reason.
ticket.testticketFrom Send a test. Always sent, whatever you subscribe to.
Envelope
{
  "id": "6f2a9a3e-1c1e-4c1f-9d6b-0b9a5d1e2c77",
  "event": "ticket.created",
  "created_at": "2026-11-10T09:14:04.218Z",
  "data": { "id": 1042, "priority": "urgent", "...": "the ticket" }
}
Headers
NameTypeDescription
X-REENG-EventstringThe event name.
X-REENG-DeliverystringThe event id. A redelivery keeps it, so you can ignore one you've already stored.
X-REENG-TimestampstringUnix seconds when it was signed.
X-REENG-Signaturestringv1= 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_at to 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.

Python
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 False
Node.js
import 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.

  1. The caller asks about their account. The agent asks your chosen security questions, one at a time.
  2. REENG sends a verify request with their answers and the number they're calling from. You reply verified or not.
  3. Once verified, the agent can send lookup requests for what you allowed, and read your say text aloud.
  4. 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.

Security questions you can choose
NameTypeDescription
full_nameTheir full name.
date_of_birthTheir date of birth, as they say it.
account_numberTheir full account number.
account_last4The last four digits of their account number.
card_last4The last four digits of their card.
registered_phoneThe phone number on the account.
Lookups and actions you can allow
NameTypeDescription
balancelookupTheir account balance.
recent_transactionslookupTheir recent transactions.
transfer_statuslookupWhether a transfer went through. The agent passes what the caller said in details.
card_statuslookupTheir card's status.
loan_statuslookupTheir loan balance and next repayment.
block_cardactionBlock their card.
freeze_accountactionStop money leaving the account.

Requests you receive

verify
{
  "type": "verify",
  "call_id": "CA9f2c...",
  "is_test": false,
  "caller_phone": "+2348095551002",
  "answers": { "date_of_birth": "4 May 1990", "account_last4": "4471" }
}
Your reply
{ "verified": true, "customer_ref": "CUST-77", "say": "" }

// or, when the details don't match:
{ "verified": false, "say": "Those details didn't match our records." }
lookup
{
  "type": "lookup", "call_id": "CA9f2c...", "is_test": false,
  "customer_ref": "CUST-77", "lookup": "transfer_status", "details": "Monday, 45,000 naira to Riverbend Bank"
}
Your reply
{ "say": "Your transfer of forty-five thousand naira on Monday was reversed to your account this morning." }
action
{ "type": "action", "call_id": "CA9f2c...", "is_test": false, "customer_ref": "CUST-77", "action": "block_card" }
Your reply
{ "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.

Rules
NameTypeDescription
SpeedAnswer within 4 seconds: a caller is waiting. Otherwise the agent apologises and takes a message.
RepliesA 2xx with a JSON object. say is read aloud as written, up to 400 characters: write it as you'd say it.
VerificationThree tries per call, then the agent stops. A verification lasts for that caller on that call only, up to 30 minutes.
customer_refYour own reference for the verified customer. It's sent back with every lookup and action on that call.
is_testtrue 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" }.

Status codes
NameTypeDescription
400Something in the request isn't valid. detail says what.
401Missing, wrong, expired or revoked key or token.
402An outbound callback needs an active plan.
403Not available to this account, or this key can't be used from this address.
404Not found, or not yours.
409Can't be done in the current state, such as a callback without a caller number.
429Too many requests. Wait, then retry.
502A provider REENG relies on didn't respond. Retry with the same Idempotency-Key.
Limits
NameTypeDescription
API requests120 a minute per key.
Token requests30 a minute per address.
Callbacks3 a day per ticket, 30 an hour.
Keys10 at a time.
Allowlist20 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.