Error Handling
Best practices for handling DocuRift API errors
Error Response Format
All DocuRift API errors follow a consistent JSON structure, making it easy to handle errors programmatically:
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable error message",
"details": {
"field": "Additional context about the error"
}
},
"requestId": "req_abc123def456"
}
Response Fields
| Field | Type | Description |
|-------|------|-------------|
| success | boolean | Always false for error responses |
| error.code | string | Machine-readable error code (see Error Codes) |
| error.message | string | Human-readable description |
| error.details | object | Additional context (optional) |
| requestId | string | Unique identifier for support requests |
HTTP Status Codes
DocuRift uses standard HTTP status codes to indicate the general category of error:
| Status | Meaning | Common Causes | |--------|---------|---------------| | 400 | Bad Request | Invalid parameters, malformed JSON, unsupported file type | | 401 | Unauthorized | Missing or invalid API key | | 402 | Payment Required | Insufficient credits or free tier exhausted | | 403 | Forbidden | Access denied, IP blocked, or resource not owned | | 404 | Not Found | Document or resource doesn't exist | | 413 | Payload Too Large | File exceeds 50MB limit | | 429 | Too Many Requests | Rate limit or concurrent request limit exceeded | | 500 | Internal Error | Server-side processing error |
Retry Strategy
For transient errors (5xx, 429), implement exponential backoff with jitter:
Recommended Settings
| Parameter | Value | |-----------|-------| | Initial delay | 1 second | | Maximum delay | 60 seconds | | Maximum retries | 5 | | Backoff multiplier | 2 | | Jitter | 0-500ms random |
Retryable Errors
Only retry these error codes:
429- Rate limit exceeded500- Internal server error502- Bad gateway503- Service unavailable504- Gateway timeout
Do not retry 4xx errors (except 429) as they indicate client-side issues.
Error Handling Examples
Python
import time
import random
import requests
from typing import Optional
class DocuRiftError(Exception):
def __init__(self, code: str, message: str, status: int, request_id: str):
self.code = code
self.message = message
self.status = status
self.request_id = request_id
super().__init__(f"{code}: {message}")
def process_document(file_path: str, api_key: str, max_retries: int = 5) -> dict:
"""Process a document with automatic retry logic."""
base_delay = 1
max_delay = 60
for attempt in range(max_retries + 1):
try:
with open(file_path, 'rb') as f:
response = requests.post(
'https://api.docurift.com/v1/documents/process',
headers={'X-API-Key': api_key},
files={'file': f},
data={'documentType': 'invoice'}
)
data = response.json()
if response.ok:
return data
# Extract error details
error = data.get('error', {})
request_id = data.get('requestId', 'unknown')
# Check if error is retryable
if response.status_code in [429, 500, 502, 503, 504]:
if attempt < max_retries:
delay = min(base_delay * (2 ** attempt), max_delay)
jitter = random.uniform(0, 0.5)
time.sleep(delay + jitter)
continue
# Non-retryable error or max retries exceeded
raise DocuRiftError(
code=error.get('code', 'UNKNOWN_ERROR'),
message=error.get('message', 'Unknown error'),
status=response.status_code,
request_id=request_id
)
except requests.RequestException as e:
if attempt < max_retries:
delay = min(base_delay * (2 ** attempt), max_delay)
time.sleep(delay)
continue
raise
raise DocuRiftError('MAX_RETRIES', 'Maximum retries exceeded', 0, 'none')
# Usage
try:
result = process_document('invoice.pdf', 'your-api-key')
print(f"Extracted data: {result['data']}")
except DocuRiftError as e:
print(f"API Error [{e.code}]: {e.message}")
print(f"Request ID for support: {e.request_id}")
except Exception as e:
print(f"Unexpected error: {e}")
JavaScript / TypeScript
interface DocuRiftError {
code: string;
message: string;
details?: Record<string, unknown>;
}
interface DocuRiftResponse<T> {
success: boolean;
data?: T;
error?: DocuRiftError;
requestId: string;
}
class DocuRiftAPIError extends Error {
constructor(
public code: string,
public message: string,
public status: number,
public requestId: string,
public details?: Record<string, unknown>
) {
super(`${code}: ${message}`);
this.name = 'DocuRiftAPIError';
}
}
async function processDocument(
file: File,
apiKey: string,
maxRetries = 5
): Promise<DocuRiftResponse<unknown>> {
const baseDelay = 1000;
const maxDelay = 60000;
const retryableCodes = [429, 500, 502, 503, 504];
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const formData = new FormData();
formData.append('file', file);
formData.append('documentType', 'invoice');
const response = await fetch(
'https://api.docurift.com/v1/documents/process',
{
method: 'POST',
headers: { 'X-API-Key': apiKey },
body: formData,
}
);
const data: DocuRiftResponse<unknown> = await response.json();
if (response.ok) {
return data;
}
// Check if we should retry
if (retryableCodes.includes(response.status) && attempt < maxRetries) {
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
const jitter = Math.random() * 500;
await new Promise((resolve) => setTimeout(resolve, delay + jitter));
continue;
}
// Throw non-retryable error
throw new DocuRiftAPIError(
data.error?.code || 'UNKNOWN_ERROR',
data.error?.message || 'Unknown error occurred',
response.status,
data.requestId,
data.error?.details
);
} catch (error) {
if (error instanceof DocuRiftAPIError) {
throw error;
}
// Network error - retry if attempts remain
if (attempt < maxRetries) {
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
throw new DocuRiftAPIError(
'MAX_RETRIES',
'Maximum retries exceeded',
0,
'none'
);
}
// Usage
async function handleDocument(file: File) {
try {
const result = await processDocument(file, 'your-api-key');
console.log('Extracted data:', result.data);
} catch (error) {
if (error instanceof DocuRiftAPIError) {
console.error(`API Error [${error.code}]: ${error.message}`);
console.error(`Request ID for support: ${error.requestId}`);
// Handle specific errors
switch (error.code) {
case 'INSUFFICIENT_CREDITS':
// Redirect to billing page
break;
case 'RATE_LIMIT_EXCEEDED':
// Show rate limit message
break;
default:
// Show generic error
break;
}
} else {
console.error('Unexpected error:', error);
}
}
}
Logging Best Practices
Always Include Request ID
The requestId is essential for debugging. Include it in all error logs:
import logging
logger = logging.getLogger('docurift')
try:
result = process_document('invoice.pdf', api_key)
except DocuRiftError as e:
logger.error(
"DocuRift API error",
extra={
'error_code': e.code,
'error_message': e.message,
'request_id': e.request_id,
'http_status': e.status
}
)
Structured Logging Format
{
"timestamp": "2024-01-15T10:30:00Z",
"level": "error",
"service": "document-processor",
"event": "docurift_api_error",
"error_code": "PROCESSING_FAILED",
"error_message": "OCR processing failed for document",
"request_id": "req_abc123def456",
"http_status": 500,
"file_name": "invoice.pdf",
"file_size": 2048576
}
Contacting Support
When contacting support, always include:
- Request ID - Found in every API response
- Timestamp - When the error occurred
- Error code and message - The full error response
- File details - Type, size, and anonymized sample if possible
- Steps to reproduce - What API call triggered the error
Common Error Patterns
Handling Credit Exhaustion
try:
result = process_document('invoice.pdf', api_key)
except DocuRiftError as e:
if e.code == 'INSUFFICIENT_CREDITS':
# Check current balance
balance = get_credit_balance(api_key)
logger.warning(f"Low credits: {balance['remaining']} remaining")
# Notify admin or auto-purchase
notify_low_credits(balance)
raise
Handling Rate Limits Gracefully
from datetime import datetime, timedelta
class RateLimitTracker:
def __init__(self):
self.reset_time = None
def handle_rate_limit(self, error: DocuRiftError):
if 'retryAfter' in error.details:
self.reset_time = datetime.now() + timedelta(
seconds=error.details['retryAfter']
)
else:
self.reset_time = datetime.now() + timedelta(seconds=60)
def can_make_request(self) -> bool:
if self.reset_time is None:
return True
return datetime.now() >= self.reset_time
Handling Concurrent Limits
import asyncio
from asyncio import Semaphore
# Limit to 5 concurrent requests (matching API limit)
semaphore = Semaphore(5)
async def process_with_limit(file_path: str, api_key: str):
async with semaphore:
return await async_process_document(file_path, api_key)
# Process multiple documents while respecting limits
async def batch_process(files: list[str], api_key: str):
tasks = [process_with_limit(f, api_key) for f in files]
results = await asyncio.gather(*tasks, return_exceptions=True)
return results