Connecting
REST API
The complete v1 API reference. Everything is JSON, errors share one shape, and money is always an integer in hundredths of a koruna - the Hal suffix on every price field - so no rounding decisions are left to you.
Authentication
Every request carries an API key in the Authorization: Bearer vk_... header. Get your key in the portal, under API and MCP (more detail in the Quickstart).
curl https://volai.cz/v1/balance \
-H "Authorization: Bearer vk_YOUR_KEY"Error format
A failure always has the same shape, no matter what broke:
{
"error": {
"code": "insufficient_credit",
"message": "Insufficient credit. Account balance is 3.00 CZK. Top up at volai.cz/en/credit and try again."
}
}These four can show up almost anywhere - the endpoint reference below only lists situational codes on top of them:
| HTTP | Code | Meaning |
|---|---|---|
| 401 | unauthorized | The Authorization header is missing or invalid. |
| 402 | insufficient_credit | Not enough credit left for this action. |
| 429 | rate_limited | Rate limit exceeded - wait as long as the Retry-After header says. |
| 500 | internal_error | An error on our side - please try again. |
400 (invalid input) and 404 (record not found) can also show up almost anywhere, but their code is specific to the field or endpoint - the exact list is always with the individual call below. A few actions can also return 502 (an error from an external provider - the phone network, ElevenLabs) or 503 (temporarily unavailable, try again shortly).
Idempotency
POST /v1/messages, POST /v1/calls, POST /v1/relay, POST /v1/numbers, POST /v1/agents, POST /v1/tools, and POST /v1/agents/{id}/test-call accept an Idempotency-Key header. Send the same value when retrying a request (typically after a timeout, when you don't know if the first attempt went through) and within 24 hours you get back exactly the same response as the first time - nothing gets sent or called twice. Feel free to use your own app's order ID.
curl -X POST https://volai.cz/v1/messages \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: objednavka-4471" \
-d '{"to": "+420777123456", "body": "Thanks for your order."}'Rate limits
60 requests per minute per API key (MCP shares the same limit - it uses the same key). Going over returns 429 and a Retry-After header with the number of seconds until the next attempt.
POST /v1/messages has its own extra cap on top of that: at most one SMS every 2 seconds, and 100 SMS per account per day.
Outbound calls (POST /v1/calls, relay, and test calls) share their own cap: 10 calls per minute and 200 per day per account, with at most 2 built-in-agent calls running at once.
Pagination
GET /v1/messages and GET /v1/calls take limit (max records to return - 50 by default, capped at 200) and before (a millisecond timestamp - only returns older records). For the next page, send the timestamp of the last record from the previous page as before.
What the API can't do
You control the whole phone side through API and MCP - numbers, calls, SMS, agents, webhooks, relay, and the do-not-call list. The account around it, though, stays in the portal, and that's by design, not an oversight. So your agent doesn't waste a call looking for an endpoint that isn't there:
| What | Where it is, and why |
|---|---|
| Adding credit | Portal /credit (one-off top-ups and auto-recharge alike). Paying by card is a human step - an agent that tops up its own credit so it can keep calling is exactly what we don't want. When an action hits insufficient_credit, tell the user and send them here. |
| API keys | Portal /api-and-mcp. A key minting another key would mean one leaked key grants permanent access that can never be revoked. |
| Account details | Portal /settings - name, billing address, password. |
| Tax invoices | Portal /credit, Invoices section - a downloadable PDF is waiting there for every credit top-up. The API can't issue or download an invoice: it's an accounting document with its own sequential numbering that never changes once issued and must never be created twice. |
| Billing details | Portal /settings - company name, Company ID, and VAT ID. These get printed on invoices issued AFTERWARD; an invoice already issued isn't amended retroactively, so this data can't be rewritten programmatically. |
| Inbound SMS | Nowhere - volai doesn't accept it today. More detail under the Messages section below. |
Going the other way, exactly one thing is missing: releasing a number you bought (DELETE /v1/numbers/{e164}) exists in both the REST API and the portal, but not among the MCP tools - an irreversible action is one misread sentence away from being too risky there.
Credit
balanceHal and every rate in this API are exclusive of VAT - volai is VAT-registered, but VAT is only applied when you top up credit by card in the portal (see the /pricing page), the API itself doesn't factor it in.
GET/v1/balance
The account's current credit balance.
Request
curl https://volai.cz/v1/balance \
-H "Authorization: Bearer vk_YOUR_KEY"Response
{
"balanceHal": 8730,
"balanceCzk": 87.30,
"currency": "CZK"
}Numbers
GET/v1/numbers
The phone numbers on the account, each with its current routing.
Request
curl https://volai.cz/v1/numbers \
-H "Authorization: Bearer vk_YOUR_KEY"Response
{
"numbers": [
{
"e164": "+420601234567",
"routing": { "mode": "agent", "agentId": "ag_kx91fa2b" },
"monthlyFeeHal": 2500,
"boughtAt": 1756111640000
}
]
}POST/v1/numbers
Buys a number. An empty body {} assigns any free number from the current listing, or send a specific { "e164": "..." } from GET /v1/numbers/available. Pick a region with { "region": "brno" } - allowed values are praha, brno, internet; a number from a different region is ordered through POST /v1/numbers/orders. This charges a monthly fee of 25.00 CZK (~EUR 1.04), and routing starts at none - set it right away with PATCH.
Request
curl -X POST https://volai.cz/v1/numbers \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{}'Response
{
"number": {
"e164": "+420601234567",
"routing": { "mode": "none" },
"monthlyFeeHal": 2500,
"boughtAt": 1756111640000
}
}Error codes
- 402
insufficient_creditYour credit doesn't cover the monthly fee of 25.00 CZK (~EUR 1.04). - 503
pool_emptyWe're out of numbers in the current listing - order one from another region (POST /v1/numbers/orders), or join the waitlist in the portal.
GET/v1/numbers/available
The listing of numbers available to buy (max 5 per region). numbers holds the numbers from the selected region (Prague if you skip the parameter), regions always returns the whole listing at once.
Request
curl "https://volai.cz/v1/numbers/available?region=brno" \
-H "Authorization: Bearer vk_YOUR_KEY"Response
{
"region": "brno",
"numbers": ["+420510510129", "+420510510132"],
"regions": [
{
"id": "praha",
"label": "Prague",
"description": "A landline with a Prague area code.",
"numbers": ["+420266266643", "+420266266645"]
},
{
"id": "brno",
"label": "Brno",
"description": "A landline with a Brno area code.",
"numbers": ["+420510510129", "+420510510132"]
},
{
"id": "internet",
"label": "Internet number",
"description": "Area code 910, not tied to any region - works from anywhere.",
"numbers": ["+420910084012"]
}
]
}GET/v1/numbers/address-options
Addresses for ordering a number from a region outside the listing. This walks the carrier's cascading directory: send psc and get municipalities, add obec and get districts, cobce returns streets, and ulice returns building numbers. Once cp is picked, a readable address arrives in the recap field. Don't invent codes - only the ones returned here are valid. A faster path that skips the cascade is query, see the callout below.
Request
curl "https://volai.cz/v1/numbers/address-options?psc=70200&obec=554821&cobce=413950" \
-H "Authorization: Bearer vk_YOUR_KEY"Response
{
"obce": [{ "value": "554821", "label": "Ostrava" }],
"casti": [{ "value": "413950", "label": "Moravská Ostrava" }],
"ulice": [{ "value": "353710", "label": "28. října" }],
"cp": [],
"recap": null
}Error codes
- 400
validationquery is longer than 200 characters, doesn't include a postal code, or the carrier has no matching option for the level it resolved to. - 502
provisioning_failedAddresses couldn't be loaded from the carrier at all. - 503
orders_disabledOrders are temporarily paused.
A faster path: the whole address in one query
Instead of the cascade, send query with the whole address (e.g. "Nadrazni 100, 702 00 Ostrava") - the server drives the cascade for you. Only the postal code is read directly out of the text; everything else is just matched against the options the carrier returned for that level - the server never invents a code.
curl -G https://volai.cz/v1/numbers/address-options \
-H "Authorization: Bearer vk_YOUR_KEY" \
--data-urlencode "query=Nádražní 100, 702 00 Ostrava"{
"obce": [],
"casti": [],
"ulice": [],
"cp": [],
"recap": "28. října 102/1, Ostrava, 70200",
"selection": {
"psc": "70200",
"obec": "554821",
"cobce": "413950",
"ulice": "353710",
"cp": "3180026"
}
}When the address is ambiguous, only the level where that becomes clear is returned (ambiguousLevel: obec/cobce/ulice/cp) with options to choose from in the matching field, for example:
{
"obce": [
{ "value": "554821", "label": "Ostrava" },
{ "value": "554813", "label": "Fulnek" }
],
"casti": [],
"ulice": [],
"cp": [],
"recap": null,
"selection": { "psc": "70200", "obec": "", "cobce": "", "ulice": "", "cp": "" },
"ambiguousLevel": "obec",
"message": "The given address is ambiguous - choose the municipality (obec) from the options. Repeat the query WITHOUT query and with the step parameters: psc=70200, obec=<value from the options>."
}selection always carries what was already resolved - continue with the same step parameter (psc/obec/cobce/ulice/cp) you would use without query. If the address can't be resolved at all, or the time budget runs out mid-lookup, a readable message arrives in the message field instead of recap, along with the selection resolved so far - continue from there with the step parameters.
query and the step parameters DO NOT MIX in one request - send both together and the endpoint returns validation. So after an ambiguous response, don't repeat the same query with an added obec (or another level) from the options - send the next request WITHOUT query, using only step parameters. The exact shape (including values already resolved) is spelled out right in the message field of that ambiguous response.
POST/v1/numbers/orders
Orders a number from a region outside the listing (Ostrava, Plzeň, Budějovice...). Address codes must come from GET /v1/numbers/address-options. The endpoint waits while we set up an emergency-services address with the carrier and buy the number - usually under a minute; the finished number then comes back in the e164 field with status done. If it doesn't work out on the first try, it returns the order with status pending and a cron job finishes it. The 25.00 CZK (~EUR 1.04) fee is only charged once the number is ready.
Request
curl -X POST https://volai.cz/v1/numbers/orders \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"psc":"70200","obec":"554821","cobce":"413950","ulice":"353710","cp":"3180026"}'Response
{
"order": {
"id": "Xa3Kd9pQmR2v",
"status": "pending",
"address": "PSČ 70200",
"createdAt": 1756111640000,
"updatedAt": 1756111640000
}
}Error codes
- 400
incomplete_addresspsc, obec, cobce or cp is missing (ulice is optional - not every address has one). - 402
insufficient_creditYour credit doesn't cover the monthly fee of 25.00 CZK (~EUR 1.04). - 503
orders_disabledOrders are temporarily paused.
GET/v1/numbers/orders
Order status: pending (queued), provisioning (being set up right now), done (the number is in the e164 field and belongs to the account), failed (the reason is in the error field, nothing was charged).
Request
curl https://volai.cz/v1/numbers/orders \
-H "Authorization: Bearer vk_YOUR_KEY"Response
{
"orders": [
{
"id": "Xa3Kd9pQmR2v",
"status": "done",
"address": "28. října 102/1, Ostrava, 70200",
"e164": "+420596123456",
"createdAt": 1756111640000,
"updatedAt": 1756118840000
}
]
}DELETE/v1/numbers/{e164}
Releases the number back into the pool - disconnects routing and any agent registration. The month already paid for is not refunded.
Request
curl -X DELETE "https://volai.cz/v1/numbers/+420601234567" \
-H "Authorization: Bearer vk_YOUR_KEY"Response
{
"released": true
}Error codes
- 404
not_foundThe number doesn't exist or doesn't belong to your account.
PATCH/v1/numbers/{e164}
Changes the number's routing.
mode: "agent"+agentId- the voice agent picks up calls.mode: "forward"+forwardTo(E.164) - call forwarding - you pay for both legs.mode: "sip"+sipUri- routes to your own SIP server (see SIP).mode: "none"- the number just receives calls without routing them anywhere.
Request
curl -X PATCH "https://volai.cz/v1/numbers/+420601234567" \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"routing": {"mode": "agent", "agentId": "ag_kx91fa2b"}}'Response
{
"number": {
"e164": "+420601234567",
"routing": { "mode": "agent", "agentId": "ag_kx91fa2b" },
"monthlyFeeHal": 2500,
"boughtAt": 1756111640000
}
}Error codes
- 400
validationrouting.mode requires the matching field (agentId / forwardTo / sipUri). - 404
not_foundThe number doesn't exist or doesn't belong to your account.
GET/v1/numbers/{e164}/sip
The number's SIP credentials - for your own softphone or PBX. Step-by-step setup is on the SIP.
Request
curl "https://volai.cz/v1/numbers/+420601234567/sip" \
-H "Authorization: Bearer vk_YOUR_KEY"Response
{
"server": "sip.volai.cz",
"username": "123456",
"password": "a1b2c3d4e5f6"
}Error codes
- 404
not_foundThe number doesn't exist or doesn't belong to your account.
Messages
Inbound SMS never reaches the API
Carriers deliver inbound SMS only to the SIM card, never to the API - so a reply to your message never shows up in GET /v1/messages or in a webhook. Don't rely on two-way SMS conversation yet.
GET/v1/messages
The history of sent SMS, newest first.
Request
curl "https://volai.cz/v1/messages?limit=20" \
-H "Authorization: Bearer vk_YOUR_KEY"Response
{
"messages": [
{
"id": "msg_7c1f9a2e",
"to": "+420777123456",
"from": "volai",
"body": "Ahoj z volai! - Moje Appka",
"status": "sent",
"priceHal": 136,
"segments": 1,
"createdAt": 1756111500000,
"source": "api"
}
]
}POST/v1/messages
Sends an SMS. Czech and Slovak numbers only (+420 / +421). Price is 1.36 CZK (~EUR 0.06) per segment - longer messages (or ones with characters outside the GSM-7 alphabet, typically Czech diacritics) split into more segments, each billed separately - see segments in the response.
The from field is always fixed ("volai") - carriers don't allow a custom SMS sender name. Put your own identity directly in the message text instead, like in the example below.
- Without diacritics (the GSM-7 alphabet), one segment holds 160 characters; longer text splits into chunks of 153.
- With diacritics or any character outside GSM-7 (UCS-2), the limit is 70 characters per segment; longer text splits into chunks of 67.
- Tip: to fit in a single segment, write without diacritics -
Prilis zlutoucky kuninstead ofPříliš žluťoučký kůň.
Request
curl -X POST https://volai.cz/v1/messages \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"to": "+420777123456", "body": "Ahoj z volai! - Moje Appka"}'Response
{
"id": "msg_7c1f9a2e",
"status": "sent",
"priceHal": 136,
"segments": 1
}Error codes
- 400
invalid_numberThat's not a valid Czech or Slovak phone number. - 400
invalid_bodyThe message text must be 1 to 765 characters. - 400
unsupported_countryNumber outside CZ/SK - not supported in the MVP. - 400
recipient_cannot_receive_smsLandline number - the SMS wouldn't arrive. We don't charge for it. - 402
insufficient_creditYour credit doesn't cover the cost of all the message's segments. - 429
rate_limitedMore than 1 SMS every 2 seconds, or over 100 SMS on the account today. - 502
send_failedThe phone network rejected the request. We didn't charge your credit - please try again. - 502
send_unknownWe handed the message to the network, but no delivery confirmation arrived - it's still billed, so please don't resend it blindly.
A message's status: pending (stored, still sending), sent (the carrier confirmed delivery), failed (the carrier rejected it - not billed, priceHal 0), and unknown (no delivery confirmation arrived, typically a timeout on the carrier's side) - it's still billed at full price, since the message may have gone through. If you get unknown, please don't resend it blindly - contact support and we'll refund any duplicate charge.
Example: a message with diacritics over 70 characters means more segments
This message is 112 characters and contains diacritics, so it's counted as UCS-2 (a 67-character limit per segment for multi-part messages) - it comes out to 2 segments, so 2.72 CZK (~EUR 0.11), not 1.36 CZK (~EUR 0.06):
curl -X POST https://volai.cz/v1/messages \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"to": "+420777123456", "body": "Příliš žluťoučký kůň úpěl ďábelské ódy - a tahle věta má přes sedmdesát znaků, takže spadne do druhého segmentu."}'{
"id": "msg_9a3f1c7d",
"status": "sent",
"priceHal": 272,
"segments": 2
}GET/v1/messages/{id}
The detail of a single message.
Request
curl https://volai.cz/v1/messages/msg_7c1f9a2e \
-H "Authorization: Bearer vk_YOUR_KEY"Response
{
"message": {
"id": "msg_7c1f9a2e",
"to": "+420777123456",
"from": "volai",
"body": "Ahoj z volai! - Moje Appka",
"status": "sent",
"priceHal": 136,
"segments": 1,
"createdAt": 1756111500000,
"source": "api"
}
}Error codes
- 404
not_foundThe message doesn't exist or doesn't belong to your account.
Calls
GET/v1/calls
The call history, newest first. The optional direction parameter (in or out) limits the list to inbound or outbound calls only.
Request
curl "https://volai.cz/v1/calls?limit=20" \
-H "Authorization: Bearer vk_YOUR_KEY"Response
{
"calls": [
{
"id": "c_8f2ac1d4",
"direction": "in",
"from": "+420777123456",
"to": "+420601234567",
"status": "completed",
"startedAt": 1756111400000,
"durationSecs": 47,
"priceHal": 236,
"agentId": "ag_kx91fa2b",
"source": "inbound",
"kind": "inbound",
"answeredBy": "human",
"endReason": "completed",
"hasRecording": true,
"data": {
"name": "Jane Smith",
"coffee_count": 2,
"urgency": "normal"
}
}
]
}POST/v1/calls
Starts an outbound call to to. Send exactly ONE of: agentId (your voice agent runs it according to its systemPrompt), from (a direct connection between two numbers, no agent - a bridge, see below), or systemPrompt (a trial call with no number of your own - no purchase or agent setup needed, see below). Never more than one, never none. With the agentId variant you can also send variables (custom values for the prompt) and ringingTimeoutSecs (how long to let it ring, see the callout below).
Request
curl -X POST https://volai.cz/v1/calls \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+420777123456",
"agentId": "ag_kx91fa2b",
"variables": { "jmeno_zakaznika": "Jana", "cislo_objednavky": "A-42" },
"ringingTimeoutSecs": 30
}'Response
{
"id": "c_9d4e2b7f",
"status": "initiated"
}Error codes
- 400
invalid_numberto is not a valid phone number. - 400
validationMissing, or more than one, of agentId/from/systemPrompt; with agentId, variables is out of limits (max 20 keys, 64-character keys, 512-character values) or ringingTimeoutSecs is outside the 5-60 range; with systemPrompt, it's missing/over 1200 characters, firstMessage is over 300 characters, or ringingTimeoutSecs is outside the 5-30 range. - 400
on_dncThat number is on your do-not-call list (/v1/dnc). - 400
destination_auto_blockedThe destination was auto-blocked for 30 days after repeated failures. - 400
cannot_call_own_numberLoop protection - you can't call your own volai number. - 400
cannot_call_volai_numberLoop protection - the destination is another account's volai number. - 404
bridge_needs_numberA bridge (from) needs at least one of your own volai numbers on the account. - 409
destination_busyYou already have another call running to the same number. - 404
agent_not_foundagentId doesn't exist or doesn't belong to your account. - 404
agent_no_numberThe agent has no phone number assigned. - 402
insufficient_creditYour credit doesn't cover the minimum to start a call. - 503
capacity_busyAll outbound lines are busy right now, try again in a minute. - 503
trial_calls_disabledWith systemPrompt: trial calls are temporarily disabled by an operational switch - call through your own agent (agentId) instead. - 403
trial_email_unverifiedWith systemPrompt: only for accounts with a verified email. - 500
trial_not_configuredWith systemPrompt: the shared volai demo agent isn't configured correctly - a temporary error on our side. - 502
call_rejectedWith systemPrompt: the voice platform rejected the connection outright, nothing was charged. - 429
rate_limitedWith systemPrompt: the daily cap of 3 trial calls per account is used up, or the shared cap across all accounts is temporarily saturated - calling with agentId doesn't have this limit.
Custom variables and ringing time
variables is a string-to-string object the agent receives as dynamic values - reference them in the prompt as {{jmeno_zakaznika}}. Limits: at most 20 keys, keys up to 64 characters, values up to 512 characters. A key may only contain letters, digits and underscores, and can't start with a digit. The names attempt_id, caller_number, called_number, trial_prompt, and trial_first_message are reserved - we fill those in ourselves, and any values you send under those names are discarded.
ringingTimeoutSecs is 5 to 60 seconds, default 25. Once it elapses, an unanswered call ends and its detail gets endReason: "no_answer". A longer ring means a better chance the customer picks up, but also a longer-held outbound slot.
Without an agent: bridging two numbers directly
Send from instead of agentId - your own number, or another number belonging to the customer. volai calls from first, and once someone picks up, it dials to. Both legs are billed separately at 0.92 CZK/min (~EUR 0.04).
curl -X POST https://volai.cz/v1/calls \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"to": "+420777123456", "from": "+420601234567"}'A trial call with no number of your own
Instead of agentId/from, send systemPrompt (instructions for the agent, for this one call only, up to 1200 characters) and optionally firstMessage (the opening line, up to 300 characters). The call goes out from volai's shared demo number, not your own - so the response also carries trial: true and from (the demo number). You can't send voiceId for this variant, it runs on the default voice. ringingTimeoutSecs has a lower cap of 5 to 30 (default 25) - it holds a slot from the shared pool.
curl -X POST https://volai.cz/v1/calls \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+420777123456",
"systemPrompt": "You are the front desk at cafe Nula. You take pickup orders and keep it brief and friendly.",
"firstMessage": "Hello, this is the volai trial agent, how can I help you?"
}'{
"id": "c_2f7a91mn",
"status": "initiated",
"trial": true,
"from": "+420266266641"
}Only for accounts with a verified email, at most 3 calls per account per 24 hours (plus a shared cap across all accounts, so a surge doesn't overwhelm the demo number). Billed exactly like a normal call, including the agent surcharge - no discount. Anyone who needs more buys their own number (POST /v1/numbers) and creates an agent (POST /v1/agents) - that cap doesn't apply there.
If trial calls are temporarily switched off (an operational toggle), systemPrompt returns a readable error (trial_calls_disabled) instead of placing the call; use your own agent (agentId) in the meantime.
GET/v1/calls/{id}
The detail of a call. Optionally, waitSecs (see the callout below) makes the server wait for the result instead of you polling. For calls with an agent, it also includes transcript (a turn-by-turn transcript) and summary (a short summary) - see Webhooks for the same shape delivered automatically once the call ends. Both the list and the detail also carry answeredBy (human, voicemail, or unknown when the transcript is missing), endReason (completed, no_answer, busy, rejected, capacity, blocked, loop_guard, credit_blocked, failed), and kind (agent, bridge, relay, inbound, transfer) - see the table below. The tools-and-recordings wave added two more fields: data (what the agent captured from the call per its dataFields; a null value means it wasn't mentioned) and hasRecording (the call has a recording available for download, see the endpoint below). Older calls don't have these fields and they're missing from the response.
Request
curl https://volai.cz/v1/calls/c_8f2ac1d4 \
-H "Authorization: Bearer vk_YOUR_KEY"Response
{
"call": {
"id": "c_8f2ac1d4",
"direction": "in",
"from": "+420777123456",
"to": "+420601234567",
"status": "completed",
"startedAt": 1756111400000,
"durationSecs": 47,
"priceHal": 236,
"agentId": "ag_kx91fa2b",
"source": "inbound",
"kind": "inbound",
"answeredBy": "human",
"endReason": "completed",
"hasRecording": true,
"data": {
"name": "Jane Smith",
"coffee_count": 2,
"urgency": "normal"
},
"transcript": [
{ "role": "agent", "message": "Hi, this is cafe Nula, how can I help you?" },
{ "role": "caller", "message": "I would like to order two lattes to go." },
{ "role": "agent", "message": "Sure, two lattes for pickup, they will be ready in fifteen minutes." }
],
"summary": "The caller ordered two lattes to go, pickup in 15 minutes."
}
}Error codes
- 404
call_not_foundThe call doesn't exist or doesn't belong to your account. - 400
validationwaitSecs is not an integer from 0 to 45.
Wait for the result instead of polling (waitSecs)
Right after POST /v1/calls, call GET /v1/calls/{id} with waitSecs (0 to 45, default 0) - the server responds once the call reaches a terminal state (completed/failed/missed/no_answer), returning transcript and summary right away, or once waitSecs elapses, whichever happens first.
curl "https://volai.cz/v1/calls/c_9d4e2b7f?waitSecs=30" \
-H "Authorization: Bearer vk_YOUR_KEY"{
"call": {
"id": "c_9d4e2b7f",
"direction": "out",
"from": "+420601234567",
"to": "+420777123456",
"status": "ringing",
"startedAt": 1756111400000,
"agentId": "ag_kx91fa2b",
"source": "api"
},
"stillRunning": true
}If the time runs out before the call ends, stillRunning is true and the fields carry the current (non-terminal) state - it never errors. Try GET (or the MCP get_call) again, with a higher waitSecs if you like.
GET/v1/calls/{id}/recording
The call's recording. Unlike the rest of the API, this response is not JSON - the body is an MP3 (content-type: audio/mpeg, 128 kbps, 16 kHz mono). We don't keep a copy of the recording ourselves: the stream flows from the voice platform straight through us, and we only verify the call belongs to your account.
?stahnout=1 switches content-disposition to attachment (the browser saves the file instead of playing it). This route doesn't offer ranges (Range) - it's meant for machine integrations that download the whole file. Only calls handled by an agent with recordCalls turned on are recorded, and we keep them for 90 days after the call.
Request
curl https://volai.cz/v1/calls/c_8f2ac1d4/recording \
-H "Authorization: Bearer vk_YOUR_KEY" \
-o call-c_8f2ac1d4.mp3Error codes
- 404
call_not_foundThe call doesn't exist or doesn't belong to your account. - 404
no_recordingThe call has no recording - it wasn't handled by an agent, or recording was turned off. - 410
recording_expiredThere was a recording, but 90 days have passed and it's been deleted. The transcript remains.
Call status: the status field
status is the only field that changes over time on a call. It can still move during the first five minutes after the call starts; after that it's final - four of these states are terminal.
| status | Meaning |
|---|---|
initiated | The call has been placed but hasn't started ringing yet. |
ringing | It's ringing at the called party. |
in_progress | The call is in progress. |
completed | Terminal. A connected call that ended normally - for the vast majority expect durationSecs > 0 (exception: a call with endReason: credit_blocked below has both a duration and a price of 0). |
no_answer | Terminal. An outbound call rang and the called party didn't pick up. |
missed | Terminal. An inbound call went unanswered. |
failed | Terminal. The call never connected at all - the platform rejected it or the network failed. endReason gives the specific reason. |
Call type: the kind field
kind says how a call came to exist - and therefore how it's billed. It's the only field that lets you tell a bring-your-own (BYO) agent's call apart from a built-in agent's call on an invoice, since relay legs don't carry the agent surcharge.
| kind | What it is | Billing |
|---|---|---|
agent | An outbound call from the built-in voice agent. | 0.92 CZK/min + 2.50 CZK/min |
bridge | A direct connection between two numbers, no agent. | 0.92 CZK/min (~EUR 0.04) for each of the two legs |
relay | An outbound call from your own agent through POST /v1/relay. | 0.92 CZK/min (~EUR 0.04), no agent surcharge |
inbound | An inbound call to your number. | 0.50 CZK/min (+ agent, if one handled it) |
transfer | The second leg created by transferring an inbound call to a human. The agent leaves the call after the transfer, so this leg is billed as a regular outbound call. | 0.92 CZK/min, no agent surcharge |
Why a call ended: the endReason field
We add endReason to every closed call. Watch out: a call blocked for unpaid debt has status completed, but endReason is credit_blocked and the price is 0 - status alone won't tell it apart from a call that actually went through. The three "unanswered" variants are deliberately distinct, not one shared state.
| endReason | Meaning |
|---|---|
completed | The call went through and ended normally. |
no_answer | It rang and the callee didn't pick up. |
busy | A busy signal - this is NOT the same as not picking up. |
rejected | The platform rejected the connection outright. |
capacity | The outbound lines (relay pool) were full. |
blocked | The destination is on the do-not-call list or in the automatic 30-day block (see Do-not-call list below). |
loop_guard | Protection against calling your own or another account's volai number. |
credit_blocked | An inbound call to an agent whose owner is over the debt limit - the agent hangs up right after its opening line, price 0. |
failed | The call never connected at all (platform or network). |
Agents
GET/v1/agents
The voice agents on the account.
Request
curl https://volai.cz/v1/agents \
-H "Authorization: Bearer vk_YOUR_KEY"Response
{
"agents": [
{
"id": "ag_kx91fa2b",
"name": "Front desk",
"systemPrompt": "You are the front desk at cafe Nula. You take pickup orders...",
"firstMessage": "Hi, this is cafe Nula, how can I help you?",
"language": "en",
"voiceId": "MpbYQvoTmXjHkaxtLiSh",
"numberE164": "+420601234567",
"createdAt": 1756111000000,
"status": "active",
"toolIds": ["tl_5c2a91f4"],
"transferTo": "+420777123456",
"transferCondition": "The caller explicitly asks to be connected with a human.",
"dataFields": [
{
"key": "name",
"type": "string",
"description": "The caller's name, as they stated it."
}
],
"recordCalls": true
}
]
}POST/v1/agents
Creates a new voice agent. Only name and systemPrompt are required - how to write a good prompt is covered on the Voice agent page. If you send numberE164, the agent is attached to that number right away (replacing whatever routing it had). voiceId picks the voice - the valid values (currently just one) come from GET /v1/voices, see the Voices section below; without voiceId the agent inherits the template's voice. It also optionally takes four fields from the tools-and-recordings wave: toolIds, transferTo with transferCondition, dataFields, and recordCalls - each is described in the table below the endpoints.
Request
curl -X POST https://volai.cz/v1/agents \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Front desk",
"systemPrompt": "You are the front desk at cafe Nula. You take pickup orders and answer questions about opening hours. Never make up prices you do not know. End the call by summarising the order.",
"firstMessage": "Hi, this is cafe Nula, how can I help you?",
"language": "en",
"numberE164": "+420601234567",
"toolIds": ["tl_5c2a91f4"],
"transferTo": "+420777123456",
"transferCondition": "The caller wants to speak with staff, or has a complaint.",
"dataFields": [
{"key": "name", "type": "string", "description": "The caller's name, as they stated it."},
{"key": "coffee_count", "type": "number", "description": "How many coffees the caller ordered."}
],
"recordCalls": true
}'Response
{
"id": "ag_kx91fa2b"
}Error codes
- 400
invalid_nameThe agent's name must be 1 to 60 characters. - 400
invalid_system_promptThe agent's instructions must be 10 to 6000 characters. - 400
invalid_languagelanguage must be cs, sk, en, de or pl. - 400
agent_limitThe account is already at its maximum number of agents (10). - 400
tool_limittoolIds has more than 10 tools. - 400
invalid_data_fieldsdataFields has an invalid key, type, description, or more than 10 entries. - 400
transfer_looptransferTo points at a volai number on the same account - the call would loop back on itself. - 400
blocked_destinationtransferTo is a premium-rate line with special pricing. - 400
invalid_numbertransferTo is not a valid phone number. - 404
tool_not_foundA tool from toolIds doesn't exist or doesn't belong to your account. - 404
not_foundnumberE164 doesn't exist or doesn't belong to your account. - 400
validationvoiceId doesn't match any entry from GET /v1/voices and doesn't look like a raw ElevenLabs ID - the message lists the valid values.
GET/v1/agents/{id}
The detail of a single agent - the same fields as in the list, just for one. A deleted agent behaves as if it never existed: 404 agent_not_found.
Request
curl https://volai.cz/v1/agents/ag_kx91fa2b \
-H "Authorization: Bearer vk_YOUR_KEY"Response
{
"agent": {
"id": "ag_kx91fa2b",
"name": "Front desk",
"systemPrompt": "You are the front desk at cafe Nula. You take pickup orders...",
"firstMessage": "Hi, this is cafe Nula, how can I help you?",
"language": "en",
"voiceId": "MpbYQvoTmXjHkaxtLiSh",
"numberE164": "+420601234567",
"createdAt": 1756111000000,
"status": "active",
"toolIds": ["tl_5c2a91f4"],
"transferTo": "+420777123456",
"transferCondition": "The caller explicitly asks to be connected with a human.",
"dataFields": [
{
"key": "name",
"type": "string",
"description": "The caller's name, as they stated it."
},
{
"key": "urgency",
"type": "string",
"description": "How urgent the request was.",
"enumValues": ["low","normal","high"]
}
],
"recordCalls": true
}
}Error codes
- 404
agent_not_foundThe agent doesn't exist, was deleted, or doesn't belong to your account.
PATCH/v1/agents/{id}
Updates an existing agent - the same fields as on creation, all of them optional. Send only what needs to change. Two of them, though, are always sent IN FULL, because they replace the existing list: toolIds and dataFields. An empty array therefore turns the feature off ("toolIds": [] removes all of the agent's tools), whereas a missing key means "leave it as is". Handoff is turned off with an empty string: "transferTo": "".
Request
curl -X PATCH https://volai.cz/v1/agents/ag_kx91fa2b \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"firstMessage": "Hi, cafe Nula, what can I get you?"}'Response
{
"agent": {
"id": "ag_kx91fa2b",
"name": "Front desk",
"systemPrompt": "You are the front desk at cafe Nula. You take pickup orders...",
"firstMessage": "Hi, cafe Nula, what can I get you?",
"language": "en",
"voiceId": "MpbYQvoTmXjHkaxtLiSh",
"numberE164": "+420601234567",
"createdAt": 1756111000000,
"status": "active",
"toolIds": ["tl_5c2a91f4"],
"transferTo": "+420777123456",
"transferCondition": "The caller explicitly asks to be connected with a human.",
"recordCalls": true
}
}Error codes
- 400
invalid_nameThe agent's name must be 1 to 60 characters. - 400
invalid_system_promptThe agent's instructions must be 10 to 6000 characters. - 400
invalid_languagelanguage must be cs, sk, en, de or pl. - 400
tool_limittoolIds has more than 10 tools. - 400
invalid_data_fieldsdataFields has an invalid key, type, description, or more than 10 entries. - 400
transfer_looptransferTo points at a volai number on the same account. - 400
blocked_destinationtransferTo is a premium-rate line with special pricing. - 404
tool_not_foundA tool from toolIds doesn't exist or doesn't belong to your account. - 404
agent_not_foundThe agent doesn't exist or doesn't belong to your account. - 400
validationvoiceId doesn't match any entry from GET /v1/voices and doesn't look like a raw ElevenLabs ID - the message lists the valid values.
DELETE/v1/agents/{id}
Deletes the agent from both volai and ElevenLabs. A number that was attached to it stays yours - just give it new routing through PATCH /v1/numbers/{e164}.
Request
curl -X DELETE https://volai.cz/v1/agents/ag_kx91fa2b \
-H "Authorization: Bearer vk_YOUR_KEY"Response
{
"deleted": true
}Error codes
- 404
agent_not_foundThe agent doesn't exist or doesn't belong to your account.
New agent fields
Besides the name, prompt, voice, and number, POST /v1/agents and PATCH /v1/agents/{id} also take these fields. All are optional, and an agent created earlier simply doesn't have them - the key is then missing from the response too.
| Field | What it does |
|---|---|
toolIds | An array of ids of webhook tools from GET /v1/tools that the agent may call during a call. At most 10. A tool belongs to the account, so more than one agent can have it turned on. |
transferTo | A number in E.164 that the agent transfers the caller to when they ask for a human. An empty string turns handoff off. The account's own volai number can't be used here (transfer_loop). |
transferCondition | When to transfer, written as an instruction to the model (up to 500 characters). If you don't send it, we substitute a default sentence. |
dataFields | What the agent should capture from the call: an array of {key, type, description, enumValues} objects, at most 10. key is lowercase letters, digits and underscores; type is string, number, or boolean; enumValues can only be set for a text field. Filled-in values then arrive in the data field on the call. |
recordCalls | Whether to record this agent's calls. On by default; when false, no recording is made and GET /v1/calls/{id}/recording returns no_recording. Informing the caller about recording is your responsibility. |
Handoff: what the API actually does
The transfer runs over SIP REFER. The agent tells the caller it's transferring them, then leaves the call - the person on the other end hears the caller directly. We can't pass along a call summary, there's no such thing as a "warm transfer" here.
The second leg is a regular outbound call, and it's billed as one: 0.92 CZK/min (~EUR 0.04) with no agent surcharge. You can spot it in the call list by kind: "transfer".
Voices
The voice catalog for the voiceId field on POST /v1/agents and PATCH /v1/agents/{id}. It currently holds a single entry - the default voice every new agent inherits from the template; more will be added over time. You can send voiceId either the id from the GET /v1/voices response below, or a raw ElevenLabs ID (for backward compatibility with agents created before the catalog existed). An unrecognized value returns 400 validation and lists the valid values. The same catalog, with a description of each voice, is on the Voice agent.
GET/v1/voices
The catalog of voices available for an agent's voiceId.
Request
curl https://volai.cz/v1/voices \
-H "Authorization: Bearer vk_YOUR_KEY"Response
{
"voices": [
{
"id": "anet",
"name": "Anet",
"gender": "female",
"description": "Young female voice, professional delivery - the voice the template currently uses, which every new agent inherits without its own voiceId setting.",
"isDefault": true
}
]
}The gender field is missing from an entry when the voice's gender isn't known (as with the default entry above) - it will appear once it's certain; we don't guess the value.
Agent tools
A tool is the address of your API that the agent calls in the middle of a call (verify an order, log a booking) and uses the response in speech. A tool belongs to the account and is assigned to an agent via toolIds - creating a tool by itself doesn't change any agent.
Limits: at most 10 tools per account, 10 parameters and 5 headers per tool, timeoutSecs 5 to 30 (20 by default). The address must be https and public - we reject internal networks and loopback (private_address), since otherwise our server could be used to probe someone else's infrastructure.
GET/v1/tools
The account's webhook tools. Header values are always masked.
Request
curl https://volai.cz/v1/tools \
-H "Authorization: Bearer vk_YOUR_KEY"Response
{
"tools": [
{
"id": "tl_5c2a91f4",
"name": "verify_order",
"label": "Verify order",
"description": "Looks up order status by number. Use when the caller asks where their order is.",
"url": "https://api.yourapp.com/orders",
"method": "POST",
"headers": { "Authorization": "Bear***" },
"params": [
{
"name": "order_number",
"type": "string",
"source": "llm",
"description": "The order number the caller dictated.",
"required": true
},
{ "name": "phone", "type": "string", "source": "caller_number" }
],
"timeoutSecs": 20,
"createdAt": 1756111000000,
"updatedAt": 1756111000000
}
]
}POST/v1/tools
Creates a tool. label is the readable name; the name the model sees is derived from it automatically (Verify order -> verify_order) and returned in the name field. description is the only thing the model bases its decision of WHEN to use the tool on - write it as an instruction ("Use when the caller asks where their order is."), 10 to 1000 characters.
Request
curl -X POST https://volai.cz/v1/tools \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"label": "Verify order",
"description": "Looks up order status by number. Use when the caller asks where their order is.",
"url": "https://api.yourapp.com/orders",
"method": "POST",
"headers": { "Authorization": "Bearer your_key" },
"params": [
{
"name": "order_number",
"type": "string",
"source": "llm",
"description": "The order number the caller dictated.",
"required": true
},
{ "name": "phone", "type": "string", "source": "caller_number" },
{ "name": "source", "type": "string", "source": "constant", "constantValue": "phone" }
],
"timeoutSecs": 20
}'Response
{
"tool": {
"id": "tl_5c2a91f4",
"name": "verify_order",
"label": "Verify order",
"url": "https://api.yourapp.com/orders",
"method": "POST",
"headers": { "Authorization": "Bear***" },
"timeoutSecs": 20,
"createdAt": 1756111000000,
"updatedAt": 1756111000000
}
}Error codes
- 400
tool_limitThe account already has 10 tools. - 400
invalid_tool_namelabel must be 1 to 60 characters. - 400
invalid_tool_urlurl is not a valid address, or isn't https. - 400
private_addressurl points into an internal network or loopback. - 400
invalid_tool_paramsA parameter has an invalid name, type, source, or there are more than 10 of them. - 400
invalid_tool_headersA header is forbidden (host, content-length, x-conversation-id, x-caller-id), duplicated, empty, or there are more than 5 of them. - 400
validationdescription is outside 10 to 1000 characters, timeoutSecs is outside 5 to 30, or method is something other than GET/POST. - 502
eleven_labs_errorThe tool couldn't be created with the voice platform - we didn't save anything locally, please try again.
GET/v1/tools/{id}
The detail of a single tool - the same fields as in the list.
Request
curl https://volai.cz/v1/tools/tl_5c2a91f4 \
-H "Authorization: Bearer vk_YOUR_KEY"Response
{
"tool": {
"id": "tl_5c2a91f4",
"name": "verify_order",
"label": "Verify order",
"url": "https://api.yourapp.com/orders",
"method": "POST",
"headers": { "Authorization": "Bear***" },
"timeoutSecs": 20,
"createdAt": 1756111000000,
"updatedAt": 1756111000000
}
}Error codes
- 404
tool_not_foundThe tool doesn't exist or doesn't belong to your account.
PATCH/v1/tools/{id}
Updates a tool - all fields optional, only the ones sent are changed. Two things to watch for: params and headers both replace the entire existing list, and changing label also rewrites the name the model knows the tool by - if you reference it in systemPrompt, update that at the same time.
Headers only ever come back masked ("Bear***"). If you send that exact masked value back, we treat it as "keep the original" - so the usual "load the tool, change one field, send the whole object back" flow won't destroy your credentials. Send the real value in full to actually change it.
Request
curl -X PATCH https://volai.cz/v1/tools/tl_5c2a91f4 \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"timeoutSecs": 10}'Response
{
"tool": {
"id": "tl_5c2a91f4",
"name": "verify_order",
"label": "Verify order",
"url": "https://api.yourapp.com/orders",
"method": "POST",
"headers": { "Authorization": "Bear***" },
"timeoutSecs": 20,
"createdAt": 1756111000000,
"updatedAt": 1756111000000
}
}Error codes
- 400
invalid_tool_urlurl is not a valid address, or isn't https. - 400
private_addressurl points into an internal network or loopback. - 400
invalid_tool_paramsA parameter has an invalid name, type, or source. - 400
invalid_tool_headersA header is forbidden, duplicated, or empty. - 404
tool_not_foundThe tool doesn't exist or doesn't belong to your account. - 502
eleven_labs_mismatchThe change wasn't saved exactly as specified with the voice platform - we didn't overwrite anything locally.
DELETE/v1/tools/{id}
Deletes a tool and returns the agents it stops working for (just id and name; fetch the full agent via GET /v1/agents/{id}). Unlike releasing a number, this is a recoverable loss - the same tool can be created again, just with a new id that then needs to be assigned to the agents again.
Request
curl -X DELETE https://volai.cz/v1/tools/tl_5c2a91f4 \
-H "Authorization: Bearer vk_YOUR_KEY"Response
{
"deleted": true,
"agents": [
{ "id": "ag_kx91fa2b", "name": "Front desk" }
]
}Error codes
- 404
tool_not_foundThe tool doesn't exist or doesn't belong to your account. - 502
eleven_labs_errorThe tool couldn't be deleted from the voice platform - please try again.
A tool's parameters: the source field
Every parameter has a name, a type (string, number, boolean), and a source - where its value comes from. For GET, parameters go into the query string; for POST, into the JSON body.
| source | Where the value comes from | What else to fill in |
|---|---|---|
llm | The model pulls it from the call. | description (exactly what to put there) and optionally required. |
caller_number | The caller's number in E.164, filled in automatically. | Nothing. The model never sees this field. |
called_number | Your called number in E.164, filled in automatically. | Nothing. The model never sees this field. |
constant | A fixed value that you provide. | constantValue. |
Test call
"Have your agent call you" - a real outbound call through makeAgentCall, just with its own daily cap of 3 calls per account (across every agent), so a test call can't be used as a back door around the normal calling limit.
POST/v1/agents/{id}/test-call
Calls you back from your own agent - the same price and the same rules as POST /v1/calls, no discount and no special rate.
Request
curl -X POST https://volai.cz/v1/agents/ag_kx91fa2b/test-call \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"to": "+420777123456"}'Response
{
"id": "c_9d4e2b7f",
"status": "initiated"
}Error codes
- 404
agent_not_foundagentId doesn't exist or doesn't belong to your account. - 404
agent_no_numberThe agent has no phone number assigned. - 402
insufficient_creditYour credit doesn't cover the minimum to start a call. - 503
capacity_busyAll outbound lines are busy right now, try again in a minute. - 429
rate_limitedThe daily cap of 3 test calls per account is used up - a regular call through POST /v1/calls doesn't have this limit.
Relay - bring your own agent
A one-off rental of a SIP name from the relay pool, for an outbound call from your own voice agent (ElevenLabs or any other platform) - with no agent surcharge (that's normally 2.50 CZK/min (~EUR 0.10), but here you pay for it on your own platform). The full setup guide for ElevenLabs is on the Bring your own agent.
POST/v1/relay
Creates a lease - to is the destination number, from is your own volai number the call should appear to come from. The returned sipName is the BARE name for ElevenLabs's to_number (not sipUri - ElevenLabs rejects a full SIP URI).
ttlSecs (optional, default 120) - how many seconds the lease waits for the first call before an unused one expires. The allowed range is 15 to 300 seconds - outside it, this returns 400 validation.
Request
curl -X POST https://volai.cz/v1/relay \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"to": "+420777123456", "from": "+420601234567"}'Response
{
"id": "rl_4f2a91cd",
"sipName": "volai_relay_2",
"sipUri": "sip:volai_relay_2@sip.volai.cz",
"expiresAt": 1756111760000,
"callId": "c_9d4e2b7f"
}Error codes
- 400
invalid_numberto or from is not a valid phone number. - 400
validationttlSecs is outside the allowed range of 15 to 300 seconds. - 404
from_number_not_ownedfrom doesn't belong to your account. - 400
on_dncto is on your do-not-call list. - 400
relay_lease_limitYou already have an active lease on this number, or two across the whole account. - 402
insufficient_creditYour credit doesn't cover the minimum to start a call. - 409
destination_busyAnother call is already running to that number. - 503
capacity_busyThe relay pool is full right now, try again shortly - we don't charge you for this.
GET/v1/relay
The list of your relay leases - pending is waiting for its first call, active means a call is running right now.
Request
curl https://volai.cz/v1/relay \
-H "Authorization: Bearer vk_YOUR_KEY"Response
{
"leases": [
{
"id": "rl_4f2a91cd",
"sipName": "volai_relay_2",
"sipUri": "sip:volai_relay_2@sip.volai.cz",
"from": "+420601234567",
"to": "+420777123456",
"callId": "c_9d4e2b7f",
"status": "active",
"createdAt": 1756111640000,
"expiresAt": 1756111760000
}
]
}DELETE/v1/relay/{id}
Releases the lease and returns the slot to the pool, as long as it's still waiting for its first call (pending). It does not end a call in progress - no API can do that today (not the phone network, not ElevenLabs) - a lease like that (active) just runs its course; this only stops that same lease from being used again.
Request
curl -X DELETE https://volai.cz/v1/relay/rl_4f2a91cd \
-H "Authorization: Bearer vk_YOUR_KEY"Response
{
"cancelled": true
}Error codes
- 404
lease_not_foundThe lease doesn't exist, has expired, or doesn't belong to your account.
Do-not-call list (DNC)
Numbers your agent (your own or the built-in one) must not call - whether through POST /v1/calls, a test call, or relay. DNC only applies to calls - sending an SMS to a listed number is not restricted.
Besides the manual list, volai automatically (and temporarily) blocks a destination on its own - after 3 failed attempts within 24 hours (busy, unanswered, rejected) it stops calling that number for 30 days. The block can't be lifted manually; it expires on its own.
GET/v1/dnc
The numbers on the do-not-call list.
Request
curl https://volai.cz/v1/dnc \
-H "Authorization: Bearer vk_YOUR_KEY"Response
{
"numbers": ["+420777998877"]
}POST/v1/dnc
Adds a number to the do-not-call list.
Request
curl -X POST https://volai.cz/v1/dnc \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"e164": "+420777998877"}'Response
{
"added": true,
"e164": "+420777998877"
}Error codes
- 400
validatione164 is not a valid phone number.
DELETE/v1/dnc/{e164}
Removes a number from the do-not-call list.
Request
curl -X DELETE "https://volai.cz/v1/dnc/+420777998877" \
-H "Authorization: Bearer vk_YOUR_KEY"Response
{
"removed": true,
"e164": "+420777998877"
}Error codes
- 400
validatione164 in the URL is not a valid phone number.
Webhook
GET/v1/webhook
The current outbound webhook settings - without the signing secret, which only comes back from PUT.
Request
curl https://volai.cz/v1/webhook \
-H "Authorization: Bearer vk_YOUR_KEY"Response
{
"url": "https://tvoje-appka.cz/webhooks/volai",
"events": ["call.completed", "call.failed", "message.sent"]
}PUT/v1/webhook
Sets (or replaces) the target URL and the events you subscribe to. The response also includes the signing secret - save it right away, GET won't return it again later. An empty events: [] means subscribe to every event, not none - including ones added later. If you only want some events, list them. Conversely, an empty url: "" cancels the webhook - volai then sends nothing, and the response returns url: null. How to verify the signature is covered on the Webhooks.
Request
curl -X PUT https://volai.cz/v1/webhook \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://tvoje-appka.cz/webhooks/volai", "events": ["call.completed", "call.failed", "message.sent"]}'Response
{
"url": "https://tvoje-appka.cz/webhooks/volai",
"secret": "whsec_9f2b7a1c4e6d8f0a",
"events": ["call.completed", "call.failed", "message.sent"]
}Error codes
- 400
invalid_urlThe URL must start with https:// (http:// is only allowed for localhost). - 400
invalid_eventsOne of the events isn't recognized (call.completed, call.failed, call.missed, call.no_answer, message.sent).
Related
MCP server
Connect Claude Code, Cursor and other AI editors.
Voice agent
System prompt, tools, handoff to a human, recordings and structured call data.
Webhooks
Events, structured data in the body, signature verification, retries on failure.
SIP
Your own softphone, SIP credentials, outbound calls through a SIP client.
Bring your own agent
Connect a third-party platform (ElevenLabs, Asterisk...) - no agent surcharge.