OCR API Integration: A Developer's Guide to Best Practices
OCR Platform Team
Technical deep-dive into integrating document extraction APIs effectively. Covers error handling, rate limiting, webhook patterns, and production-ready implementation strategies.
OCR API Integration: A Developer's Guide to Best Practices
Integrating OCR APIs into production applications requires careful attention to reliability, performance, and error handling. This guide covers essential patterns for building robust document extraction integrations.
API Architecture Overview
Request Flow
Client Application
↓
API Gateway
↓
Load Balancer
↓
OCR Service Cluster
↓
Processing Queue
↓
Extraction Workers
↓
Result Storage
↓
Webhook/Polling Response
Authentication
All API requests require authentication via API keys:
curl -X POST https://api.ocrplatform.com/v1/extract \
-H "Authorization: Bearer ds_live_xxxxxxxxxxxx" \
-H "Content-Type: multipart/form-data" \
-F "document=@passport.jpg" \
-F "type=passport"
Key Management Best Practices:
- Use environment variables, never hardcode
- Rotate keys quarterly
- Implement separate keys for development/staging/production
- Monitor key usage for anomalies
Synchronous vs. Asynchronous Processing
Synchronous (Small Documents)
For documents under 5MB with expected processing under 10 seconds:
const extractDocument = async (file, type) => {
const formData = new FormData();
formData.append('document', file);
formData.append('type', type);
const response = await fetch('https://api.ocrplatform.com/v1/extract', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OCR_API_KEY}`
},
body: formData
});
if (!response.ok) {
throw new OCRError(response.status, await response.json());
}
return response.json();
};
Asynchronous (Large Documents/Batch)
For multi-page documents or batch processing:
// Step 1: Submit job
const submitJob = async (documents, type) => {
const response = await fetch('https://api.ocrplatform.com/v1/jobs', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OCR_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
documents: documents.map(d => d.url),
type,
webhook_url: 'https://yourapp.com/webhooks/ocr'
})
});
return response.json(); // { job_id: "job_xxxxx" }
};
// Step 2: Receive webhook or poll
const checkJobStatus = async (jobId) => {
const response = await fetch(
`https://api.ocrplatform.com/v1/jobs/${jobId}`,
{
headers: {
'Authorization': `Bearer ${process.env.OCR_API_KEY}`
}
}
);
return response.json();
};
Error Handling Strategies
Error Classification
| HTTP Code | Meaning | Retry Strategy | |-----------|---------|----------------| | 400 | Bad request | No retry, fix request | | 401 | Unauthorized | Check API key | | 413 | File too large | Compress or split | | 422 | Unprocessable | Document quality issue | | 429 | Rate limited | Exponential backoff | | 500 | Server error | Retry with backoff | | 503 | Service unavailable | Retry with backoff |
Implementing Retry Logic
const extractWithRetry = async (file, type, maxRetries = 3) => {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await extractDocument(file, type);
} catch (error) {
lastError = error;
// Don't retry client errors
if (error.status >= 400 && error.status < 500 && error.status !== 429) {
throw error;
}
// Exponential backoff
const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
await sleep(delay);
}
}
throw lastError;
};
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
Graceful Degradation
const extractWithFallback = async (file, type) => {
try {
// Primary extraction
return await extractDocument(file, type);
} catch (error) {
if (error.code === 'DOCUMENT_QUALITY_LOW') {
// Attempt with enhanced preprocessing
const enhanced = await enhanceImage(file);
return await extractDocument(enhanced, type);
}
if (error.code === 'EXTRACTION_PARTIAL') {
// Return partial results with flag
return {
...error.partialResults,
extraction_complete: false,
requires_review: true
};
}
throw error;
}
};
Rate Limiting and Throttling
Understanding Rate Limits
| Plan | Requests/Minute | Concurrent Jobs | Burst | |------|-----------------|-----------------|-------| | Free | 10 | 2 | 15 | | Starter | 60 | 10 | 100 | | Professional | 300 | 50 | 500 | | Enterprise | Custom | Custom | Custom |
Implementing Client-Side Rate Limiting
class RateLimiter {
constructor(maxRequests, windowMs) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
this.requests = [];
}
async acquire() {
const now = Date.now();
this.requests = this.requests.filter(t => now - t < this.windowMs);
if (this.requests.length >= this.maxRequests) {
const oldestRequest = this.requests[0];
const waitTime = this.windowMs - (now - oldestRequest);
await sleep(waitTime);
return this.acquire();
}
this.requests.push(now);
return true;
}
}
// Usage
const rateLimiter = new RateLimiter(60, 60000); // 60 req/min
const rateLimitedExtract = async (file, type) => {
await rateLimiter.acquire();
return extractDocument(file, type);
};
Webhook Implementation
Setting Up Webhook Endpoint
// Next.js API route example
export async function POST(request) {
const signature = request.headers.get('x-ocr-signature');
const body = await request.text();
// Verify webhook signature
if (!verifySignature(body, signature)) {
return Response.json({ error: 'Invalid signature' }, { status: 401 });
}
const payload = JSON.parse(body);
switch (payload.event) {
case 'extraction.completed':
await handleExtractionComplete(payload.data);
break;
case 'extraction.failed':
await handleExtractionFailed(payload.data);
break;
case 'job.progress':
await handleJobProgress(payload.data);
break;
}
return Response.json({ received: true });
}
const verifySignature = (payload, signature) => {
const expected = crypto
.createHmac('sha256', process.env.OCR_WEBHOOK_SECRET)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
};
Webhook Best Practices
- Respond quickly - Return 200 immediately, process asynchronously
- Handle duplicates - Webhooks may be sent multiple times
- Implement idempotency - Use event IDs to prevent duplicate processing
- Set up monitoring - Alert on webhook failures
Batch Processing Patterns
Queue-Based Architecture
import { Queue, Worker } from 'bullmq';
const extractionQueue = new Queue('document-extraction', {
connection: redisConnection
});
// Producer: Add documents to queue
const queueExtraction = async (documents) => {
const jobs = documents.map(doc => ({
name: 'extract',
data: {
documentId: doc.id,
documentUrl: doc.url,
type: doc.type
}
}));
await extractionQueue.addBulk(jobs);
};
// Consumer: Process queue
const worker = new Worker('document-extraction', async (job) => {
const { documentId, documentUrl, type } = job.data;
try {
const result = await extractDocument(documentUrl, type);
await saveExtractionResult(documentId, result);
return result;
} catch (error) {
await logExtractionError(documentId, error);
throw error;
}
}, {
connection: redisConnection,
concurrency: 10
});
Progress Tracking
const processBatch = async (documents, onProgress) => {
const total = documents.length;
let completed = 0;
let failed = 0;
const results = [];
await Promise.all(
documents.map(async (doc) => {
try {
const result = await extractDocument(doc.file, doc.type);
results.push({ id: doc.id, success: true, data: result });
completed++;
} catch (error) {
results.push({ id: doc.id, success: false, error: error.message });
failed++;
}
onProgress({
total,
completed,
failed,
percentage: Math.round(((completed + failed) / total) * 100)
});
})
);
return results;
};
Caching Strategies
Result Caching
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
const CACHE_TTL = 86400; // 24 hours
const extractWithCache = async (file, type) => {
// Generate cache key from file hash
const fileHash = await hashFile(file);
const cacheKey = `ocr:${type}:${fileHash}`;
// Check cache
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Extract and cache
const result = await extractDocument(file, type);
await redis.setex(cacheKey, CACHE_TTL, JSON.stringify(result));
return result;
};
const hashFile = async (file) => {
const buffer = await file.arrayBuffer();
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
return Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
};
Monitoring and Observability
Key Metrics to Track
const metrics = {
requestCount: new Counter('ocr_requests_total'),
requestDuration: new Histogram('ocr_request_duration_seconds'),
errorCount: new Counter('ocr_errors_total'),
queueDepth: new Gauge('ocr_queue_depth')
};
const instrumentedExtract = async (file, type) => {
metrics.requestCount.inc({ type });
const timer = metrics.requestDuration.startTimer({ type });
try {
const result = await extractDocument(file, type);
timer({ status: 'success' });
return result;
} catch (error) {
metrics.errorCount.inc({ type, error: error.code });
timer({ status: 'error' });
throw error;
}
};
Alerting Rules
| Metric | Threshold | Alert | |--------|-----------|-------| | Error rate | > 5% | Page on-call | | Latency p99 | > 30s | Warning | | Queue depth | > 1000 | Warning | | Rate limit hits | > 10/min | Review capacity |
Security Considerations
Data Handling
// Don't log sensitive extraction results
const sanitizeForLogging = (result) => ({
document_type: result.document_type,
extraction_confidence: result.confidence,
field_count: Object.keys(result.fields).length,
// Never log actual field values
});
// Encrypt at rest
const storeResult = async (id, result) => {
const encrypted = await encrypt(JSON.stringify(result));
await db.extractionResults.create({
id,
data: encrypted,
created_at: new Date()
});
};
Conclusion
Building production-ready OCR integrations requires attention to reliability, performance, and security. Key takeaways:
- Choose sync vs. async based on document size and latency requirements
- Implement comprehensive error handling with appropriate retry strategies
- Respect rate limits with client-side throttling
- Use webhooks for long-running operations
- Cache results to reduce redundant processing
- Monitor everything with appropriate alerting thresholds
Following these patterns ensures your document extraction integration handles production workloads reliably.
Tagged with: