Error Handling
The anyformat API uses conventional HTTP response codes and provides structured error responses to help you build robust integrations.
HTTP Status Codes
| Status Code | Meaning | Description |
|---|
200 | OK | Request succeeded |
201 | Created | Resource was successfully created |
202 | Accepted | Request accepted for processing (async operations) |
204 | No Content | Request succeeded with no response body |
400 | Bad Request | Invalid request data or parameters |
401 | Unauthorized | Invalid API key (rejected by backend validation) |
403 | Forbidden | Missing API key, empty Bearer token, or insufficient permissions |
404 | Not Found | Resource doesn’t exist |
412 | Precondition Failed | Results not yet complete (poll again) |
429 | Too Many Requests | Rate limit exceeded (check Retry-After header) |
500 | Internal Server Error | Unexpected server error |
502 | Bad Gateway | Upstream service error |
503 | Service Unavailable | Service temporarily unavailable |
504 | Gateway Timeout | Backend did not respond in time |
All error responses follow a consistent JSON structure:
{
"error": "Brief, human-readable error description",
"detail": "Detailed explanation of what went wrong",
"error_code": "MACHINE_READABLE_ERROR_CODE",
"retryable": false,
"request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
| Field | Description |
|---|
error | Short, human-readable summary |
detail | Detailed explanation for debugging (may be an array for validation errors) |
error_code | Machine-readable code for programmatic handling |
retryable | Whether the client should retry this request |
request_id | Unique request identifier for debugging and support |
Error Codes Reference
| Error Code | HTTP Status | Retryable | Description |
|---|
AUTH_FAILED | 401 | No | Invalid API key (rejected by backend) |
ACCESS_DENIED | 403 | No | Missing API key, empty Bearer token, or insufficient permissions |
VALIDATION_ERROR | 400 | No | Request validation failed |
INVALID_JSON | 400 | No | Malformed JSON in request body |
NOT_FOUND | 404 | No | Resource not found |
PRECONDITION_FAILED | 412 | Yes | Results not yet complete (poll again) |
RATE_LIMITED | 429 | Yes | Too many requests (check Retry-After header) |
INTERNAL_ERROR | 500/502/503 | Yes | Server error |
GATEWAY_TIMEOUT | 504 | Yes | Backend did not respond in time |
Authentication Errors
AUTH_FAILED (401) — Invalid API key
{
"error": "Authentication failed",
"detail": "Invalid or missing authentication credentials.",
"error_code": "AUTH_FAILED",
"retryable": false,
"request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
ACCESS_DENIED (403) — Missing API key or insufficient permissions
{
"error": "Access denied",
"detail": "API key is required. Use 'Authorization: Bearer <key>'.",
"error_code": "ACCESS_DENIED",
"retryable": false,
"request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
Validation Errors
VALIDATION_ERROR (400) — The detail field is an array of validation error objects:
{
"error": "Validation failed",
"detail": [
{"type": "missing", "loc": ["body", "name"], "msg": "Field required"},
{"type": "missing", "loc": ["body", "fields"], "msg": "Field required"}
],
"error_code": "VALIDATION_ERROR",
"retryable": false,
"request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
INVALID_JSON (400)
{
"error": "Invalid JSON format",
"detail": "The request body contains invalid JSON. Please check your JSON syntax.",
"error_code": "INVALID_JSON",
"retryable": false,
"request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
Resource Errors
NOT_FOUND (404)
{
"error": "Resource not found",
"detail": "The requested resource could not be found.",
"error_code": "NOT_FOUND",
"retryable": false,
"request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
Processing Status
PRECONDITION_FAILED (412) — Results not yet available.
The retryable flag indicates whether to continue polling:
retryable: true — processing is still running. Poll again with backoff.
retryable: false — processing reached a terminal failure (error or cancelled). Stop polling.
{
"error": "Extraction not yet available",
"detail": "Extraction status is 'processing'. Results are available when status is 'processed'.",
"error_code": "PRECONDITION_FAILED",
"retryable": true,
"request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
Rate Limiting
RATE_LIMITED (429) — Response includes Retry-After header
{
"error": "Rate limited",
"detail": "Rate limit exceeded. Try again in 12s.",
"error_code": "RATE_LIMITED",
"retryable": true,
"request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
Rate-limited responses include a Retry-After header indicating how many seconds to wait before retrying. All successful responses include x-ratelimit-limit, x-ratelimit-remaining, and x-ratelimit-reset headers showing the limit for the tier that applies to that endpoint (extraction or general). The Retry-After header is only present on 429 responses.
Server Errors
INTERNAL_ERROR (500/502/503)
{
"error": "Internal server error",
"detail": "An unexpected error occurred while processing your request. Please try again or contact support.",
"error_code": "INTERNAL_ERROR",
"retryable": true,
"reference_id": "err_1234",
"request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
GATEWAY_TIMEOUT (504)
{
"error": "Gateway timeout",
"detail": "The backend did not respond in time. Please try again.",
"error_code": "GATEWAY_TIMEOUT",
"retryable": true,
"request_id": "a1b2c3d4e5f67890abcdef1234567890"
}
Server errors (500) include a reference_id field. Use this ID along with the request_id when contacting support to help identify and resolve the specific issue.
Error Handling Best Practices
1. Check HTTP Status Codes
import requests
response = requests.get(
"https://api.anyformat.ai/v2/workflows/",
headers={"Authorization": "Bearer your-api-key"}
)
if response.status_code == 200:
workflows = response.json()
elif response.status_code == 401:
print("Authentication failed. Check your API key.")
elif response.status_code == 403:
print("Access denied. API key may be missing.")
elif response.status_code == 404:
print("Resource not found.")
elif response.status_code == 412:
print("Results not ready yet. Retry after a delay.")
elif response.status_code == 429:
retry_after = response.headers.get("Retry-After", "5")
print(f"Rate limited. Retry after {retry_after}s.")
elif response.status_code >= 500:
print("Server error. Retry with backoff.")
else:
error = response.json()
print(f"Error: {error.get('detail')}")
2. Use Error Codes for Logic
def handle_api_error(response):
if response.status_code >= 400:
error_data = response.json()
error_code = error_data.get('error_code')
if error_code == 'AUTH_FAILED':
refresh_authentication()
elif error_code == 'ACCESS_DENIED':
print("Missing or invalid API key.")
elif error_code == 'VALIDATION_ERROR':
show_validation_error(error_data.get('detail'))
elif error_code == 'NOT_FOUND':
handle_missing_resource()
elif error_code == 'PRECONDITION_FAILED':
# Still processing — retry
return retry_after_delay()
elif error_code == 'RATE_LIMITED':
retry_after = int(response.headers.get('Retry-After', 5))
time.sleep(retry_after)
return retry_request()
elif error_code == 'INTERNAL_ERROR':
retry_with_backoff()
elif error_code == 'GATEWAY_TIMEOUT':
retry_with_backoff()
else:
log_unexpected_error(error_data)
3. Implement Retry Logic
For retryable errors (5xx, 429, 412), implement exponential backoff:
import time
import random
def api_request_with_retry(url, headers, max_retries=3):
for attempt in range(max_retries + 1):
try:
response = requests.get(url, headers=headers)
# Don't retry non-retryable client errors
if 400 <= response.status_code < 500 and response.status_code not in (412, 429):
return response
# Handle rate limiting
if response.status_code == 429:
delay = int(response.headers.get('Retry-After', 5))
time.sleep(delay)
continue
# Handle results polling
if response.status_code == 412 and attempt < max_retries:
time.sleep(5)
continue
# Retry server errors (5xx)
if response.status_code >= 500 and attempt < max_retries:
delay = (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
continue
return response
except requests.exceptions.RequestException as e:
if attempt < max_retries:
delay = (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
continue
raise e
4. Validate Before Sending
def create_workflow(name, description, fields):
# Validate required fields locally first
if not name:
raise ValueError("Workflow name is required")
if not fields or len(fields) == 0:
raise ValueError("At least one field is required")
for field in fields:
if not field.get('name'):
raise ValueError("Field name is required")
if not field.get('data_type'):
raise ValueError("Field data_type is required")
# Make API request
response = requests.post(
"https://api.anyformat.ai/v2/workflows/",
headers={"Authorization": "Bearer your-api-key", "Content-Type": "application/json"},
json={"name": name, "description": description, "fields": fields}
)
return handle_response(response)
5. Log Errors for Debugging
import logging
def log_api_error(response, context=""):
try:
error_data = response.json()
error_code = error_data.get('error_code', 'UNKNOWN')
request_id = error_data.get('request_id', 'N/A')
reference_id = error_data.get('reference_id', 'N/A')
logging.error(
f"API Error {response.status_code}: {error_code} - "
f"{error_data.get('detail', 'No detail')} "
f"[Context: {context}] [Request: {request_id}] [Reference: {reference_id}]"
)
except ValueError:
logging.error(f"API Error {response.status_code}: {response.text}")
Testing Error Scenarios
When building your integration, test these common scenarios:
| Scenario | How to Test |
|---|
| Missing API key | Omit the Authorization header |
| Invalid API key | Use a fake or expired key |
| Missing required fields | Create workflow without name or fields |
| Invalid JSON | Send malformed JSON in request body |
| Nonexistent resource | Request a workflow or file ID that doesn’t exist |
| Results not ready | Poll GET /v2/files/{id}/extraction/ before processing completes (expect 412) |
| Rate limit exceeded | Send rapid requests to trigger 429 with Retry-After header |
| Server errors | Test retry logic (mock 500 responses) |
Framework-Level Errors
While most errors use the structured { "error", "detail", "error_code", "retryable", "request_id" } format, some framework-level responses (e.g., unknown URL path) may return a simpler format:{ "detail": "Not found." }