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
Every error response returns a JSON body with a code and a human-readable message. Some errors include a details object with field-level information.
// 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."
}
}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). |
| 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). |
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 requests per minute for your tier. |
| 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. |
// 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