Voice calls & voice OTP
Place outbound calls from your Sozuri voice number, read a one-time code to someone over the phone, and look up every call with its status, duration and price. Same API key and project name as SMS.
- Before you start
- Place a call
- Read a one-time code (voice OTP)
- Look up a call
- List recent calls
- List your voice numbers
- Call statuses
- Billing
- Error responses
Before you start
- Your project needs an active voice number. Request one in the dashboard under Project → Voice → Numbers; calls go out from that number, and it is the number your customers see.
- What a call does once it is answered is set on the number, under When someone calls this number: play a message, run a call flow (an IVR you build under Voice → Call flows), send the caller to your call centre, forward to another phone, or send it to your own URL (ask support for the format your server answers with). An outbound call placed through this API is handled the same way when the person picks up. A voice OTP call is the exception: it only reads the code.
-
Authenticate exactly as for SMS: your API key as
Authorization: Bearer <key>(or anapiKeyfield in the query string or body) plusproject, your project's name. See Authentication. -
Unlike the SMS API, every voice error comes back with a 4xx status and a machine-readable
code, so branch on the HTTP status:201means at least one call was placed.
Place a call
| Field | Required | Type | Description |
|---|---|---|---|
| project | Yes | String | Your project's name. |
| to | Yes | String or array | Who to call, in international format (254700000000). Several numbers as an array or a comma-separated string; at most 100 per request. Each number is its own call. |
| from | No | String | Which of your active voice numbers to call from. Leave it out to use your project's first active number. |
| tag | No | String | Up to 60 characters to group these calls by, e.g. reminder. Letters, digits, _ and - are kept (the first 40); filter on it later with List recent calls. |
curl -X POST https://sozuri.net/api/v1/voice/calls \
-H "Authorization: Bearer $SOZURI_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{ "project": "your_project_name", "to": ["254700000000", "254700000001"], "tag": "reminder" }'
201 Created, one entry per call placed:
{
"status": "ok",
"from": "+254711000000",
"testMode": false,
"calls": [
{ "callSid": "9b2f1c3e-0d4a-4e8b-9a51-6f0c2d7e8a10", "to": "254700000000", "status": "queued", "tag": "reminder_3f9a0c1b2d4e_1" },
{ "callSid": "0c7e5a12-8b3f-4d26-b1e4-2a9f6c0d3e57", "to": "254700000001", "status": "queued", "tag": "reminder_3f9a0c1b2d4e_2" }
]
}
callSidis the call's id: keep it to look the call up.tagis your tag plus a code for this request and the call's position in it, so every call can be told apart.-
Each number is dialled separately, so some can be refused while others ring. When that happens the answer is
still
201and carriesrejected, one entry per number that was not called:{ "to": "...", "code": "...", "message": "..." }. When no number could be called the answer is422with the first refusal'scodeandmessage. testModeistrueonly when the call was simulated and nobody's phone rang. It isfalseon a live project.
Read a one-time code (voice OTP)
Calls one number and reads the code one digit at a time, then repeats it: "Your Acme verification code is 4, 8, 2, 1. Again, 4, 8, 2, 1. Goodbye." The call is never recorded, and the code is not stored, logged or returned.
| Field | Required | Type | Description |
|---|---|---|---|
| project | Yes | String | Your project's name. |
| to | Yes | String | The one number to call, international format. Up to 32 characters. |
| code | Yes | String | The code to read, up to 12 characters. Only its digits are read; anything else is dropped. A code with no digits is refused (invalid_code). |
| brand | No | String | The name read before the code, up to 60 characters. Default Sozuri. |
| repeat | No | Integer | How many times the code is read, 1 to 3. Default 2. |
| voice | No | String | A text-to-speech voice: en-US-Standard-C (the default, female), en-US-Standard-B (male), en-GB-Standard-A, en-GB-Standard-B. A value we don't recognise uses the default. |
| from | No | String | Which of your active voice numbers to call from. Default: your project's first active number. |
curl -X POST https://sozuri.net/api/v1/voice/otp \
-H "Authorization: Bearer $SOZURI_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{ "project": "your_project_name", "to": "254700000000", "code": "4821", "brand": "Acme" }'
201 Created:
{
"status": "ok",
"callSid": "5d1e9f07-3a2b-4c6d-8e90-1f2a3b4c5d6e",
"to": "254700000000",
"from": "+254711000000",
"testMode": false
}
Look up a call
One call of your project, with its timeline. A callSid that is not your project's answers 404 not_found.
curl "https://sozuri.net/api/v1/voice/calls/9b2f1c3e-0d4a-4e8b-9a51-6f0c2d7e8a10?project=your_project_name" \
-H "Authorization: Bearer $SOZURI_API_KEY" -H "Accept: application/json"
{
"status": "ok",
"call": {
"callSid": "9b2f1c3e-0d4a-4e8b-9a51-6f0c2d7e8a10",
"direction": "outbound",
"from": "+254711000000",
"to": "254700000000",
"status": "completed",
"statusLabel": "Completed",
"durationSecs": 221,
"billableSecs": 240,
"price": 16.0,
"currency": "KES",
"tag": "reminder_3f9a0c1b2d4e_1",
"recordingUrl": null,
"createdAt": "2026-11-08T09:15:02+03:00",
"endedAt": "2026-11-08T09:18:51+03:00",
"events": [
{ "sequence": 1, "event": "start", "occurredAt": "2026-11-08T09:15:09+03:00" },
{ "sequence": 2, "event": "status", "occurredAt": "2026-11-08T09:18:51+03:00" },
{ "sequence": 3, "event": "billed", "occurredAt": "2026-11-08T09:18:51+03:00" }
]
}
}
| Field | Description |
|---|---|
| direction | outbound (you placed it) or inbound (someone called your number). |
| status / statusLabel | The call's state and its label: see Call statuses. |
| durationSecs | How long the call lasted once answered. null until it ends. |
| billableSecs, price, currency | What the call was charged, set when it ends (see Billing). null while it is in progress. |
| recordingUrl | The recording, when the number records calls. null otherwise. Voice OTP calls are never recorded. |
| events | The call's timeline, oldest first: e.g. start (answered), dtmf (a key pressed), flow (call-flow steps run), record, enqueue / dequeue (call centre), status (the final report) and billed. Only on this endpoint, not in the list. |
List recent calls
Your project's calls, newest first, in the same shape as a single call without events.
| Query parameter | Description |
|---|---|
| limit | How many to return, 1 to 200. Default 50. |
| status | Only calls in this state, e.g. completed or no_answer. An unknown value is ignored. |
| tag | Only calls whose tag starts with this, e.g. reminder for every call placed with that tag. |
curl "https://sozuri.net/api/v1/voice/calls?project=your_project_name&status=no_answer&tag=reminder&limit=20" \
-H "Authorization: Bearer $SOZURI_API_KEY" -H "Accept: application/json"
{ "status": "ok", "count": 1, "calls": [ { "callSid": "0c7e5a12-8b3f-4d26-b1e4-2a9f6c0d3e57", "direction": "outbound", "status": "no_answer", "...": "..." } ] }
List your voice numbers
Your project's numbers that are active, suspended or still being requested.
{
"status": "ok",
"numbers": [
{ "number": "+254711000000", "type": "regular", "label": "Main line", "status": "active", "handlerType": "flow", "aiMode": false }
]
}
| Field | Values |
|---|---|
| type | regular, tollfree, premium or sip. |
| status | active (can call and be called), suspended (paused, e.g. an unpaid monthly fee) or requested (not assigned yet). Only an active number can be used as from. |
| handlerType | What answers a call to the number: say (play a message), flow (a call flow), queue (the call centre), forward (another phone) or webhook (your own URL). |
| aiMode | Reserved for numbers answered by an AI agent. Always false today. |
Call statuses
status | statusLabel | Meaning |
|---|---|---|
queued | Queued | Accepted, not dialled yet. |
ringing | Ringing | Dialling, or ringing at the other end. |
in_progress | In progress | Answered; the call is live. |
on_hold | On hold | The caller is waiting in your call-centre queue. |
completed | Completed | Answered and ended normally. |
no_answer | No answer | Rang out, was rejected or busy, or the number could not be reached. |
failed | Failed | The call could not be placed. |
Billing
- A call is charged when it ends, per minute at your project's voice rate: the answered duration, at least one minimum call, rounded up to the next whole increment. A 3-minute-41-second call on a per-minute rate is charged as 4 minutes.
- A call nobody answered lasts 0 seconds and costs nothing.
- Before dialling, your balance must cover one minimum call, or the request is refused with
402 insufficient_balanceand nothing is dialled. - The charge is on the call itself:
billableSecs,priceandcurrency. - On a postpay account, calls are priced and added to your invoice instead of taken from a balance.
Error responses
A refusal from the voice API looks like this:
{ "status": "error", "code": "no_voice_number", "message": "This project has no active voice number. Request one in the dashboard." }
| HTTP | code | Meaning |
|---|---|---|
| 402 | insufficient_balance | Your balance does not cover one minimum call. Top up and retry. |
| 404 | not_found | No call with that callSid on your project. |
| 422 | no_voice_number | Your project has no active voice number, or from is not one of them. |
| 422 | too_many_recipients | More than 100 numbers in to. |
| 422 | no_recipients | to held no number once blanks were removed. |
| 422 | invalid_code | Voice OTP: code has no digits. |
| 422 | voice_disabled | Voice calling is switched off on the platform for now. Retry later. |
| 422 | provider_not_configured | Your number cannot place calls yet. Contact support. |
| 422 | other | None of the numbers could be called; message says why for the first one. |
Two refusals come before the request reaches the voice API, and have the shape every Sozuri API uses for
them: a wrong key or project name is 401, and a missing or malformed field is 422
with the fields listed:
{
"error_code": "VALIDATION_FAILED",
"message": "The submitted data failed validation.",
"retryable": false,
"errors": { "code": ["The code field is required."] }
}
Ready to start sending?
Create a free Sozuri project, grab your API key, and send your first message in minutes.