VoixCall

VoixCall for developers

Errors

One JSON envelope everywhere, a stable code per condition, and this page as the anchor every doc_url points at.

The envelope

Every error from /v1, /mcp (before the JSON-RPC layer), /v1/api-keys and the consent endpoints has this shape, with Content-Type: application/json and Cache-Control: no-store. Every field is always present (null when absent); billing_url appears only on insufficient_credits_error. message is safe to show to an end user. request_id equals the Request-Id response header.

GET /v1/me without a token (recorded from production)
HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Cache-Control: no-store
Request-Id: req_Fa3rR6ZW8v3AM31xlv9CYQ
VoixCall-Version: 2026-10-01
WWW-Authenticate: Bearer error="invalid_token", resource_metadata="https://api.voixcall.com/.well-known/oauth-protected-resource/v1"

{
  "error": {
    "type": "authentication_error",
    "code": "invalid_token",
    "message": "Provide a bearer access token.",
    "param": null,
    "doc_url": "https://voixcall.com/developers/errors#invalid_token",
    "request_id": "req_Fa3rR6ZW8v3AM31xlv9CYQ"
  }
}

type is the coarse class for your switch; code is the specific condition. Match on code, treat unknown codes as their type, and never parse message.

Types

typeStatusesMeaning
invalid_request_error400, 405, 406, 409, 410, 413, 415The request is malformed, a parameter is invalid (param names it), the version is unknown or retired, or the body is too large.
authentication_error401No usable credential. Also carries WWW-Authenticate with the resource-metadata URL.
permission_error403The credential is valid but may not do this: missing scope, IP outside the key's allowlist, or a policy check.
not_found_error404No such object or route. Objects that belong to another account also return 404.
insufficient_credits_error402Reserved for paid operations (call placement, transcription requests). Carries billing_url. No live endpoint returns it yet.
call_error422Reserved for a destination the carrier rejected. No live endpoint returns it yet.
idempotency_error409Idempotency-Key conflicts and other concurrency conflicts.
rate_limit_error429A request budget is exhausted. Retry-After says when to retry.
api_error500, 503Our fault. Retry with the same Idempotency-Key where one applies, then write to support with the request_id.

Codes: /v1 and /mcp

Each code is an anchor: https://voixcall.com/developers/errors#<code>, which is the doc_url the API sends.

invalid_request 400 invalid_request_error

Where: any /v1 operation

Request validation failed; param names the offending field or query parameter and message says what is wrong.

What to do: Fix the named parameter and retry. Also the catch-all for other 4xx conditions without a more specific code.

invalid_body 400 invalid_request_error

Where: POST /v1/*, /v1/api-keys, consent decision

The body could not be read or is not the JSON shape the endpoint expects.

What to do: Send valid JSON with Content-Type: application/json and the documented fields.

invalid_version 400 invalid_request_error

Where: any /v1 request with a VoixCall-Version header

The header names a version the server does not know. message lists the known versions (currently 2026-10-01).

What to do: Send a known version or omit the header to use your pinned or the current version.

version_sunset 410 invalid_request_error

Where: any /v1 request on a retired version

The effective VoixCall-Version passed its Sunset date. message links the migration guide. No version is deprecated today.

What to do: Move to the current version; see the changelog entry for the migration.

token_in_query 400 invalid_request_error

Where: /v1, /mcp

An access_token query parameter was present. Tokens in URLs end up in logs, so they are refused before any lookup.

What to do: Send the token only in the Authorization: Bearer header.

invalid_token 401 authentication_error

Where: /v1, /mcp, /v1/api-keys, /api/oauth/*

No bearer token, or one that is invalid, expired, revoked, of the wrong kind (a refresh token, an API key at /mcp) or bound to a different resource. The message says which: “Provide a bearer access token.”, “The access token is invalid or expired.”, “Reconnect VoixCall in your assistant.” (the authorization was revoked or the password changed), “The API key is invalid or its account is closed.”, or “Sign in to continue.” on session-only endpoints.

What to do: Refresh the token, or re-run the OAuth flow; for “Reconnect”, the user must connect the app again. The WWW-Authenticate header carries the resource-metadata URL for discovery.

insufficient_scope 403 permission_error

Where: scoped /v1 operations and MCP tools (none live yet: /v1/me and voixcall_whoami accept any scope)

The token's scopes do not include the one this operation needs. WWW-Authenticate: Bearer error="insufficient_scope", scope="<needed>".

What to do: Ask the user to reconnect with the named scope. Tokens never gain scopes after consent.

ip_not_allowed 403 permission_error

Where: /v1 with an API key

The key is valid but the request came from an address outside the key's IP allowlist.

What to do: Call from an allowed address, or edit the key's allowlist.

forbidden 403 permission_error

Where: /v1 (generic)

A policy check refused the request and no more specific code applies.

What to do: Read message; if it is unexpected, write to support with the request_id.

not_found 404 not_found_error

Where: /v1 object reads

No object with that id in the connected account. Objects owned by other accounts look identical to missing ones.

What to do: Check the id and its prefix (usr_, call_, …).

route_not_found 404 not_found_error

Where: any unknown path under /v1

No such endpoint. message links the OpenAPI document.

What to do: Compare the path and method with the reference; most operations are still planned.

method_not_allowed 405 invalid_request_error

Where: /v1

The path exists but not with this HTTP method.

What to do: Use the method the reference lists for the path.

not_acceptable 406 invalid_request_error

Where: /v1

The Accept header excludes application/json.

What to do: Send Accept: application/json or omit Accept.

unsupported_media_type 415 invalid_request_error

Where: /v1 POST

The body's Content-Type is not one the endpoint accepts.

What to do: Send Content-Type: application/json.

request_too_large 413 invalid_request_error

Where: /v1 (256 KB), /v1/api-keys (16 KB)

The body exceeds the cap for that endpoint.

What to do: Send a smaller body; no legitimate request needs more.

invalid_limit 400 invalid_request_error

Where: list endpoints (GET /v1/api-keys today; the phase 2 lists when they ship)

limit is not an integer between 1 and 100. param is limit.

What to do: Use 1–100; the default is 10.

invalid_cursor 400 invalid_request_error

Where: list endpoints (GET /v1/api-keys today; the phase 2 lists when they ship)

starting_after or ending_before is missing the object prefix, is not one of your objects, or both cursors were sent. param names the cursor.

What to do: Pass exactly one cursor, using an id from a previous page of the same list.

idempotency_key_required 400 invalid_request_error

Where: POST /v1/calls and POST /v1/calls/{id}/transcript (planned)

This operation spends money and requires an Idempotency-Key header.

What to do: Send Idempotency-Key: <UUID v4> and reuse it when retrying the same request.

invalid_idempotency_key 400 invalid_request_error

Where: any /v1 POST with the header

Idempotency-Key is empty, longer than 255 characters, or contains non-printable ASCII.

What to do: Use a UUID v4.

idempotency_in_progress 409 idempotency_error

Where: any /v1 POST with the header

A request with the same key is still executing.

What to do: Wait a moment and retry with the same key; you will get the stored response.

idempotency_key_reused 409 idempotency_error

Where: any /v1 POST with the header

The key was already used within 24 hours with a different method, path or body.

What to do: Use a new key for a new request. Keys are unique per user and credential.

conflict 409 idempotency_error

Where: /v1 (generic)

A concurrency conflict without a more specific code.

What to do: Re-read the object and retry.

rate_limited 429 rate_limit_error

Where: /v1, /mcp, /v1/api-keys, /.well-known/*, openapi.json

A request budget is exhausted. Retry-After (seconds) and the RateLimit headers say which budget and when it refills. Also returned instead of 401 when a source exceeds 60 failed bearer checks a minute.

What to do: Back off for Retry-After seconds. Budgets are on the rate limits page.

rate_limiter_unavailable 503 api_error

Where: /v1, /mcp, /v1/api-keys, /.well-known/*

The rate limiter's store is unreachable; public paths fail closed rather than open. Retry-After: 5.

What to do: Retry after five seconds. If it persists, we are already paged; write to support with the request_id.

internal_error 500 api_error

Where: anywhere

Something failed on our side. The message never contains internal detail.

What to do: Retry once; if it persists, write to support with the request_id.

Codes: API-key management (/v1/api-keys, signed-in session only)

These endpoints take the dashboard session, not an API key or OAuth token; the Settings screen that calls them ships later.

invalid_name 400 invalid_request_error

Where: POST /v1/api-keys

name is empty or too long. param is name.

What to do: Use 1 to 100 characters.

invalid_scopes 400 invalid_request_error

Where: POST /v1/api-keys

scopes is empty or contains an unknown scope. param is scopes.

What to do: List at least one scope from the scope table.

invalid_expiry 400 invalid_request_error

Where: POST /v1/api-keys

expires_in_days is outside 1 to 365. param is expires_in_days.

What to do: Omit it for the 90-day default, or choose 1–365.

invalid_ip_allowlist 400 invalid_request_error

Where: POST /v1/api-keys

ip_allowlist has too many entries or one that is not an IP address or CIDR. param is ip_allowlist.

What to do: Use valid IPv4/IPv6 addresses or CIDR ranges.

api_key_limit 409 invalid_request_error

Where: POST /v1/api-keys

You already have 10 live keys.

What to do: Revoke a key you no longer use, then create the new one.

api_key_revoked 409 invalid_request_error

Where: POST /v1/api-keys/{id}/rotate

The key was revoked and cannot be rotated.

What to do: Create a new key.

api_key_expired 409 invalid_request_error

Where: POST /v1/api-keys/{id}/rotate

The key has expired and cannot be rotated.

What to do: Create a new key.

api_key_not_found 404 not_found_error

Where: /v1/api-keys/{id}

No key with that id in your account.

What to do: List your keys and use an id from the list.

Returned to the consent page on app.voixcall.com and to the Connected apps screen. Listed because their doc_url also points here.

transaction_not_found 404 not_found_error

Where: GET/POST /api/oauth/transactions/{id}

The authorization request id does not exist.

What to do: Start the flow again from the client.

transaction_binding_failed 403 permission_error

Where: GET/POST /api/oauth/transactions/{id}

The consent page was opened in a different browser from the one that started the flow, or by a different user.

What to do: Go back to the client and start again in the same browser.

transaction_decided 409 idempotency_error

Where: GET/POST /api/oauth/transactions/{id}

This authorization request was already allowed or denied.

What to do: Nothing to do; start a new flow if you need another decision.

transaction_expired 410 invalid_request_error

Where: GET/POST /api/oauth/transactions/{id}

More than 10 minutes passed before a decision.

What to do: Start the flow again from the client.

client_disabled 403 permission_error

Where: GET /api/oauth/transactions/{id}

The client application was disabled by VoixCall.

What to do: Contact support if you believe this is a mistake.

confirmation_required 400 invalid_request_error

Where: POST /api/oauth/transactions/{id}/decision

A self-registered client asked for a sensitive scope and the confirmation checkbox was not ticked.

What to do: Tick the confirmation and allow again, or deny.

invalid_decision 400 invalid_request_error

Where: POST /api/oauth/transactions/{id}/decision

decision is neither allow nor deny.

What to do: Send {"decision": "allow"} or {"decision": "deny"}.

connection_not_found 404 not_found_error

Where: DELETE /api/oauth/connections/{client_id}

No connected app with that client id on this account.

What to do: List connections and use a client_id from the list.

OAuth endpoints (/oauth/*)

The token, revocation and registration endpoints answer in the RFC 6749 §5.2 / RFC 7591 shape, not the envelope, because OAuth clients parse {"error": …}. error_description is present where it helps. No Request-Id is echoed in these bodies, but the header is still on the response.

OAuth error bodies
{"error": "invalid_grant"}
{"error": "temporarily_unavailable", "error_description": "too many requests; retry after 12 seconds"}

Token and revocation: POST /oauth/token, POST /oauth/revoke

invalid_request 400

A required form field is missing (grant_type, code, code_verifier, client_id, redirect_uri, resource on the code grant; refresh_token and client_id on refresh), the body is not form-encoded, or the token to revoke is missing.

What to do: Send every field as application/x-www-form-urlencoded in the body, never in the query string.

invalid_grant 400

The code or refresh token is unknown, expired, already used, issued to another client, bound to a different redirect_uri or resource, or the PKCE verifier does not match. A reused refresh token revokes its whole grant family.

What to do: Start a new authorization. Three failures with one credential revoke it.

invalid_scope 400

A refresh asked for scopes the grant does not hold.

What to do: Omit scope, or request a subset of the granted scopes.

unsupported_grant_type 400

grant_type is not authorization_code or refresh_token.

What to do: Use one of the two supported grants; there are no client-credentials or password grants.

temporarily_unavailable 429 or 503

429 with Retry-After: this source is backed off after failed exchanges, this grant family refreshed more than 10 times a minute, or the token-endpoint flood guard tripped. 503 with Retry-After: the limiter or the database was unreachable, or (on /oauth/register) registration is closed because too many clients are pending purge.

What to do: Wait Retry-After seconds and retry the same request.

server_error 500

The exchange failed on our side.

What to do: Retry; if it persists, write to support.

/oauth/revoke returns 200 for unknown tokens too (RFC 7009), so it cannot be used to probe.

Authorization: GET /oauth/authorize

A bad client or redirect URI renders an error page and never redirects. Everything else redirects back to the client's redirect_uri with error, error_description, state and iss (RFC 9207).

errorDelivered asMeaning
invalid_clienterror page (no redirect)client_id is unknown, its metadata document could not be fetched or validated, or the client is disabled.
invalid_requesterror page or redirectOn the error page: redirect_uri is not registered for the client. On redirect: state, code_challenge or code_challenge_method is missing, or the method is not S256.
unsupported_response_typeredirectresponse_type is not code.
invalid_targetredirectresource is missing, repeated, or not exactly https://api.voixcall.com/v1 or https://api.voixcall.com/mcp.
invalid_scoperedirectscope is empty or lists a scope the server does not know.
access_deniedredirectThe user chose Deny on the consent page.
server_errorredirectThe authorization request could not be stored.

Registration: POST /oauth/register

400 with an RFC 7591 error; 503 temporarily_unavailable with Retry-After: 3600 when registration is closed.

errorMeaning
invalid_client_metadataThe body is not a JSON object, application_type is not web or native, token_endpoint_auth_method is not none, grant_types or response_types include unsupported values, client_name exceeds 100 characters, or client_uri is not an http(s) URL.
invalid_redirect_uriMore than 10 redirect_uris, or one that is not an https URL or an http loopback URL, or has a fragment, userinfo or wildcard; web clients may register https only.

429 and 503 on /oauth/*

Rate limits and lockouts on the OAuth routes answer 429 {"error": "temporarily_unavailable"} with Retry-After and the RateLimit headers; RFC 6749 has no rate-limit code and invalid_request would wrongly tell a compliant client its request was malformed. When the limiter's store is unreachable the same routes answer 503 {"error": "temporarily_unavailable"} with Retry-After: 5. Budgets are on the rate limits page.

Errors inside MCP

HTTP-level failures on /mcp (401, 403, 429, 503) use the envelope above. Once a request is authenticated, tool failures come back as MCP results with isError: true and a plain sentence such as “Not connected. Reconnect VoixCall in your assistant.” or “VoixCall is temporarily unavailable. Try again.”; JSON-RPC errors are reserved for unknown tools and malformed arguments.