Developer#API#Integration#Developer

OCR API Integration: A Developer's Guide to Best Practices

OCR Platform Team

December 02, 20257 min read

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

  1. Respond quickly - Return 200 immediately, process asynchronously
  2. Handle duplicates - Webhooks may be sent multiple times
  3. Implement idempotency - Use event IDs to prevent duplicate processing
  4. 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:

  1. Choose sync vs. async based on document size and latency requirements
  2. Implement comprehensive error handling with appropriate retry strategies
  3. Respect rate limits with client-side throttling
  4. Use webhooks for long-running operations
  5. Cache results to reduce redundant processing
  6. Monitor everything with appropriate alerting thresholds

Following these patterns ensures your document extraction integration handles production workloads reliably.

Tagged with:

#API#Integration#Developer#Best Practices
111 views
Last updated: Jan 01, 2026