Error Handling
ProofAge uses conventional HTTP status codes and structured JSON error responses. This guide covers every error format, code, and reason you may encounter so you can build robust error handling into your integration.
Error Response Formats
The API returns errors in three distinct formats depending on where the error originates.
Format 1: Standard API Errors
Returned by authentication middleware and general API errors. The response contains a top-level error object:
{
"error": {
"code": "INVALID_API_KEY",
"message": "The provided API key is not valid."
}
}| Field | Type | Description |
|---|---|---|
error.code | string | Machine-readable error code (see catalog below). |
error.message | string | Human-readable description of the error. |
Format 2: Media Validation Errors
Returned when an uploaded media file fails quality or content checks. The error is a flat object (no error wrapper):
{
"code": "FACE_TOO_BLURRY",
"message": "The uploaded selfie is too blurry. Please upload a clearer image."
}| Field | Type | Description |
|---|---|---|
code | string | Machine-readable validation error code. |
message | string | Human-readable description suitable for display to end users. |
Format 3: Laravel Validation Errors
Returned when request input fails validation rules (e.g., missing required fields, wrong types). This follows the standard Laravel validation response format:
{
"message": "The flow field is required.",
"errors": {
"flow": [
"The flow field is required."
],
"mode": [
"The mode field must be one of: live, test."
]
}
}| Field | Type | Description |
|---|---|---|
message | string | Summary of the first validation error. |
errors | object | Map of field names to arrays of error messages. |
Error Code Catalog
Authentication Errors
These errors are returned by the authentication middleware when a request cannot be authorized.
| HTTP Status | Code | Description |
|---|---|---|
401 | MISSING_API_KEY | The X-API-Key header was not provided. |
401 | INVALID_API_KEY | The API key does not match any active workspace. |
401 | NO_SECRET_KEYS | The workspace has no secret keys. Generate one in the dashboard. |
401 | MISSING_SIGNATURE | The X-HMAC-Signature header is required but was not provided. |
401 | INVALID_SIGNATURE | The HMAC signature does not match the expected value. Check your secret key and signing logic. |
Authorization Errors
| HTTP Status | Code | Description |
|---|---|---|
403 | WORKSPACE_SUSPENDED | The workspace has been suspended. Contact support. |
403 | CONSENT_REQUIRED | User consent has not been recorded for this verification. Call the consent endpoint first. |
Billing Errors
| HTTP Status | Code | Description |
|---|---|---|
402 | PAYMENT_METHOD_REQUIRED | No payment method is on file. Add one in the dashboard to use live mode. |
Rate Limiting
| HTTP Status | Code | Description |
|---|---|---|
429 | RATE_LIMIT | Too many requests. The default limit is 60 requests per minute per API key. |
When you receive a 429 response, the Retry-After header indicates how many seconds to wait before retrying:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json
{
"error": {
"code": "RATE_LIMIT",
"message": "Rate limit exceeded. Try again in 30 seconds."
}
}Submission Errors
Returned when attempting to submit a verification that is not in a submittable state.
| HTTP Status | Code | Description |
|---|---|---|
422 | INVALID_STATUS | The verification is not in a status that allows submission (e.g., already submitted or already in a terminal state). |
422 | MISSING_REQUIRED_MEDIA | The verification is missing required media files. For age flows, a selfie is required. For kyc flows, a selfie and at least one document image are required. |
Media Validation Errors
Returned synchronously when an uploaded media file fails quality checks. These use Format 2.
Selfie Validation
| HTTP Status | Code | Description |
|---|---|---|
422 | FACE_NOT_FOUND | No face was detected in the uploaded image. |
422 | FACE_INVALID_SIZE | The face is too small or too large relative to the frame. |
422 | FACE_TOO_BLURRY | The image is too blurry for reliable analysis. |
422 | FACE_BRIGHTNESS_INVALID | The image is too dark or too bright. |
422 | FACE_OCCLUDED | Part of the face is covered (e.g., hand, mask, sunglasses). |
422 | FACE_ALIGNMENT_FAILED | The face landmarks could not be aligned for processing. |
422 | FACE_TOO_TILTED | The face is rotated too far from a frontal position. |
422 | FACE_VALIDATION_FAILED | A general face validation failure not covered by the codes above. |
Document Validation
| HTTP Status | Code | Description |
|---|---|---|
422 | DOCUMENT_NOT_FOUND | No document was detected in the uploaded image. |
422 | DOCUMENT_TOO_BLURRY | The document image is too blurry to read. |
422 | DOCUMENT_BRIGHTNESS_INVALID | The document image is too dark or too bright. |
422 | DOCUMENT_NOT_READABLE | The document text could not be extracted. Ensure all corners are visible and the image is in focus. |
422 | DOCUMENT_VALIDATION_FAILED | A general document validation failure not covered by the codes above. |
HTTP Status Code Summary
| Status Code | Meaning | Typical Use |
|---|---|---|
200 | OK | Successful read or update. |
201 | Created | Resource successfully created (verification, media). |
400 | Bad Request | Malformed JSON or invalid request structure. |
401 | Unauthorized | Missing or invalid API key / signature. |
402 | Payment Required | No payment method on file. |
403 | Forbidden | Workspace suspended or consent not given. |
404 | Not Found | Verification or resource does not exist. |
408 | Request Timeout | The request took too long to complete. |
422 | Unprocessable Entity | Validation failure (input, media, or status). |
429 | Too Many Requests | Rate limit exceeded. |
500 | Internal Server Error | Unexpected server error. Contact support if persistent. |
Decision Reason Catalog
When a verification reaches declined or resubmission_requested status, the webhook payload includes a reason field. Reasons are grouped by severity and category, each with an internal priority range that determines which reason takes precedence when multiple issues are detected.
Blocklist & Document Rejection (Priority 100–159) — Declined
These are hard rejections with no option to resubmit.
| Reason | Priority | Description |
|---|---|---|
blocklist_match | 100 | Biometrics match a previously blocklisted identity. |
not_a_document | 150 | The uploaded document image is not a recognized identity document. |
Fraud Detection (Priority 200–249) — Declined
Automated fraud signals result in immediate decline.
| Reason | Priority | Description |
|---|---|---|
spoof_detected | 200 | Liveness check detected a spoofing attempt (printed photo, screen capture, mask). |
document_tampering | 210 | Signs of physical or digital alteration on the document. |
ai_generated | 220 | The submitted image appears to be AI-generated or synthetically manipulated. |
Identity & Business Rules (Priority 300–330) — Declined
The verification failed identity matching or business rule checks.
| Reason | Priority | Description |
|---|---|---|
face_mismatch | 300 | The selfie does not match the face on the identity document. |
age_threshold_not_met | 310 | The estimated age is below the required threshold. |
duplicate_identity | 320 | The same biometric identity has already been verified under a different external ID. |
Quality Issues (Priority 400–479) — Resubmission Requested
The user can fix these issues by re-uploading better media.
| Reason | Priority | Description |
|---|---|---|
selfie_too_blurry | 400 | The selfie is too blurry for reliable analysis. |
selfie_bad_lighting | 410 | The selfie is too dark or overexposed. |
selfie_face_occluded | 420 | Part of the face is covered in the selfie. |
selfie_poor_angle | 425 | The face is too tilted or not facing the camera. |
document_too_blurry | 440 | The document image is too blurry to read. |
document_bad_lighting | 450 | The document image has poor lighting. |
document_damaged | 460 | The document appears physically damaged or partially obscured. |
document_not_readable | 470 | The document text could not be extracted. |
Technical Failures (Priority 900) — Resubmission Requested
Transient processing errors. Safe to retry without changing the media.
| Reason | Priority | Description |
|---|---|---|
processing_error | 900 | An internal error occurred during pipeline processing. |
TIP
When multiple issues are detected, only the highest-priority reason (lowest number) is returned. For example, if a selfie is both blurry and a spoofing attempt, the reason will be spoof_detected (priority 200), not selfie_too_blurry (priority 400).
Best Practices
Differentiate Error Formats
Check for the presence of specific keys to determine which error format you received:
function handleApiError(response) {
const body = response.data;
if (body.error && body.error.code) {
// Format 1: Standard API error
return { code: body.error.code, message: body.error.message };
}
if (body.code) {
// Format 2: Media validation error
return { code: body.code, message: body.message };
}
if (body.errors) {
// Format 3: Laravel validation error
const firstField = Object.keys(body.errors)[0];
return { code: "VALIDATION_ERROR", message: body.errors[firstField][0] };
}
return { code: "UNKNOWN_ERROR", message: "An unexpected error occurred." };
}Implement Retry Logic
Not all errors are retryable. Follow this decision tree:
| Status | Action |
|---|---|
401, 403 | Do not retry. Fix authentication/authorization first. |
402 | Do not retry. Add a payment method in the dashboard. |
404 | Do not retry. The resource does not exist. |
408 | Retry after a brief delay. |
422 | Do not retry with the same payload. Fix the validation issue or prompt the user to upload better media. |
429 | Retry after the number of seconds in the Retry-After header. |
5xx | Retry with exponential backoff (e.g., 1s, 2s, 4s, up to 30s). |
Surface User-Facing Messages
Media validation errors (Format 2) include messages suitable for showing to end users. Use them directly in your UI to guide users toward better uploads:
"The uploaded selfie is too blurry. Please upload a clearer image."Map Reason Codes to User Actions
When you receive a webhook with resubmission_requested, map the reason to a clear instruction:
| Reason | Suggested User Message |
|---|---|
selfie_too_blurry | "Your selfie was blurry. Please retake it in good lighting." |
selfie_bad_lighting | "Your selfie was too dark or bright. Try a well-lit area." |
selfie_face_occluded | "Your face was partially covered. Remove glasses, masks, or hats." |
document_too_blurry | "Your document photo was blurry. Hold your phone steady." |
document_not_readable | "We could not read your document. Make sure all four corners are visible." |
processing_error | "Something went wrong on our end. Please try again." |
Log Error Codes
Always log the full error response (code, message, HTTP status) for debugging. Do not discard error details silently.
Next Steps
- Webhooks — Set up webhook receivers and verify signatures.
- Testing — Test error scenarios in sandbox mode.
- Verification Flow — Understand the full verification lifecycle.