Free trial

API documentation

The Voza API uses HTTPS, JSON and the usual HTTP verbs. Message, call and webhook bodies follow the format of the official WhatsApp API (Meta), so the same code works with both kinds of Voza number.

Base URL:

https://www.vozaapi.com/v1
QR code numberOfficial number (Meta)
How it connectsScanning a QR code with the phone, like on WhatsApp WebWith the credentials of your Meta app (WhatsApp Cloud API)
WhatsApp accountRegular WhatsApp or WhatsApp Business; the phone keeps workingWhatsApp Business Platform, in your Meta account
MessagesText and mediaText, media, templates, interactive messages, stickers, location, contacts and reactions
24-hour windowNoneOutside it, only templates approved by Meta
CallsYes, several at the same time on the same numberYes, when Meta enables calls for the number
Conversation chargesNone besides the planMeta charges your Meta account
RiskWhatsApp may block numbers that send spamFollows Meta's policies and quality ratings

Quick start

  1. Create an account and, in Dashboard › API keys, a key.
  2. Create a number:
curl https://www.vozaapi.com/v1/numbers \
  -H "Authorization: Bearer vz_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"name": "Downtown Store", "webhook_url": "https://your-system.com/webhooks/whatsapp"}'
  1. Open the response's qr_url and scan the QR code with WhatsApp on the phone (Linked devices › Link a device). Or connect an official number (see Official numbers).
  2. Send the first message:
curl https://www.vozaapi.com/v1/numbers/NUMBER_ID/messages \
  -H "Authorization: Bearer vz_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"to": "5511987654321", "type": "text", "text": {"body": "Hello! 👋"}}'
  1. The contact's replies reach your webhook, in Meta's format. Done: the rest of this page is detail.

Authentication

Create a key in Dashboard › API keys and send it in every request, in the Authorization header:

Authorization: Bearer vz_SUA_CHAVE

Errors

All errors have the same body. The code is stable and lets your system decide what to do; the message is for people and may change.

The message comes in the language asked for in the Accept-Language header or the ?lang= parameter: pt-BR, en or es (any other language gets English). Without one, it comes in the language of whoever created the account.

{
  "error": {
    "code": "invalid_request",
    "message": "The to field is required.",
    "details": { "to": ["The to field is required."] }
  }
}

When the failure comes from Meta, on an official number, the error also has meta: Meta's error as Meta gave it, with code (131047 outside the 24-hour window, 190 invalid token, 1380xx for calls…), subcode, type, details and fbtrace_id. When it comes from the engine of QR code numbers, meta.reason says why: not_on_whatsapp, lines_busy, lines_starting, restarting, not_linked, not_connected, timeout…

{
  "error": {
    "code": "whatsapp_refused",
    "message": "Meta did not accept the request: Message failed to send because more than 24 hours have passed.",
    "meta": {
      "code": 131047,
      "subcode": null,
      "type": "OAuthException",
      "details": "Message failed to send because more than 24 hours have passed since the customer last replied to this number.",
      "fbtrace_id": "AbCdEf123"
    }
  }
}
StatusCodeWhen it happens
401invalid_api_keyThe key is missing, invalid or revoked.
401invalid_webphone_tokenThe web phone token is missing, invalid or expired.
403trial_expiredThe free trial has ended: subscribe to a plan in the dashboard.
403subscription_inactiveThe subscription ended (canceled or unpaid): subscribe again in the dashboard.
403account_suspendedThe account is suspended.
403number_limit_reachedThe account already has all of the plan's numbers.
404not_foundThe address or the resource does not exist (or belongs to another account).
404call_not_foundThe call does not exist or has already ended.
405method_not_allowedThe address exists, but not with this HTTP method. The Allow header lists the ones that work.
409number_not_connectedThe QR code number is not connected: scan the QR code before sending or calling.
409meta_token_invalidMeta refused the official number's token (it expired, was revoked or lacks the permissions). Send a new one to PUT /v1/numbers/{id}/meta.
409not_a_qr_numberThe request only applies to QR code numbers.
409not_an_official_numberThe request only applies to official numbers.
409send_in_progressAnother send with the same Idempotency-Key is still in progress.
409action_in_progressAnother call action with the same Idempotency-Key is still in progress.
409call_answered_elsewhereAnother webphone, the voice agent or your system has already answered this call.
409call_not_readyThe call is still being prepared; try again in a moment.
413media_too_largeThe media received by a QR code number was over 64 MB and was not stored.
422invalid_requestThe data sent is invalid. The details field lists each field's errors.
422unsupported_typeThe message type only exists for official numbers (template, interactive…).
422meta_refusedMeta refused the credentials when connecting an official number; the message says why.
422whatsapp_refusedMeta refused an official number's message (template that does not exist, invalid parameter…).
422not_on_whatsappThe number called is not on WhatsApp.
422call_failedMeta did not accept the call action of an official number (no permission from the contact, for example).
429rate_limitedThe account's request limit (wait for the seconds in Retry-After) or Meta's sending limit on an official number (send more slowly).
429channel_limit_reachedThe account is already at the plan's limit of simultaneous calls.
429lines_busyAll of the number's lines are on a call.
4xxhttp_errorAnother HTTP error, such as a body that is too large (413). The status says which.
500server_errorInternal error. Try again; if the error continues, contact us.
502whatsapp_refusedWhatsApp did not accept the message from a QR code number (the destination is not on WhatsApp, for example).
502call_failedWhatsApp did not accept the call action of a QR code number.
502media_unavailableWhatsApp did not deliver the file of media received by a QR code number.
503number_reconnectingThe QR code number is reconnecting to WhatsApp. Try again in a moment.
503engine_unavailableThe service that keeps the QR code numbers is unavailable. Try again in a moment.
503meta_unavailableThe request did not reach Meta (no connection) or Meta answered that it is unavailable: nothing was done. Try again in a moment.
503lines_startingThe QR code number's call lines are starting. Try again in a few seconds.
504send_timeoutThe send took too long. Repeat it with the same Idempotency-Key.
504meta_timeoutMeta did not answer in time after receiving the request (a send or a call action): it may or may not have been carried out. Repeat it with the same Idempotency-Key.
504call_timeoutThe service that keeps the QR code numbers took too long to answer a call action.

A 503 usually clears in moments: try again, waiting a little longer each time. To find out whether Voza is down, see Voza status.

Limits

Idempotency

When sending a message, send the Idempotency-Key header (up to 128 letters, digits and _ . : -). Repeating the send with the same key returns the first response instead of sending again. Use it whenever your system retries a send that failed or got no response.

Idempotency-Key: order-4521-delivery-notice

Voza keeps the first response for 24 hours, for both kinds of number. A repeated send while the first is still in progress waits for it for up to 30 seconds; after that, the response is 409 send_in_progress, and you just try again with the same key.

Call actions (POST /v1/numbers/{id}/calls) take the same header: repeating with the same key returns the first answer instead of repeating the action (a connect does not call twice). One repeated while the first is still in progress gets 409 action_in_progress.

On an official number, 503 meta_unavailable means the request did not reach Meta, and 504 meta_timeout that it did, but Meta did not answer in time: the message may have been sent (or the action carried out). In both cases, repeat it with the same Idempotency-Key: an error is not kept, so the repetition goes to Meta; Meta has no idempotency of its own, and after a meta_timeout the contact may get the message twice.

QR code numbers

A QR code number is a regular WhatsApp or WhatsApp Business connected as a device, like on WhatsApp Web. The phone keeps working normally.

The number object

{
  "id": "01jbk3m8x2c9q4r7t6v5w0y1z3",
  "name": "Downtown Store",
  "external_ref": "canal-981",
  "type": "qr",
  "status": "connected",
  "status_reason": null,
  "phone": "5511987654321",
  "qr_url": "https://www.vozaapi.com/qr/…",
  "webhook_url": "https://your-system.com/webhooks/whatsapp",
  "agent_mode": "off",
  "connected_at": "2026-10-22T15:04:05Z",
  "created_at": "2026-10-22T15:01:10Z"
}
FieldDescription
external_refYour own reference for the number (up to 100 characters), such as the channel's id in your system. It comes back in the webhooks, in metadata.external_ref, so you find the channel without storing Voza's id.
typeqr (QR code) or meta (official).
status_reasonWhy the number is in its current status (see Status); empty when there is nothing to explain.
phoneThe number's digits, with the country code, once connected.
qr_urlA public page with the QR code, to send to whoever has the phone. It follows the connection on its own.
agent_modeWhen the number's voice agent answers: off, always or fallback (see Voice agents).
connected_atWhen the number connected, in UTC (ISO 8601).

Status

StatusMeaning
startingThe connection is starting; the QR code comes next.
qrWaiting for the QR code to be scanned.
connectedConnected: the number sends and receives messages and calls.
failedRefused: by WhatsApp (a blocked number, for example; use relink) or, for an official number, by Meta (see meta.error).

If someone disconnects Voza on the phone (Linked devices), the number goes back to starting and then to qr, with a new QR code. Each change reaches the webhook as number.status.

status_reason says why the number is in its current status, and a change of the reason alone also reaches the webhook:

status_reasonMeaning
logged_outThe phone removed the device (or the number was relinked): the QR code must be scanned again.
restartingThe engine is reconnecting the number, which is still linked to the phone: wait, without doing anything.
failedWhatsApp refused the device.
meta_refusedMeta refused the official number's token or the number (see meta.error).

In the other cases (qr, connected, a new number) the reason is empty.

Create a number

POST /v1/numbers
{
  "name": "Downtown Store",
  "external_ref": "canal-981",
  "webhook": { "url": "https://your-system.com/webhooks/whatsapp" }
}

Responds 201 with the number in starting. external_ref and webhook are optional (webhook_url is also accepted, instead of webhook.url). With a webhook in the request, the response also carries the secret that signs the events, so you can check the very first one:

{
  "id": "01jbk3m8x2c9q4r7t6v5w0y1z3",
  "status": "starting",
  …
  "webhook": { "url": "https://your-system.com/webhooks/whatsapp", "secret": "whsec_…" }
}

The webhook address may name the number: {id} becomes the number's id at Voza and {external_ref} your reference (URL-encoded; the request must have external_ref). The webhook is set before the number connects, so no event is lost, with no PUT afterwards.

{ "name": "Downtown Store", "external_ref": "canal-981", "webhook": { "url": "https://your-system.com/webhooks/voza/{external_ref}?numero={id}" } }

Above the plan's limit, it responds 403 with number_limit_reached.

Show the QR code

GET /v1/numbers/{id}/qr
{
  "status": "qr",
  "qr": "data:image/png;base64,iVBORw0KGgo…",
  "qr_url": "https://www.vozaapi.com/qr/…"
}

qr is a PNG image ready for an <img src>. The QR code changes every 20 seconds: check again every 3 to 5 seconds until the status is connected (then qr comes empty). Or send the qr_url to whoever has the phone.

List, get and delete

GET /v1/numbers

All of the account's numbers, in {"data": [...]}. With ?external_ref=canal-981, only those with that reference.

GET /v1/numbers/{id}

The number, with its status read on the spot.

PATCH /v1/numbers/{id}

Changes the number's name or external_ref ("external_ref": null removes it) and responds with the number.

DELETE /v1/numbers/{id}

Disconnects the phone and deletes the number. Responds 204.

Connect another phone

POST /v1/numbers/{id}/relink

Disconnects the current phone and starts again, with a new QR code. Use it to change phones or to recover a refused number.

Official numbers

An official number is a WhatsApp Cloud API number, in your Meta account. Voza uses your app's credentials: messages and calls go through the Meta API, and reach your webhook in the same format as QR code numbers.

Before connecting, at Meta

  1. Have a Meta app with the WhatsApp product and a phone number in the WhatsApp Business account (WABA).
  2. In WhatsApp › Configuration, subscribe to the messages and calls fields of the app's webhook (with any webhook address: Voza points only this number at itself, with a per-number override, and your app's other events stay where they are).
  3. Create a permanent token for a system user, with the whatsapp_business_messaging and whatsapp_business_management permissions.
  4. Write down the phone number ID and the WhatsApp Business account ID (WhatsApp › API Setup), and the app secret (App settings › Basic).

Connect

POST /v1/numbers
{
  "name": "Official support",
  "type": "meta",
  "webhook_url": "https://your-system.com/webhooks/whatsapp",
  "meta": {
    "phone_number_id": "109876543210987",
    "waba_id": "102030405060708",
    "access_token": "EAAG…",
    "app_secret": "0123456789abcdef0123456789abcdef"
  },
  "calling": { "call_icon_visibility": "DISABLE_ALL" }
}

Voza checks the number and the token at Meta, subscribes your app to the account, points the number's messages and calls webhooks at Voza and turns calls on. If everything works, it responds 201 with the number already connected:

{
  "id": "01jbk4…",
  "type": "meta",
  "status": "connected",
  "phone": "5511912345678",
  "qr_url": null,
  "meta": {
    "phone_number_id": "109876543210987",
    "waba_id": "102030405060708",
    "display_phone_number": "+55 11 91234-5678",
    "verified_name": "Your Company",
    "quality_rating": "GREEN",
    "calling": "enabled",
    "error": null
  }
}

Change the token

PUT /v1/numbers/{id}/meta
{ "access_token": "EAAG…new", "app_secret": "(only if it changed)" }

Stores the new credentials and connects the number again. POST /v1/numbers/{id}/relink only checks again, with the current credentials. DELETE gives the number's webhooks back to your app and takes it out of Voza (the number stays in your Meta account).

Neither a new token nor a relink touches the calling settings: Voza only sets them when the number is created.

Calling settings

GET /v1/numbers/{id}/calling
PUT /v1/numbers/{id}/calling

The calling object of the number's settings at Meta, read and changed as at Meta. With PUT, only the fields sent change, and the answer is {"success": true}.

{
  "calling": {
    "status": "ENABLED",
    "call_icon_visibility": "DEFAULT",
    "callback_permission_status": "ENABLED",
    "call_hours": {
      "status": "ENABLED",
      "timezone_id": "America/Sao_Paulo",
      "weekly_operating_hours": [{ "day_of_week": "MONDAY", "open_time": "0900", "close_time": "1800" }]
    }
  }
}
FieldDescription
statusENABLED or DISABLED: the number's calls.
call_icon_visibilityDEFAULT or DISABLE_ALL: the call button in the chat.
callback_permission_statusENABLED or DISABLED: ask a contact who called for permission to call back.
call_hours, call_iconsThe business hours and the countries that see the button, as at Meta, which checks them.

sip is left out: calls go through Voza. On a QR code number, the answer is 409 not_an_official_number.

Sending messages

The body is the same as in the official WhatsApp API. The number must be connected.

POST /v1/numbers/{id}/messages
curl https://www.vozaapi.com/v1/numbers/NUMBER_ID/messages \
  -H "Authorization: Bearer vz_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-4521" \
  -d '{
    "messaging_product": "whatsapp",
    "to": "5511987654321",
    "type": "text",
    "text": { "body": "Your order is out for delivery!" }
  }'
{
  "messaging_product": "whatsapp",
  "contacts": [{ "input": "5511987654321", "wa_id": "5511987654321" }],
  "messages": [{ "id": "3EB0C431D5A1B2C3" }]
}

Message types

TypeBodyNumbers
text{"text": {"body": "…"}} (up to 4096 characters)all
image, video{"image": {"link": "https://…", "caption": "…"}}all
audio{"audio": {"link": "https://…"}}; with "voice": true, a voice note (use OGG/Opus)all
document{"document": {"link": "https://…", "filename": "invoice.pdf"}}all
templateAn approved template: {"template": {"name": "…", "language": {"code": "pt_BR"}, "components": […]}}official
interactiveButtons, lists, links and the call permission request, as at Metaofficial
sticker, location, contacts, reactionAs in the Meta APIofficial

The 24-hour window (official numbers)

On an official number, Meta only delivers free-form messages to people who wrote to the number in the last 24 hours. Outside that window, start the conversation with a template approved in WhatsApp Manager:

{
  "to": "5511987654321",
  "type": "template",
  "template": {
    "name": "pedido_enviado",
    "language": { "code": "pt_BR" },
    "components": [{ "type": "body", "parameters": [{ "type": "text", "text": "4521" }] }]
  }
}

A free-form message outside the window is accepted by the API, but reaches the webhook as a failed status, with Meta's error.

Message templates

GET /v1/numbers/{id}/templates

The templates of an official number's WhatsApp Business account, read at Meta, one page at a time, in Meta's response. Each template carries id, name, language, category, status and components, or the fields asked for in fields.

{
  "data": [{
    "id": "1234567890",
    "name": "pedido_enviado",
    "language": "pt_BR",
    "category": "UTILITY",
    "status": "APPROVED",
    "components": [{ "type": "BODY", "text": "Seu pedido {{1}} saiu para entrega." }]
  }],
  "paging": {
    "cursors": { "before": "MAZDZD", "after": "MjQZD" },
    "next": "https://www.vozaapi.com/v1/numbers/NUMBER_ID/templates?after=MjQZD"
  }
}

Buttons

{
  "to": "5511987654321",
  "type": "interactive",
  "interactive": {
    "type": "button",
    "body": { "text": "Can you confirm delivery tomorrow at 10 a.m.?" },
    "action": { "buttons": [
      { "type": "reply", "reply": { "id": "confirm", "title": "Yes" } },
      { "type": "reply", "reply": { "id": "reschedule", "title": "Reschedule" } }
    ] }
  }
}

The contact's choice reaches the webhook as a message of type interactive, with the button's id.

Media

Received messages with an image, video, audio, document or sticker carry the file's id (in image.id, audio.id…). Download it with:

GET /v1/numbers/{id}/media/{media_id}

Webhooks

Each number has its own webhook. Voza sends it the messages received, the delivery statuses of the messages sent, the call events and the number's status changes.

PUT /v1/numbers/{id}/webhook
curl -X PUT https://www.vozaapi.com/v1/numbers/NUMBER_ID/webhook \
  -H "Authorization: Bearer vz_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-system.com/webhooks/whatsapp"}'
{ "url": "https://your-system.com/webhooks/whatsapp", "secret": "whsec_…" }

The address must be public https. Send "url": null to remove the webhook and, together with the url, "rotate_secret": true to change the secret. GET on the same address shows the current setup.

Messages and delivery statuses

They come in the same envelope as the official API, so the code that reads Meta's webhooks reads Voza's:

{
  "object": "whatsapp_business_account",
  "entry": [{
    "id": "42",
    "changes": [{
      "field": "messages",
      "value": {
        "messaging_product": "whatsapp",
        "metadata": { "display_phone_number": "5511987654321", "phone_number_id": "NUMBER_ID", "external_ref": "canal-981" },
        "contacts": [{ "wa_id": "5521999990000", "profile": { "name": "Bruno" } }],
        "messages": [{
          "from": "5521999990000",
          "id": "ABCD1234",
          "timestamp": "1761150000",
          "type": "text",
          "text": { "body": "Hi, how are you?" }
        }]
      }
    }]
  }]
}

A delivery status comes in the same envelope, in statuses:

"statuses": [{
  "id": "3EB0C431D5A1B2C3",
  "status": "delivered",
  "timestamp": "1761150030",
  "recipient_id": "5521999990000"
}]

Number status

{
  "object": "voza",
  "event": "number.status",
  "number": { "id": "NUMBER_ID", "name": "Downtown Store", "external_ref": "canal-981", "status": "starting", "status_reason": "logged_out", "phone": null },
  "timestamp": 1761150000
}

The test event, sent from the dashboard, has the same shape. status_reason says why the number changed (see Status).

Headers and signature

HeaderContent
X-Voza-Signaturesha256= and the HMAC-SHA256 of the body, with the webhook's secret. The same value goes in X-Hub-Signature-256, as at Meta.
X-Voza-DeliveryThe delivery's id, the same in every attempt: use it to ignore repeats.
X-Voza-Eventmessages, calls, number.status or test
User-AgentVoza-Webhooks/1.0

Verify the signature with the raw body, before reading the JSON:

// PHP
$body = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $body, getenv('VOZA_WEBHOOK_SECRET'));
if (! hash_equals($expected, $_SERVER['HTTP_X_VOZA_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}

// Node.js (body is the request's raw Buffer)
const expected = 'sha256=' + crypto.createHmac('sha256', process.env.VOZA_WEBHOOK_SECRET).update(body).digest('hex');
const given = req.headers['x-voza-signature'] || '';
const valid = given.length === expected.length &&
  crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));

# Python (body is the request's raw bytes)
expected = 'sha256=' + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
valid = hmac.compare_digest(expected, request.headers.get('X-Voza-Signature', ''))

Responses and retries

Web phone

The simplest way to answer and make calls: a JavaScript SDK, with no dependencies, that runs in your agents' browser. The API key stays on your server; the browser only gets a short-lived token for one number. It works with both kinds of number.

1. On your server, ask for a token for the agent:

POST /v1/numbers/{id}/webphone-tokens
curl https://www.vozaapi.com/v1/numbers/NUMBER_ID/webphone-tokens \
  -H "Authorization: Bearer vz_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"label": "Ana", "minutes": 480}'
{ "token": "wp_…", "expires_at": "2026-10-23T01:00:00Z", "sdk_url": "https://www.vozaapi.com/sdk/webphone.js" }

minutes goes from 5 to 720 (8 hours by default). Use one token per browser tab.

2. On the agent's page, the ready-made phone:

<script src="https://www.vozaapi.com/sdk/webphone.js"></script>
<voza-phone token="wp_…" lang="en"></voza-phone>

Or your own interface, with the SDK's API:

const phone = new VozaWebphone({ token: 'wp_…' });
phone.on('incoming', (call) => {
  // call.contact, call.contactName; call.accept() or call.reject()
});
phone.on('call', (call) => console.log(call.direction, call.state));
phone.on('error', (error) => console.warn(error.code, error.message));
phone.on('disconnected', () => { /* the token expired: ask for another */ });
await phone.connect();

const call = await phone.call('5511987654321');
call.mute(true);
call.hangup();

phone.disconnect(); // stops following calls and hangs up the current ones
EventWhen it happens
readyThe phone is ready. It receives the session, with the number (see Web phone API).
stateThe phone changed state: connecting, ready or closed.
incomingAn incoming call started ringing.
callAny change in a call: a new state or mute.
endedA call ended; call.reason says why.
monitorA supervision opened with phone.monitor() changed state (see Supervising an AI call).
errorAn error, with error.code: microphone_denied, microphone_unavailable, insecure_context, unsupported_browser, connection_failed, network_error or an API code, such as not_on_whatsapp.
disconnectedThe token expired or was refused: get another one and create a new phone.

Supervising an AI call

A supervisor listens, in the browser, to a call the voice agent answered, and may take it over and give it back to the AI. Joining needs only a short token; taking over, giving back and whispering need the API key, so the SDK calls functions of your page, and they ask your server.

1. On your server, ask for a supervision token with POST /v1/numbers/{id}/calls/{call_id}/monitor and pass {token, ice_servers} to the browser. Each token joins once, within 5 minutes.

2. In the browser, with the SDK’s API:

// your function that calls your server
const post = (path, body) => fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });

const monitor = await phone.monitor(callId, { // or VozaWebphone.monitor(), without a webphone
  token: 'mon_…',
  iceServers, // the token’s ice_servers
  takeover: ({ callId, monitorId }) => post('/supervise/takeover', { callId, monitorId }),
  release: ({ callId, monitorId }) => post('/supervise/release', { callId, monitorId }),
  whisper: ({ callId, text }) => post('/supervise/whisper', { callId, text }),
});
monitor.on('state', (m) => console.log(m.state)); // listening, takeover, ended
await monitor.takeover(); // asks for the microphone and speaks to the caller
await monitor.whisper('Offer Friday at 9:30.');
await monitor.release(); // the AI speaks again
monitor.leave();

3. Or in the ready-made phone: with the functions in supervisor, a call the AI answered shows “Answered by AI” and the Listen button; the supervision has Take over, Give back to AI, Mute, Leave and a field for an instruction to the AI.

const element = document.querySelector('voza-phone');
element.supervisor = {
  token: (call) => post('/supervise/token', { callId: call.id }), // your server returns {token, ice_servers}
  takeover: ({ callId, monitorId }) => post('/supervise/takeover', { callId, monitorId }),
  release: ({ callId, monitorId }) => post('/supervise/release', { callId, monitorId }),
  whisper: ({ callId, text }) => post('/supervise/whisper', { callId, text }),
};
// or, for a call you picked:
element.monitor(callId, { token: 'mon_…', iceServers });

Calls through the API

For those who prefer their own WebRTC client: the same actions and events as Meta's calling API. Voza does not record calls.

POST /v1/numbers/{id}/calls
ActionBody
connectCalls to with the browser's SDP offer in session (sdp_type: offer). Responds {"calls": [{"id": "…"}]}; the SDP answer comes in the connect event.
pre_acceptFor an incoming call: the browser's SDP answer (sdp_type: answer), so the audio connects right away.
acceptAnswers the call on WhatsApp. For official numbers, send the same SDP answer again in session.
reject, terminateDeclines or ends the call call_id.
{
  "action": "pre_accept",
  "call_id": "wacid.ABGG…",
  "session": { "sdp_type": "answer", "sdp": "v=0\r\no=- …" }
}

Permission to call

GET /v1/numbers/{id}/call-permissions/{wa_id}

Whether the number may call the contact now. On an official number, it is the answer of Meta's call_permissions lookup; on a QR code number, always {"permission": {"status": "granted"}}, since there is no permission to ask for.

{
  "messaging_product": "whatsapp",
  "permission": { "status": "temporary", "expiration_time": 1761236400 },
  "actions": [{ "action_name": "start_call", "can_perform_action": true, "limits": [ … ] }]
}

status is granted, temporary (expires at expiration_time) or no_permission. wa_id is the contact's number, in digits only.

Events

Events reach the number's webhook with field: calls, in Meta's envelope: calls[] with event: connect (the SDP offer of an incoming call, or the answer to an outgoing call) and event: terminate (with duration when it was answered), and statuses[] of an outgoing call (RINGING, ACCEPTED, REJECTED). direction is USER_INITIATED on an incoming call and BUSINESS_INITIATED on an outgoing one.

"value": {
  "messaging_product": "whatsapp",
  "metadata": { "display_phone_number": "5511987654321", "phone_number_id": "NUMBER_ID" },
  "contacts": [{ "wa_id": "5521999990000", "profile": { "name": "Bruno" } }],
  "calls": [{
    "id": "wacid.ABGG…",
    "from": "5521999990000",
    "to": "5511987654321",
    "event": "connect",
    "direction": "USER_INITIATED",
    "timestamp": "1761150000",
    "session": { "sdp_type": "offer", "sdp": "v=0\r\no=- …" }
  }]
}

When the call ends:

"calls": [{ "id": "wacid.ABGG…", "event": "terminate", "direction": "USER_INITIATED", "status": "COMPLETED", "duration": 126, … }]

Call log

GET /v1/numbers/{id}/calls

The number's calls, newest first: ?limit= from 1 to 200, 50 by default.

{
  "data": [{
    "id": 981,
    "call_id": "wacid.ABGG…",
    "direction": "incoming",
    "contact": "5521999990000",
    "contact_name": "Bruno",
    "status": "completed",
    "end_reason": null,
    "end_detail": null,
    "started_at": "2026-10-22T15:20:00Z",
    "answered_at": "2026-10-22T15:20:04Z",
    "ended_at": "2026-10-22T15:22:10Z",
    "duration": 126,
    "answered_by": "agent",
    "agent": { "agent_ref": "agent_42", "session_id": "…", "seconds": 124 }
  }]
}
FieldDescription
call_idThe call's id on WhatsApp, the same as in the events and the actions.
statusdialing, ringing, active, completed, missed, rejected, no_answer or failed.
end_reasonchannel_limit: declined for going over the simultaneous calls limit. agent_unavailable: declined because the voice agent could not answer it. lost: had no end for over 6 hours and was closed as failed. Empty for the others.
end_detailWith agent_unavailable, why the agent declined the call (see Who answers the call). Empty for the others.
answered_atWhen it was answered; empty for an unanswered call. Times are in UTC.
durationThe seconds of conversation; empty for an unanswered call.
answered_byWho answered: webphone, agent (the voice agent) or api (your system); on an outgoing call, who placed it. Empty on a call nobody answered.
agentFor a voice agent's call: agent_ref, the session in the runtime and the AI's seconds (without the time a supervisor had the call).

Web phone API

What the SDK uses underneath, for those who build their own phone in the browser. These addresses take the web phone token, not the API key, and answer pages on any domain (CORS). The token goes in Authorization: Bearer wp_… or, in the event stream, in ?token=, because EventSource does not send headers.

GET /v1/webphone/session
{
  "session_id": "01jbk5…",
  "label": "Ana",
  "number": { "id": "01jbk3…", "name": "Downtown Store", "phone": "5511987654321", "status": "connected" },
  "ice_servers": [{ "urls": ["turn:turn.cloudflare.com:3478?transport=udp", …], "username": "…", "credential": "…" }],
  "expires_at": "2026-10-23T01:00:00Z"
}

ice_servers goes straight into new RTCPeerConnection({ iceServers }). Ask for the session again before each call: the TURN credentials expire.

GET /v1/webphone/events

The number's calls, as Server-Sent Events. The calls event carries the same value as the calls webhook (contacts, calls and statuses, with the SDP). The claimed event says someone answered a call, with call_id, session_id and by (webphone, agent or api); if the session_id is not yours, it was answered elsewhere.

const events = new EventSource('https://www.vozaapi.com/v1/webphone/events?token=wp_…');
events.addEventListener('calls', (event) => {
  const { calls, statuses } = JSON.parse(event.data);
});
events.addEventListener('claimed', (event) => {
  const { call_id, session_id, by } = JSON.parse(event.data);
});
POST /v1/webphone/calls

The call actions on the token's number, with the same body as POST /v1/numbers/{id}/calls. When several phones get the same call, the first pre_accept or accept keeps it, and the others get 409 call_answered_elsewhere. The same goes against the voice agent and your system with the API key (see Voice agents).

Voice agents

A voice agent answers a number's calls with AI: it listens, thinks and speaks. It works on numbers linked by QR code and on official ones, with the same API. The logic is your product's: Voza keeps no prompt and no configuration, only which agent of your product answers the number and where to ask. At the start of each call, Voza's agents runtime asks your product for the configuration, and the tools (calendar, knowledge base, messages) are also calls to your product. A person can always listen, guide the AI and take over the call.

Voza keeps no content: the transcript and the summary go to your product in the events, and stay at Voza only until they are delivered. The agents' runtime keeps only the audio of the agent's fixed phrases (the greeting, the filler phrases and the handoff messages), to play them without waiting for the synthetic voice; a phrase not played for 30 days is removed, and deleting the account deletes its phrases. The AI providers' keys are your account's own.

A number's agent

PUT /v1/numbers/{id}/agent
{
  "mode": "always",
  "fallback_after_seconds": 15,
  "agent_ref": "agent_42",
  "callback_url": "https://your-system.com/voza/agents",
  "events_url": "https://your-system.com/voza/events"
}
FieldDescription
modealways: the agent answers every incoming call. fallback: the webphones ring first and, if nobody answers within fallback_after_seconds (5 to 120, 15 by default), the agent answers. off: the agent only answers the calls your product hands to it.
agent_refThe agent in your product (up to 255 characters). Voza repeats it in every request and event.
callback_urlWhere the runtime asks for the configuration (agent.config) and calls the tools (tool.call). https and public.
events_urlOptional: where the agent's events go. Without it, they go to the number's webhook.

The first time, the answer carries agent.secret: it signs the runtime's requests to your product and the events sent to events_url. Keep it: GET on the same address shows the configuration without it. POST /v1/numbers/{id}/agent/secret creates a new one (the previous one stops working at once) and DELETE switches the agent off and forgets the secret.

{ "agent": { "mode": "always", "fallback_after_seconds": 15, "agent_ref": "agent_42", "callback_url": "https://…/voza/agents", "events_url": "https://…/voza/events", "secret": "agsec_…" } }

Who answers the call

An incoming call stays with whoever answers first: a webphone, the agent or your system with the API key (pre_accept or accept in POST /v1/numbers/{id}/calls). The others get 409 call_answered_elsewhere, and the webphones get the claimed event with by: webphone, agent or api. The call log shows who answered in answered_by.

With the agent in always, as soon as the call arrives the agent takes it and the runtime answers the SDP offer; the webphones see it ring and then the claimed. The number's webhook gets the calls events as always. If the runtime cannot answer (your product did not send the configuration and none was stored, an AI key is missing…), the call is declined and stays in the log with end_reason: agent_unavailable. In fallback, it keeps ringing on the webphones.

Your product may also refuse a call: if it answers agent.config with 409 or 410 and {"error": {"code": "agent_unavailable"}}, the runtime does not use the cached configuration and does not answer. In always, the call is rejected (end_reason: agent_unavailable); in fallback, it keeps ringing on the webphones; handed over through the API, the answer is 409 agent_unavailable.

A blocked account (trial over, subscription ended or suspended) has no agent, as it has no API and no webphone: the call reaches the number's webhook and rings until the caller gives up.

When the agent cannot take a ringing call, your product gets the call.agent.declined event, with the reason in reason, the refusal's code in detail, the number's mode in mode and, in outcome, what became of the call: rejected (declined, in always) or ringing (it goes on ringing on the webphones, in fallback). A declined call stays in the log with end_reason: agent_unavailable and the reason in end_detail. A call handed over through the API gets no event: your product gets the error.

reasonWhen it happens
product_refusedYour product refused the call (it answered agent.config with 409 or 410 and agent_unavailable).
config_unavailableYour product did not send a valid configuration in time, and the runtime had none kept.
missing_provider_keyA key of an AI provider the agent uses is missing.
invalid_offerThe runtime could not answer the SDP offer.
at_capacity, drainingThe agents' runtime is at its call limit, or restarting (Voza asks again once, within the same deadline).
account_limitThe account already has as many simultaneous agent sessions as it may (limits.agent_sessions in GET /v1/account).
runtime_unavailable, runtime_errorThe agents' runtime did not answer in time, or failed.
whatsapp_refusedMeta or the QR engine refused the agent's answer (pre_accept).
errorAnother failure at Voza.

Each account has a limit of agent sessions at the same time, counting the calls the agents answer and the test calls, and a lower limit for test calls only: limits.agent_sessions and limits.agent_tests in GET /v1/account (10 and 2 by default; Voza sets a partner account's). Past it, an always call is declined (account_limit), a fallback one goes on ringing on the webphones, and the API answers 429 agent_limit_reached.

POST /v1/numbers/{id}/calls/{call_id}/agent

Hands an incoming call that is still ringing to the agent: your product decides call by call. {"agent_ref": "…"} is optional (another agent of your product). It answers {"success": true, "call": {…}}, or 409 call_answered_elsewhere if someone already answered and 409 call_not_ringing if it has ended. call_id is the call's id at WhatsApp; its id at Voza works too.

What the runtime asks your product

POST requests to the callback_url, with X-Voza-Event (agent.config or tool.call), X-Voza-Timestamp (Unix seconds) and X-Voza-Signature: sha256=…, the HMAC-SHA256 of {timestamp}.{body} (the timestamp, a dot and the body as it arrived) with the agent's secret. Check the signature and refuse an X-Voza-Timestamp more than 5 minutes away from your clock: that way a captured request cannot be replayed later.

$expected = 'sha256='.hash_hmac('sha256', $timestamp.'.'.$body, $secret);
$valid = hash_equals($expected, $signature) && abs(time() - (int) $timestamp) <= 300;

At the start of each call, the configuration (answer within 2 seconds):

{ "type": "agent.config", "agent_ref": "agent_42", "call": { "id": "01jbk5…", "direction": "incoming", "test": false }, "number": { "id": "NUMBER_ID", "phone": "5511987654321", "type": "qr" }, "contact": { "wa_id": "5521999990000", "name": "Bruno" } }
{
  "version": "42.3",
  "language": "pt-BR",
  "greeting": { "text": "Hello, this is the virtual assistant of Smile Clinic. How can I help?", "interruptible": false },
  "instructions": "Book, reschedule and cancel appointments. Confirm the date out loud.",
  "guardrails": ["Never give medical advice."],
  "voice": { "provider": "elevenlabs", "voice_id": "…", "speed": 1.0 },
  "stt": { "provider": "deepgram", "language": "multi" },
  "llm": { "provider": "anthropic", "model": "claude-haiku-5-5" },
  "turn": { "interruption": "medium", "silence_ms": 600 },
  "tools": [{
    "name": "find_slots",
    "description": "Free slots in the calendar",
    "parameters": { "type": "object", "properties": { "day": { "type": "string" } }, "required": ["day"] },
    "filler": "One moment, let me check the calendar."
  }],
  "handoff": {
    "on_request": true,
    "message": "I'll pass you to the team, one moment.",
    "timeout_seconds": 45,
    "timeout_message": "Nobody from the team can take the call right now. May I take a message?",
    "timeout_action": "resume"
  },
  "error_message": "Sorry, I am having a technical problem. I will pass you to the team.",
  "max_duration_seconds": 900
}

The runtime keeps the last configuration of each agent by its version: if your product does not answer in time, it uses the stored one. Each tool is called with {"type": "tool.call", "call_id", "agent_ref", "tool", "arguments"} and has 8 seconds to answer {"result": …}, with optional say (a phrase to say), end_call: true or handoff: true. The knowledge base is a tool like any other.

When a provider fails

If speech-to-text, the AI or the voice fails in the middle of a call (the provider refused the account's key, keeps failing, or the AI does not answer a turn within 20 seconds), the caller is not left in silence: your product gets call.agent.error, the agent says error_message (or, without it, the handoff message; when the voice itself failed, only a phrase already kept, or else a tone) and hands the call to the team, when the agent has handoff.on_request, or ends it (reason: provider_error). The AI does not answer again in that call: if the team gives it back, or nobody takes it, it ends.

{ "service": "llm", "provider": "anthropic", "status": 401, "category": "authentication", "action": "handoff" }

service is stt, llm or tts; status, the provider's HTTP status, when there is one; category, the kind of failure (authentication, rate_limit, quota, server, timeout…); action, handoff or end.

The agent's events

They go to the agent's events_url, signed with the agent's secret, or to the number's webhook, signed with the webhook's secret. They have object: voza and the event's name in event and in X-Voza-Event. They go out on a queue of their own, and a call's events go out in order, one after the other: an event that failed holds back the next ones of the same call until its retry. Retries come after 5, 15 and 30 seconds, for up to 2 minutes; call.agent.ended (with the whole conversation), call.agent.error and call.agent.declined go on after that, for as long as message events.

{
  "object": "voza",
  "event": "call.transcript",
  "event_id": "5f2c…",
  "number": { "id": "NUMBER_ID", "name": "Downtown Store", "external_ref": null, "status": "connected", "status_reason": null, "phone": "5511987654321" },
  "call": { "id": "01jbk5…", "call_id": "wacid.ABGG…", "direction": "incoming", "contact": "5521999990000", "contact_name": "Bruno", "agent_ref": "agent_42", "agent_session_id": "…" },
  "test": false,
  "data": { "speaker": "caller", "text": "I'd like to reschedule Friday's appointment.", "start": 4.2, "end": 6.9 },
  "timestamp": 1761150012,
  "timestamp_ms": 1761150012345
}
EventWhenIn data
call.agent.startedThe agent answered.call, number, contact, agent_ref and the configuration's version (config_version).
call.agent.stateOn every change.state: listening, thinking, speaking, handoff, handoff_timeout (nobody took the call over in time) or supervised.
call.transcriptOn every complete utterance.speaker (caller, agent or supervisor), text, start and end in seconds since the answer.
call.agent.tool_callOn every tool.name, duration_ms and result (ok, timeout, error); without the arguments.
call.metricsOn every turn.The latencies in milliseconds: stt_ms, llm_ms (to the first word), tts_ms and end_to_end_ms.
call.supervisionA supervisor joins, changes mode, whispers or leaves.action, monitor_id and mode.
call.agent.errorA provider failed in the middle of the call.service, provider, status, category and action (see When a provider fails).
call.agent.declinedThe agent could not take a call that was ringing.reason, detail, mode and outcome (see Who answers the call).
call.agent.endedThe end.reason, duration_seconds, the AI's seconds (ai_seconds) and a supervisor's with the call (supervised_seconds), the whole transcript (transcript), the tools used (tools) and the usage (usage).

timestamp is the moment of the event in seconds, and timestamp_ms the same moment in milliseconds: use it to order events of the same second.

event_id is the same on every attempt: use it so you never process an event twice. The calls the agent answered and the AI's seconds count in the usage (agent_calls and agent_seconds in GET /v1/account/usage).

The reason of call.agent.ended: remote_ended (the caller hung up), agent or tool (the agent ended it), handoff_timeout, max_duration, max_call_duration, idle, media_closed, connect_timeout, accept_failed, provider_error (a provider failed), agent_removed, number_removed and account_blocked (the agent or the number was removed, or the account blocked: the call ends at once), runtime_lost (the agents' runtime went down in the middle of the call: Voza ends the call and sends this event, without the transcript) or shutdown.

Supervision

Your product decides who may supervise; Voza only checks the API key. Ending the call is still the terminate action.

POST /v1/numbers/{id}/calls/{call_id}/monitor

A short token (mon_…, 5 minutes, once) for the supervisor's browser, with the ice_servers. The browser sends its SDP offer to POST /v1/monitor/session with Authorization: Bearer mon_… and gets the runtime's answer and the monitor_id. mode is listen (listens; the default) or takeover (joins speaking). This address accepts pages from any domain (CORS).

{ "session": { "sdp_type": "offer", "sdp": "v=0\r\no=- …" }, "mode": "listen" }
{ "monitor_id": "mon_1", "call_id": "wacid.ABGG…", "session": { "sdp_type": "answer", "sdp": "v=0\r\no=- …" } }
AddressBodyWhat it does
POST …/calls/{call_id}/whisper{"text": "Offer Friday at 9:30."}The text goes into the AI's context; the caller does not hear it.
POST …/calls/{call_id}/takeover{"monitor": "mon_1"}The AI stops speaking and that supervisor's microphone goes to the call.
POST …/calls/{call_id}/release{}Gives the call back to the AI.
DELETE …/calls/{call_id}/monitor/{monitor_id}Removes a supervisor from the call; if they had it, the AI speaks again.

In the browser, the webphone SDK does the supervisor’s part: see Supervising an AI call.

Test call

POST /v1/agents/test-sessions

Talk to an agent from the browser, without WhatsApp: send {"agent_ref", "callback_url"} and get {id, token, ice_servers, expires_at}. The browser sends the SDP offer to POST /v1/agents/test-sessions/connect with Authorization: Bearer agt_… (once) and gets the runtime's answer. With number_id, the runtime signs with that number's agent secret, and the events go out with "test": true wherever the number's go; without it, the answer carries a secret for this call only, and the events are not sent. DELETE /v1/agents/test-sessions/{id} ends it.

A test call lasts at most 10 minutes and counts in the account's agent sessions: past limits.agent_tests test calls at the same time, connect answers 429 test_session_limit_reached (and 429 agent_limit_reached past limits.agent_sessions). The token can be used again after such a refusal.

Voice and AI keys

PUT /v1/account/ai-keys

Stores the account's AI providers' keys, encrypted: anthropic, deepgram, elevenlabs and cartesia. Only the providers sent change, and null removes a key. Only an API key created by one of the account's owners can change them (otherwise, 403 owner_only). They are never given back: the answer, like GET, only says which exist.

Before saving, Voza checks each new key at the provider itself, at an address that costs nothing (the list of models, projects or voices). If the provider refuses a key (401), no key of the request is saved, and the answer is 422 invalid_provider_key, with the providers in meta.providers. A provider that is down, slow, or takes the key without the permission for that list does not stop the key.

{ "anthropic": "sk-ant-…", "deepgram": "…" }
{ "anthropic": true, "deepgram": true, "elevenlabs": false, "cartesia": false }
POST /v1/tts

{"provider", "voice_id", "text", "speed"} → the audio (audio/mpeg, or audio/wav with "format": "wav"), with the provider's key stored in the account. Use it to hear the greeting before publishing it. The text is not kept; a fixed phrase an agent's call already kept (the same voice and text) comes back without waiting (X-Voza-Cache: hit).

GET /v1/voices?provider=elevenlabs

The account's voices at a provider (elevenlabs or cartesia), read with the key stored in the account, so its cloned voices come too. Each voice has id (the voice_id of the configuration and of /v1/tts), name, languages, gender and accent when the provider says, and preview_url. The list is kept for an hour, per account and provider.

{ "provider": "elevenlabs", "data": [{ "id": "21m00Tcm4TlvDq8ikWAM", "name": "Rachel", "languages": ["en", "pt-BR"], "gender": "female", "accent": "american", "preview_url": "https://…/preview.mp3" }] }

Agent errors

StatusCodeWhen it happens
401invalid_monitor_token, invalid_test_tokenThe supervision or test call token is missing, invalid or expired.
403owner_onlyOnly an API key created by an owner can store the AI keys.
404monitor_not_foundThere is no supervisor with this id on the call.
409agent_not_configuredThe number has no agent.
409agent_unavailableYour product refused the call (it answered agent.config with 409 or 410 and agent_unavailable).
409call_not_ringingThe call is no longer ringing.
409call_not_with_agentThe call is not with the agent: someone else answered it, it ended, or it has not been answered yet.
409token_usedThe supervision or test call token has already been used.
409too_many_monitors, not_in_controlThe call already has all the supervisors it takes, or another supervisor has taken it over.
422missing_provider_keyThe key of a provider the agent uses is missing: store it with PUT /v1/account/ai-keys.
422invalid_offerThe runtime could not answer the SDP offer.
422invalid_provider_keyA provider refused the key sent in PUT /v1/account/ai-keys.
429agent_limit_reached, test_session_limit_reachedThe account already has as many simultaneous agent sessions, or test calls, as it may.
502agent_config_unavailableYour product did not send the configuration in time, and the runtime had none stored.
502tts_failedThe voice provider did not produce the audio.
502voices_failedThe voice provider did not return the list of voices (a refused key, for instance).
503agents_unavailable, agents_busyThe agents' runtime did not answer or is at its call limit. meta.reason carries the reason the runtime gave.

Account, plan and usage

GET /v1/account
{
  "id": 42,
  "name": "Your Company",
  "status": "active",
  "trial_ends_at": null,
  "partner": false,
  "plan": "pro",
  "subscription": { "status": "active", "currency": "brl", "renews_at": "2026-11-23T14:30:00Z", "ends_at": null },
  "limits": { "numbers": 5, "channels": 10, "requests_per_minute": 600, "agent_sessions": 10, "agent_tests": 2 },
  "webhook_retry_hours": 24
}
FieldDescription
statustrial (free trial), active (active), suspended (suspended) or canceled (subscription ended).
planstart, pro, scale, or empty without a subscription.
subscriptionThe subscription at Stripe: status (active, past_due when a payment failed, canceled…), the currency and the dates. Empty without a subscription.
limitsHow many numbers the account connects, how many simultaneous calls it can have, how many API requests it makes per minute and how many voice-agent sessions it has at the same time (agent_sessions, of which up to agent_tests test calls).
webhook_retry_hoursFor how many hours a failed message event is tried again: 24, or the period agreed with Voza, up to 168 (7 days).
partnerA partner account, of another product, with limits agreed with Voza: its calls are not declined over the simultaneous calls limit, because the product limits its customers' calls.

Subscribing is done in the dashboard (Plan and usage), by card, through Stripe. When the subscription ends, the API responds subscription_inactive until the account subscribes again.

GET /v1/account/usage?month=2026-10
{
  "month": "2026-10",
  "totals": { "messages_sent": 1840, "messages_received": 2311, "calls": 96, "call_seconds": 20412, "agent_calls": 40, "agent_seconds": 7310 },
  "numbers": [
    { "number_id": "01jbk3…", "name": "Downtown Store", "messages_sent": 1200, "messages_received": 1500, "calls": 60, "call_seconds": 14000, "agent_calls": 40, "agent_seconds": 7310 }
  ]
}

The month's usage in UTC, in total and per number (number_id empty for a number already deleted). Without month, the current month. agent_calls and agent_seconds are the calls a voice agent answered and the AI's seconds in them (without the time a supervisor had the call).

OpenAPI and Postman

The whole API is described in OpenAPI 3.1: https://www.vozaapi.com/docs/openapi.json?lang=en. Import it into Postman or Insomnia, or generate a client in your language.

The spec's texts come in the language of ?lang=: pt-BR, en or es.

Voza status

Voza's live status (site and API, database, webhooks and emails, retries, the numbers linked by QR code and the voice agents) is at https://www.vozaapi.com/status. For an uptime monitor, the same as JSON: https://www.vozaapi.com/status.json, which answers 503 when a service is down or late.

{
  "status": "operational",
  "checked_at": "2026-10-27T15:00:00Z",
  "components": [{ "key": "engine", "name": "QR code numbers", "status": "operational", "message": null }]
}

status is operational, degraded or down. Official numbers also depend on Meta's API, which this page does not track.

What's new

Changes that may break your integration are announced in advance and go into a new version of the address (/v2); /v1 only gains new fields and events.