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.
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
| type | Statuses | Meaning |
|---|---|---|
invalid_request_error | 400, 405, 406, 409, 410, 413, 415 | The request is malformed, a parameter is invalid (param names it), the version is unknown or retired, or the body is too large. |
authentication_error | 401 | No usable credential. Also carries WWW-Authenticate with the resource-metadata URL. |
permission_error | 403 | The credential is valid but may not do this: missing scope, IP outside the key's allowlist, or a policy check. |
not_found_error | 404 | No such object or route. Objects that belong to another account also return 404. |
insufficient_credits_error | 402 | Reserved for paid operations (call placement, transcription requests). Carries billing_url. No live endpoint returns it yet. |
call_error | 422 | Reserved for a destination the carrier rejected. No live endpoint returns it yet. |
idempotency_error | 409 | Idempotency-Key conflicts and other concurrency conflicts. |
rate_limit_error | 429 | A request budget is exhausted. Retry-After says when to retry. |
api_error | 500, 503 | Our 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_request400 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_body400 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_version400 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_sunset410 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_query400 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_token401 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_scope403 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_allowed403 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.
-
forbidden403 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_found404 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_found404 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_allowed405 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_acceptable406 invalid_request_error -
Where: /v1
The Accept header excludes application/json.
What to do: Send Accept: application/json or omit Accept.
-
unsupported_media_type415 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_large413 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_limit400 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_cursor400 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_required400 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_key400 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_progress409 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_reused409 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.
-
conflict409 idempotency_error -
Where: /v1 (generic)
A concurrency conflict without a more specific code.
What to do: Re-read the object and retry.
-
rate_limited429 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.
-
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_error500 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_name400 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_scopes400 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_expiry400 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_allowlist400 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_limit409 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_revoked409 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_expired409 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_found404 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.
Codes: consent and connections (/api/oauth/*, signed-in session only)
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_found404 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_failed403 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_decided409 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_expired410 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_disabled403 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_required400 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_decision400 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_found404 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.
{"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_request400-
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_grant400-
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_scope400-
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_type400-
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.
-
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_error500-
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).
| error | Delivered as | Meaning |
|---|---|---|
invalid_client | error page (no redirect) | client_id is unknown, its metadata document could not be fetched or validated, or the client is disabled. |
invalid_request | error page or redirect | On 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_type | redirect | response_type is not code. |
invalid_target | redirect | resource is missing, repeated, or not exactly https://api.voixcall.com/v1 or https://api.voixcall.com/mcp. |
invalid_scope | redirect | scope is empty or lists a scope the server does not know. |
access_denied | redirect | The user chose Deny on the consent page. |
server_error | redirect | The 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.
| error | Meaning |
|---|---|
invalid_client_metadata | The 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_uri | More 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.