Error Codes
All Staffify API errors follow a consistent format. This page covers HTTP status codes, authentication errors, call lifecycle errors, and validation errors across every endpoint.
Error response format
There are two error envelope shapes across the API. Most REST v1 endpoints use a flat envelope. The OpenAI-compatible /v1/chat/completions endpoint uses a nested envelope that matches the OpenAI SDK error shape so client libraries parse it natively.
Flat envelope (REST v1)
Used by every endpoint under /api/v1/* except /chat/completions. Body includes a code, a human-readable message, and optionally a details object with field-level info.
// Standard error
HTTP/1.1 401 Unauthorized
{
"code": "INVALID_API_KEY",
"message": "The API key provided is invalid or has been revoked."
}
// Validation error with field details
HTTP/1.1 400 Bad Request
{
"code": "VALIDATION_ERROR",
"message": "Request validation failed.",
"details": {
"language": "Must be a valid BCP-47 language code.",
"max_duration": "Must be between 60 and 7200."
}
}Nested envelope (chat completions)
Used by POST /api/v1/chat/completions. Every field is present on every error. Every response also includes an X-Request-Id header that matches error.request_id. Include that value when you contact support.
HTTP/1.1 402 Payment Required
X-Request-Id: chatreq-8a1e6b9f-3c2d-4d5f-9e2b-1a0b2c3d4e5f
{
"error": {
"message": "Insufficient credits. Estimated worst-case cost €0.0362 exceeds balance €0.0100.",
"type": "invalid_request_error",
"code": "INSUFFICIENT_CREDITS",
"param": null,
"request_id": "chatreq-8a1e6b9f-3c2d-4d5f-9e2b-1a0b2c3d4e5f"
}
}type is one of invalid_request_error, authentication_error, permission_error, rate_limit_error, upstream_error, server_error. See the full list of chat completions codes on the chat completions page.
Always check the code field programmatically. Never match on the message string, which may change between releases.
HTTP status codes
| Status | Meaning | When it occurs |
|---|---|---|
| 200 | OK | Request succeeded. Body contains the response data. |
| 201 | Created | Resource created successfully (e.g. POST /v1/agents, POST /v1/webhooks). |
| 400 | Bad Request | Request validation failed. Check the details object for field-level errors. |
| 401 | Unauthorized | Missing, invalid, or revoked API key. See authentication errors below. |
| 402 | Payment Required | Insufficient credits to complete the action. |
| 403 | Forbidden | The key does not have permission for this action (e.g. read-only key on a write endpoint, or project key on an org endpoint). |
| 404 | Not Found | The requested resource does not exist, or belongs to a different project. |
| 409 | Conflict | Operation cannot be completed due to current resource state (e.g. deleting an agent with active calls, or an Idempotency-Key reused with a different body). |
| 413 | Payload Too Large | Request body exceeds the 2 MB cap on chat completions, or the prompt exceeds the per-tier input token ceiling. |
| 422 | Unprocessable Entity | The request was well-formed but the action failed downstream (e.g. a phone transfer rejected by the carrier). |
| 429 | Too Many Requests | Rate limit exceeded. Check X-RateLimit-Remaining and X-RateLimit-Reset headers. |
| 500 | Internal Server Error | Unexpected server error. Retry with exponential back-off. Contact support if persistent. |
Authentication errors
| Code | HTTP | Description |
|---|---|---|
| MISSING_API_KEY | 401 | No Authorization or X-Api-Key header was provided. |
| INVALID_API_KEY | 401 | The API key does not exist or has been revoked. |
| INSUFFICIENT_CREDITS | 402 | Your credit balance is zero or below the minimum required for this action. |
| READ_ONLY_KEY | 403 | The API key is read-only and cannot be used for write operations (POST, PATCH, DELETE). |
| ORG_KEY_REQUIRED | 403 | This endpoint requires an org-scoped key (sfy_org_...), but a project key was provided. |
| PROJECT_NOT_FOUND | 404 | When using an org key, the X-Project-Id header references a project that does not exist or does not belong to this org. |
| RATE_LIMIT_EXCEEDED | 429 | Requests per minute limit reached for your tier. See X-RateLimit-Reset for when the window resets. |
| AUTH_RATE_LIMITED | 429 | Too many failed authentication attempts from this IP (100 failures in 15 minutes triggers a temporary block). |
Chat completions error codes
These codes are specific to POST /api/v1/chat/completions and use the nested envelope with a type field.
| Code | HTTP | Type | Description |
|---|---|---|---|
| UNSUPPORTED_MODEL | 400 | invalid_request_error | model field must be "staffi-1.0". |
| INVALID_MESSAGES | 400 | invalid_request_error | messages missing or not a non-empty array. |
| MODEL_NOT_IN_PLAN | 403 | permission_error | Requested model is not available on your tier. |
| WALLET_MISSING | 402 | invalid_request_error | No credits wallet exists for this organization. Contact support. |
| INPUT_TOO_LARGE | 413 | invalid_request_error | Prompt exceeds the per-tier input token ceiling. |
| REQUEST_TOO_LARGE | 413 | invalid_request_error | Request body larger than 2 MB. |
| AI_RATE_LIMIT_EXCEEDED | 429 | rate_limit_error | Chat completions per-minute limit exceeded (stricter than the general REST RATE_LIMIT_EXCEEDED). |
| AI_CONCURRENT_LIMIT_EXCEEDED | 429 | rate_limit_error | Too many simultaneous in-flight requests. Reduce parallelism. |
| IDEMPOTENCY_KEY_TOO_LONG | 400 | invalid_request_error | Idempotency-Key header exceeds 128 characters. |
| IDEMPOTENCY_KEY_CONFLICT | 409 | invalid_request_error | Same Idempotency-Key already used with a different request body. |
| IDEMPOTENCY_STREAMING_REPLAY | 409 | invalid_request_error | Same Idempotency-Key was used on a streaming request. Streaming responses cannot be replayed. |
| IDEMPOTENCY_KEY_QUOTA_EXCEEDED | 429 | rate_limit_error | Exceeded 10,000 unique Idempotency-Keys for this API key in 24 hours. |
| INVALID_REQUEST | 400 | invalid_request_error | Upstream provider rejected the request as invalid. |
| UPSTREAM_FORBIDDEN | 403 | permission_error | Upstream provider refused the request. |
| UPSTREAM_RATE_LIMIT | 429 | rate_limit_error | Upstream provider is throttling. Retry with backoff. |
| UPSTREAM_UNAVAILABLE | 502 | upstream_error | Upstream provider temporarily unavailable. Retry. |
| UPSTREAM_ERROR | 502 | upstream_error | Upstream provider returned an unexpected error. |
| UPSTREAM_AUTH_FAILURE | 502 | server_error | Internal issue reaching the upstream provider. Contact support. |
| UPSTREAM_NOT_FOUND | 502 | upstream_error | Upstream provider returned an unexpected 404. |
For security reasons the message field on chat-completion errors never echoes fragments of your request. Include request_id when contacting support.
Rate limit headers
These headers are included on every API response so you can monitor your limit consumption before hitting a 429.
| Header | Description |
|---|---|
| X-RateLimit-Limit | Maximum general REST requests per minute for your tier (all /api/v1/* endpoints). |
| X-RateLimit-Remaining | Requests remaining in the current 1-minute window. |
| X-RateLimit-Reset | Unix timestamp (seconds) when the rate limit window resets. |
| X-RateLimit-Tier | Your current tier (payg, starter, growth, scale, enterprise). |
| X-Concurrent-Call-Limit | Maximum simultaneous active calls allowed on your tier. |
| X-AI-RateLimit-Limit | Chat completions per-minute cap (stricter than the general REST cap). Only on /api/v1/chat/completions. |
| X-AI-RateLimit-Remaining | Chat completions requests remaining in the current 1-minute window. |
| X-AI-RateLimit-Reset | Unix timestamp (seconds) when the chat completions window resets. |
| X-AI-RateLimit-Tier | Tier used for the chat completions caps. |
| X-AI-Concurrent-Limit | Maximum simultaneous in-flight chat completions requests. |
| X-AI-Concurrent-Current | Current in-flight chat completions requests count. |
| X-Request-Id | Unique per-request identifier. Present on chat completions responses. Include in support tickets. |
// Handle 429 with exponential back-off
async function apiRequest(url, options, attempt = 0) {
const res = await fetch(url, options);
if (res.status === 429) {
const reset = res.headers.get('X-RateLimit-Reset');
const waitMs = reset
? (parseInt(reset) * 1000 - Date.now()) + 100
: Math.min(1000 * 2 ** attempt, 30000);
await new Promise(r => setTimeout(r, waitMs));
return apiRequest(url, options, attempt + 1);
}
return res;
}Call ended_reason values
The ended_reason field on a completed call tells you exactly why the call ended. Available in GET /v1/calls/:callId and in the call.ended webhook payload.
| ended_reason | What happened |
|---|---|
| agent-hangup | The AI agent ended the call (e.g. used the end-call tool or end_call_enabled triggered). |
| caller-hangup | The caller hung up. |
| api | The call was ended via POST /v1/calls/:callId/end. |
| transfer | The call was transferred to another number via POST /v1/calls/:callId/transfer. |
| timeout | The call reached its max_duration limit. |
| silence-timeout | The inactivity_timeout was reached: the caller was silent for too long. |
| error | An internal error occurred during the call (e.g. STT/TTS failure, LLM error). Check the call detail for more context. |
| max-duration | Alias for timeout. The global maximum call duration was reached. |
Call status and disposition
status
| ringing | Call connected, agent starting up. |
| in-progress | Active live call. |
| completed | Call ended normally. |
| failed | Call ended due to an error. |
| busy | Destination was busy (transfers only). |
| no-answer | Call was not answered. |
| canceled | Call canceled before connecting. |
| rejected | Call rejected (e.g. insufficient credits, concurrency limit). |
disposition
| answered | Call was answered and conversation took place. |
| no-answer | Never answered (missed). |
| transferred | Caller was transferred to another number. |
| busy | Line was busy. |
| failed | Call failed before connection. |
| voicemail | Reached voicemail (future). |
| rejected | Call rejected before ringing. |
Common validation errors
| Scenario | HTTP | Error message |
|---|---|---|
| Missing required field (e.g. name, server_url) | 400 | name is required. |
| Invalid language code | 400 | language must be a supported BCP-47 code. |
| Voice not available for language | 400 | Voice is not available for the selected language. |
| server_url is not HTTPS or not reachable | 400 | server_url must be a valid public HTTPS URL. |
| server_url blocked by SSRF filter (internal IP) | 400 | server_url blocked by SSRF filter. |
| max_duration out of range | 400 | max_duration must be between 60 and 7200. |
| knowledge_base_ids exceeds limit of 20 | 400 | knowledge_base_ids cannot exceed 20 entries. |
| Deleting agent with active calls | 409 | Agent has active calls and cannot be deleted. |
| Purchasing number with no address (non-US/CA) | 400 | address_id is required for this country. |
| Phone number not in E.164 format | 400 | to must be a valid E.164 phone number. |
| SMS message exceeds 160 characters | 400 | message must not exceed 160 characters. |
| Transfer while call is not in-progress | 409 | Call is not in-progress and cannot be transferred. |
Retrying requests
The following errors are safe to retry. All others should not be retried automatically without fixing the underlying cause first.
Safe to retry
429: after the rate limit window resets500: with exponential back-off (max 3 attempts)503: service temporarily unavailable
Do not retry
400: fix the request first401: fix or rotate the API key402: add credits first403: use the correct key type404: resource does not exist409: resolve the conflict first