Skip to main content

Error Responses

The Voka API uses RFC 9457 problem details (the successor to RFC 7807) for every error response. Every error has the same shape and a stable code field for machine-readable handling.

Envelope

{
"type": "about:blank",
"title": "Forbidden",
"status": 403,
"detail": "API key does not have read_calls permission.",
"code": "perm.denied",
"request_id": "8c1c1b2a-9f3e-4a4d-9f9b-2c0e0f1a2b3c"
}
FieldTypeDescription
typeURICurrently always about:blank — the title is the canonical type label
titlestringShort human-readable summary (HTTP status text)
statusintHTTP status code (matches the response status)
detailstringLonger human-readable explanation
codestringStable machine identifier — branch on this
request_iduuidPass to support; also echoed in X-Request-Id response header

Content-Type is application/problem+json; charset=utf-8 per RFC 9457. Most JSON clients parse it fine; if yours is strict about content type, accept application/*+json.

Code reference

codeStatusMeaningAction
auth.missing401No Authorization headerAdd the header
auth.invalid401Token doesn't match voka_live_… formatCheck key value
auth.revoked401Key was disabled in the dashboardRe-authenticate
auth.expired401Key passed its expires_atMint a new key
perm.denied403Key lacks the required scopeAdd the scope in the dashboard
rate.exceeded429Per-customer bucket emptyHonor Retry-After
rate.exceeded_key429Per-API-key bucket emptyHonor Retry-After
input.invalid422Request input failed validationSee issues[] for field-level details
resource.not_found404Resource doesn't exist or belongs to another customerCheck the ID
resource.conflict409Conflicts with existing state (rare)Reload + retry
carrier.unavailable502Upstream provider unavailableTransient — retry with backoff
internal.error500Unexpected server-side errorRetry; if persistent, share request_id with support

Validation errors carry per-field issues

When you send invalid input — bad enum value, malformed UUID, out-of-range number — the response includes an issues array:

curl "https://voice.vokaai.com/api/v1/calls?limit=abc&direction=sideways" \
-H "Authorization: Bearer voka_live_..."
{
"type": "about:blank",
"title": "Unprocessable Entity",
"status": 422,
"detail": "Request input failed validation.",
"code": "input.invalid",
"request_id": "...",
"issues": [
{ "path": ["limit"], "message": "Expected number, received string" },
{ "path": ["direction"], "message": "Invalid enum value. Expected 'inbound' | 'outbound'" }
]
}

Handling errors in a wrapper

Recommended branching pattern (TypeScript):

async function callVoka(path: string, init: RequestInit) {
const res = await fetch(`https://voice.vokaai.com${path}`, init);
if (res.ok) return res.json();

const problem = await res.json();
switch (problem.code) {
case 'auth.revoked':
case 'auth.expired':
// Prompt user to re-authenticate
throw new ReAuthRequiredError(problem.detail);

case 'rate.exceeded':
case 'rate.exceeded_key':
// Wait and retry
const retryAfter = parseInt(res.headers.get('retry-after') ?? '60');
await sleep(retryAfter * 1000);
return callVoka(path, init);

case 'input.invalid':
// Show field-level errors to the user
throw new ValidationError(problem.issues);

case 'carrier.unavailable':
case 'internal.error':
// Transient — retry with backoff, surface request_id on persistent failure
throw new TransientError(problem.detail, problem.request_id);

default:
throw new UnknownError(problem);
}
}

Filing a bug

Include the request_id from the failing response — it lets us trace the exact request through our logs and audit trails. Without it triage is much slower.

Email: support@vokaai.com.