Endpoint Reference
All endpoints below live under https://voice.vokaai.com/api/v1.
Headers required on every authenticated request:
Authorization: Bearer voka_live_...
Content-Type: application/json (only on POST)
Headers returned on every response:
| Header | Purpose |
|---|---|
X-Request-Id | UUID — share this with support when filing issues |
X-RateLimit-Limit | Per-customer ceiling (currently 60/min) |
X-RateLimit-Remaining | Remaining requests in this window |
Retry-After | Seconds to wait — sent on 429 only |
GET /auth/whoami
Connection-test endpoint. No scope required — any valid key works.
curl https://voice.vokaai.com/api/v1/auth/whoami \
-H "Authorization: Bearer voka_live_..."
{
"customer_id": "0bf91...",
"customer_name": "Acme Dental",
"mode": "live",
"permissions": { "read_calls": true, "manage_webhooks": true }
}
GET /assistants
List voice assistants. Scope: read_assistants.
Query params: id (specific assistant), limit (default 50, max 100), offset.
curl https://voice.vokaai.com/api/v1/assistants \
-H "Authorization: Bearer voka_live_..."
{
"data": [
{
"id": "550e8400-...",
"name": "Reception Agent",
"description": "Handles new-patient calls",
"language": "en-US",
"voice_id": "natural-female",
"is_active": true,
"phone_numbers": ["+12025551111"],
"created_at": "2026-01-15T00:00:00Z",
"updated_at": "2026-04-22T00:00:00Z"
}
],
"pagination": { "limit": 50, "offset": 0, "total": 1 }
}
GET /calls
Unified list of inbound and outbound calls. Scope: read_calls.
| Param | Type | Description |
|---|---|---|
direction | inbound | outbound | Filter by direction |
assistant_id | uuid | Filter by assistant |
from | ISO-8601 | Inclusive lower bound on started_at |
to | ISO-8601 | Inclusive upper bound on started_at |
limit | int 1-100 (default 25) | Page size |
cursor | string | Keyset pagination — pass next_cursor from prior response |
page | int ≥ 0 | Zapier-compat offset alias |
curl "https://voice.vokaai.com/api/v1/calls?direction=inbound&limit=10" \
-H "Authorization: Bearer voka_live_..."
{
"data": [
{
"id": "550e8400-...",
"direction": "inbound",
"status": "completed",
"started_at": "2026-05-10T12:00:00Z",
"answered_at": "2026-05-10T12:00:05Z",
"ended_at": "2026-05-10T12:03:30Z",
"duration_seconds": 205,
"from_number": "+12025551111",
"to_number": "+12025552222",
"assistant_id": "550e8400-...",
"assistant_name": "Reception Agent",
"caller_name": "John Doe",
"external_id": "appt_12345",
"has_transcript": true,
"recording_available": true,
"created_at": "2026-05-10T12:00:00Z"
}
],
"next_cursor": "MjAyNi0wNS0xMFQxMjowMDowMC4wMDBafDU1MGU4NDAw..."
}
next_cursor is null when you've reached the end. Pass it as ?cursor=... to fetch the next page. Cursor pagination is recommended for stable iteration; ?page=N is a fallback for tools that can't handle opaque cursors.
external_id is your own reference for the call, passed to POST /outbound-calls. It's present only on API-initiated outbound calls (null for inbound calls and any outbound call placed without one). It's the recommended way to tie a call — and its duration_seconds — back to your customer for usage or chargeback reporting. Filter or group on it in your own system after listing.
GET /calls/{id}
Single-call detail. Scope: read_calls.
Same fields as the list shape plus: summary, disconnect_reason, conversation_id, cost, billable_seconds.
Returns 404 (not 403) when the call belongs to another customer — we never leak existence.
GET /calls/{id}/transcript
On-demand transcript pull. Scope: read_calls.
- Cached transcript: returned immediately (sub-100ms)
- Not yet cached, conversation_id present: fetched from carrier, persisted, returned
- Conversation still settling:
202withstatus: "pending"— subscribe tocall.transcript.readyfor push delivery
{
"call_id": "550e8400-...",
"conversation_id": "conv_abc123",
"transcript": {
"full_text": "Caller: hi...\n\nAssistant: ...",
"segments": [
{ "speaker": "caller", "text": "hi", "timestamp": "2026-05-10T12:00:00Z" }
]
}
}
POST /outbound-calls
Have one of your assistants place a phone call. Scope: outbound_calls.
curl https://voice.vokaai.com/api/v1/outbound-calls \
-H "Authorization: Bearer voka_live_..." \
-H "Content-Type: application/json" \
-d '{
"assistant_id": "550e8400-...",
"to": "+12025551111",
"variables": { "name": "John Doe", "appointment_datetime": "Tuesday at 3pm" },
"external_id": "appt_12345",
"callback_url": "https://your-app.example.com/voka/callback",
"metadata": { "your_internal_id": "abc-123" }
}'
| Field | Required | Description |
|---|---|---|
assistant_id | yes | One of your active assistants |
to | yes | Number to call, E.164 format (e.g. +12025551111) |
from | no | E.164 number to call from. Must be one of your active Voka numbers. Defaults to the first number assigned to the assistant |
scheduled_at | no | ISO-8601 time in the future. Omit to call right away |
variables | no | Key/value pairs your assistant's prompt can use (e.g. {{name}}) |
external_id | no | Your own reference for the call. Also works as an idempotency key: a second request with an external_id that is already in use (and not failed or canceled) returns 409 |
metadata | no | Your own tracking data, stored with the call |
callback_url | no | URL that receives the completion event for this call (see below) |
notification_email | no | Address that gets an email summary when the call ends |
Response 201 Created:
{
"id": "7c9e6679-...",
"external_id": "appt_12345",
"assistant_id": "550e8400-...",
"assistant_name": "Reception Agent",
"from": "+12025559999",
"to": "+12025551111",
"scheduled_at": "2026-05-10T12:00:05Z",
"status": "scheduled",
"variables": { "name": "John Doe", "appointment_datetime": "Tuesday at 3pm" },
"telnyx_event_id": "...",
"created_at": "2026-05-10T12:00:00Z"
}
Status lifecycle: scheduled → calling → completed or failed. A call that never connects (zero duration, never answered) ends as failed. There is no voicemail detection and no automatic retry, so a call that reaches voicemail is handled like any other answered call.
Rate limit and cost protection:
- 60 requests per minute per customer.
- Every API key has a daily voice spend cap (default $50, resets at UTC midnight) and a destination country allowlist (default US and Canada). Calls outside the allowlist return
400withcode: "country_not_allowed"; calls past the cap return429withcode: "daily_limit_exceeded". Both are checked before anything reaches the carrier. To change either one, email support@vokaai.com.
Account requirements:
- Your account needs a live free trial or an active subscription. Otherwise the request returns
402withcode: "CALLS_REQUIRE_SUBSCRIPTION". - On a free trial, calls can be placed right away or scheduled up to 15 minutes ahead (and not past the trial's end). Further out returns
402withcode: "SCHEDULE_BEYOND_TRIAL". Trial calls use trial minutes. - Outbound calls use your plan's minutes the same way inbound calls do.
Other errors: 400 for a missing or badly formatted field or a scheduled_at in the past, 403 if the key lacks the outbound_calls scope or from is not one of your numbers, 404 if the assistant isn't yours or isn't active, 502 if the carrier rejects the call (the call is kept with status failed so you can see it in GET /outbound-calls). Errors from this endpoint use the shape { "error": "...", "code": "..." }.
You are responsible for having the consent required to call each number (for example under the TCPA in the US) and for calling at appropriate times.
Completion callback
If you set callback_url, Voka sends one POST to it when the call ends:
{
"event": "outbound_call.completed",
"delivered_at": "2026-05-10T12:04:01Z",
"call": {
"id": "7c9e6679-...",
"external_id": "appt_12345",
"status": "completed",
"started_at": "2026-05-10T12:00:05Z",
"answered_at": "2026-05-10T12:00:11Z",
"ended_at": "2026-05-10T12:03:59Z",
"duration_seconds": 228,
"cost": 0.46,
"recording_url": "https://...",
"conversation_id": "...",
"metadata": { "your_internal_id": "abc-123" },
"to_number": "+12025551111",
"from_number": "+12025559999",
"assistant_id": "550e8400-..."
}
}
- The event is named
outbound_call.completedfor every finished call; checkcall.status(completedorfailed). - It is a single attempt with an 8-second timeout. There are no retries. If you miss it, use
GET /outbound-callsto check the result. - It is not signed. Unlike webhook subscriptions, there is no HMAC header. Use a hard-to-guess URL (for example one with a secret token in the path) and treat the payload as a hint to re-fetch.
GET /outbound-calls
List the outbound calls placed through the API. Scope: read_analytics.
| Param | Description |
|---|---|
status | scheduled, calling, completed, failed or canceled |
assistant_id | Filter by assistant |
external_id | Filter by your reference |
limit | Default 50, max 100 |
offset | Pagination offset |
curl "https://voice.vokaai.com/api/v1/outbound-calls?external_id=appt_12345" \
-H "Authorization: Bearer voka_live_..."
{
"data": [
{
"id": "7c9e6679-...",
"external_id": "appt_12345",
"assistant_id": "550e8400-...",
"from_number": "+12025559999",
"to_number": "+12025551111",
"status": "completed",
"scheduled_at": "2026-05-10T12:00:05Z",
"duration_seconds": 228,
"created_at": "2026-05-10T12:00:00Z"
}
],
"pagination": { "limit": 50, "offset": 0, "total": 1 }
}
List rows use the stored column names (from_number, to_number) and include more fields than shown here. This endpoint uses limit/offset only (no cursor). There is no single-call GET /outbound-calls/{id}; filter the list by external_id, or find the call in GET /calls (direction=outbound), which also returns external_id.
GET /outbound-calls/{id}/transcript
Turn-by-turn transcript for an API-placed outbound call. Scope: read_analytics.
{
"outbound_call_id": "7c9e6679-...",
"conversation_id": "...",
"status": "completed",
"transcript": {
"full_text": "Assistant: Hi, this is...\n\nCaller: ...",
"segments": [
{ "speaker": "assistant", "text": "Hi, this is...", "timestamp": "2026-05-10T12:00:11Z" }
]
}
}
Until the call has ended and its conversation is saved, this returns 404 with "error": "Transcript not available yet", the current status, and a hint. Retry once the status is completed.
POST /webhook-subscriptions
Subscribe to one or more events. Scope: manage_webhooks.
curl https://voice.vokaai.com/api/v1/webhook-subscriptions \
-H "Authorization: Bearer voka_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example.com/webhooks/voka",
"events": ["call.completed", "call.transcript.ready"],
"description": "Production receiver"
}'
{
"id": "sub_01H...",
"url": "https://your-app.example.com/webhooks/voka",
"events": ["call.completed", "call.transcript.ready"],
"secret": "whsec_...",
"is_active": true,
"created_at": "2026-05-10T12:00:00Z"
}
secret is shown onceStore it now — used to verify HMAC signatures on incoming events. We can rotate it but cannot show it again.
Idempotent: posting the same (url, events) set returns the existing subscription with 200 (no new secret) instead of creating a duplicate.
Make.com verification probe: if you POST with ?challenge=<token> query param, we echo { challenge: token } with 202 Accepted — no auth, no DB write. This is the contract Make's connection-validation step expects.
GET /webhook-subscriptions
List your active subscriptions. Scope: manage_webhooks.
DELETE /webhook-subscriptions/{id}
Unsubscribe. Scope: manage_webhooks. Also fired automatically when a subscription has 10 consecutive delivery failures (auto-disable, not delete — re-enable via the dashboard).
Pagination conventions
GET /calls supports both pagination styles so wrapper authors can pick (GET /assistants and GET /outbound-calls use limit/offset):
Cursor (recommended):
curl "https://voice.vokaai.com/api/v1/calls?limit=25"
# Response includes: "next_cursor": "..."
curl "https://voice.vokaai.com/api/v1/calls?limit=25&cursor=..."
Offset (Zapier-friendly):
curl "https://voice.vokaai.com/api/v1/calls?limit=25&page=0"
curl "https://voice.vokaai.com/api/v1/calls?limit=25&page=1"
When both cursor and page are passed, cursor wins. Cursor is O(1) per page; page is O(N) for deep pages so prefer cursor for sync jobs.