Error Codes
All error responses follow a consistent JSON structure. The error.code field contains a machine-readable string identifier that you should use in your error handling logic. Never rely on the message field, which may change.
Error response format
{
"success": false,
"error": {
"code": "FACE_NOT_FOUND",
"message": "No face found with the given ID in this collection."
},
"request_id": "req_01j8...",
"timestamp": "2024-01-15T11:22:33Z"
}HTTP status codes
| Status | Meaning |
|---|---|
| 200 OK | Request succeeded. |
| 201 Created | Resource created successfully. |
| 400 Bad Request | Missing or invalid parameters. |
| 401 Unauthorized | Missing or invalid authentication credentials. |
| 403 Forbidden | Valid credentials but insufficient permissions or IP blocked. |
| 404 Not Found | Requested resource does not exist. |
| 409 Conflict | Resource already exists (e.g. duplicate external_id). |
| 422 Unprocessable Entity | Validation passed but business logic rejected the request. |
| 429 Too Many Requests | Rate limit exceeded. |
| 500 Internal Server Error | Unexpected server error. Contact support with request_id. |
Authentication errors
| Code | HTTP | Description |
|---|---|---|
| INVALID_CREDENTIALS | 401 | Email or password is incorrect. |
| TOKEN_EXPIRED | 401 | JWT access token has expired. Refresh it. |
| TOKEN_INVALID | 401 | JWT token is malformed or tampered. |
| INVALID_API_KEY | 401 | API key does not exist or has been revoked. |
| IP_NOT_ALLOWED | 403 | Request source IP is not in the key's whitelist. |
| REQUIRES_2FA | 403 | Account has 2FA enabled; OTP code required. |
| EMAIL_NOT_VERIFIED | 403 | Email address has not been verified. |
Face & recognition errors
| Code | HTTP | Description |
|---|---|---|
| FACE_NOT_FOUND | 404 | No face with the given face_id or external_id in this collection. |
| FACE_NOT_DETECTED | 422 | No face was detected in the submitted image. |
| MULTIPLE_FACES | 422 | More than one face detected; submit a single-face image. |
| FACE_QUALITY_TOO_LOW | 422 | Quality score below threshold (blur, occlusion, extreme pose). |
| LIVENESS_CHECK_FAILED | 422 | Image failed anti-spoofing check (print/replay/mask detected). |
| DUPLICATE_EXTERNAL_ID | 409 | An active face with this external_id already exists in the collection. |
Collection & API key errors
| Code | HTTP | Description |
|---|---|---|
| COLLECTION_NOT_FOUND | 404 | Collection does not exist or belongs to a different org. |
| COLLECTION_LIMIT_REACHED | 403 | Plan collection limit reached. Upgrade to create more. |
| FACE_LIMIT_REACHED | 403 | Collection face limit reached for your current plan. |
| API_KEY_LIMIT_REACHED | 403 | API key limit per member reached for your current plan. |
| RATE_LIMIT_EXCEEDED | 429 | API key RPM limit exceeded. See Retry-After header. |
Validation errors
When multiple fields fail validation, the error includes a details array:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed.",
"details": [
{"field": "name", "message": "name is required"},
{"field": "collection_id", "message": "collection_id must be a valid UUID"}
]
}
}Handling rate limits
When a 429 is returned, check the Retry-After header for the number of seconds to wait before retrying:
async function identifyWithRetry(formData, apiKey, retries = 3) {
for (let i = 0; i < retries; i++) {
const res = await fetch(
'https://your-domain.com/api/v1/collections/COLLECTION_ID/identify',
{ method: 'POST', headers: { 'X-API-Key': apiKey }, body: formData }
)
if (res.status === 429) {
const wait = parseInt(res.headers.get('Retry-After') ?? '5', 10)
await new Promise(r => setTimeout(r, wait * 1000))
continue
}
const data = await res.json()
if (!data.success) throw new Error(data.error.code)
return data
}
throw new Error('Max retries exceeded')
}