Home Blog How to Convert 100 Invoices in 10 Minutes with Python (myocr SDK Tutorial) 2026-05-26 11 min read How to Convert 100 Invoices in 10 Minutes with Python (myocr SDK Tutorial) From zero to a working batch pipeline using the official myocr-client Python SDK. Real code, real error handling, webhook delivery. By myocr.app team 100 PDF invoices → 100 structured Excel files in under 10 minutes with 30 lines of Python Why a dedicated Python SDK matters If you're shipping production OCR in Python, you have three options. You can call the API with raw requests and write the boilerplate yourself — auth, retries, error mapping, polling for async jobs, signed-URL downloads. You can use a generic OpenAPI generator that produces a working but ugly client. Or you can install a maintained SDK that abstracts the protocol and lets you write business code. This tutorial uses the third option: myocr-client, the official Python SDK for myocr.app. In 11 minutes you'll go from pip install to a script that converts 100 PDF invoices in parallel, gracefully handles quota and OCR errors, and notifies your app via webhook when each job finishes. What you'll build A single Python script that: Reads a folder of PDF invoices (mixed digital and scanned) Submits them as async jobs via /v1/batch Polls the API or receives webhook notifications when each job is done Downloads structured Excel files (Vendor, Customer, Line Items, Total, VAT) Recovers from quota exhaustion, network blips, and per-file OCR errors without crashing Total: ~30 lines of business code, plus a webhook receiver if you want push-based delivery. Prerequisites Python 3.8+ An API key from myocr.app/account/api (free tier: 100 calls/month, no card) A folder of test PDF invoices — or use the sample fixtures in the SDK repo Step 1: Install pip install myocr-client That's all. The SDK depends only on requests. No native extensions, no SDK-specific runtime. Step 2: Convert your first invoice (sync) from myocr_client import MyOCRClient client = MyOCRClient(api_key="sk_live_...") # or set MYOCR_API_KEY env var result = client.convert("invoice.pdf", model="invoice") result.save("invoice.xlsx") print(f"{result.pages_used} pages → invoice.xlsx ({result.request_id})") That's a complete OCR call. The convert() method is synchronous and best for files under 5 MB and 10 pages. The returned ConversionResult includes content (bytes), pages_used, the original model, and a request_id you can grep for in server logs. Step 3: Switch to async for batch processing For 100 invoices, you don't want 100 sequential sync calls — that's serial latency. Use /v1/batch instead. The SDK exposes it as client.batch(): from pathlib import Path from myocr_client import MyOCRClient client = MyOCRClient() # MYOCR_API_KEY from env folder = Path("./invoices") pdfs = sorted(folder.glob("*.pdf"))[:20] # /v1/batch accepts up to 20 files batch = client.batch([str(p) for p in pdfs], model="invoice") print(f"Created {batch.jobs_created} jobs, {len(batch.errors)} errors") done = batch.wait_all(timeout=600) for job in done: if job.is_done: job.download(f"out/{job.request_id}.xlsx") else: print(f"Failed: {job.request_id} — {job.error_detail}") Three things to notice. First, the /v1/batch endpoint accepts at most 20 files per call — for 100 invoices, chunk your folder into 5 batches. Second, wait_all() polls each job individually with exponential backoff (1s → 2s → 4s → 8s → 15s cap), so 100 idle sleep(1) calls don't burn through your rate limit. Third, if a single file fails OCR extraction, only that Job ends up in failed — the others complete normally. Step 4: Process 100 invoices in 5 chunks from pathlib import Path from myocr_client import MyOCRClient client = MyOCRClient() folder = Path("./invoices") all_pdfs = sorted(folder.glob("*.pdf")) CHUNK = 20 out_dir = Path("./out"); out_dir.mkdir(exist_ok=True) for i in range(0, len(all_pdfs), CHUNK): chunk = all_pdfs[i:i + CHUNK] print(f"\nBatch {i // CHUNK + 1}: {len(chunk)} files") batch = client.batch([str(p) for p in chunk], model="invoice") for job in batch.wait_all(timeout=900): target = out_dir / f"{job.request_id}.xlsx" if job.is_done: job.download(str(target)) print(f" ✓ {target.name} ({job.pages_used} pages)") else: print(f" ✗ {job.request_id}: {job.error_detail}") print("\nDone.") Total wall time on a free-tier key with default concurrency: roughly 8-10 minutes for 100 invoices, dominated by the OCR processing latency, not by your code. On paid plans with higher concurrency the same workload completes in 3-5 minutes. Step 5: Handle errors like a grown-up The SDK maps every API error code to a typed exception. Code defensively: from myocr_client import ( MyOCRClient, QuotaExceeded, OcrEngineError, RateLimited, InvalidApiKey, FileTooLarge, ) client = MyOCRClient() try: result = client.convert("invoice.pdf", model="invoice") except QuotaExceeded as e: print(f"Plan {e.current_plan}: {e.calls_used}/{e.calls_limit} calls used") print(f"Upgrade at {e.upgrade_url}, resets {e.reset_date}") except FileTooLarge: print("Use create_job() instead of convert() — switch to async path") except OcrEngineError: pass except RateLimited: pass except InvalidApiKey: raise SystemExit("Rotate your API key — current one is invalid") The auto-retry on 429 and 5xx responses (with Retry-After honored) is the most common ergonomic win compared to writing raw requests code. It saves the typical "I forgot to handle a flaky network" production incident. Step 6: Switch to webhooks (zero polling) Polling works but it's chatty. If you have an HTTP endpoint you can expose publicly, webhook delivery is cleaner. Pass webhook_url when creating jobs: batch = client.batch( pdfs, model="invoice", webhook_url="https://my.app/webhooks/myocr", ) Then on the receiving side (Flask example): from flask import Flask, request from myocr_client import verify_webhook_signature app = Flask(__name__) SECRET = "your-shared-webhook-secret" @app.route("/webhooks/myocr", methods=["POST"]) def myocr_webhook(): body = request.get_data() # IMPORTANT: raw bytes, not request.get_json() sig = request.headers.get("X-MyOCR-Signature", "") if not verify_webhook_signature(body, sig, SECRET): return "invalid signature", 401 event = request.get_json() request_id = event["data"]["request_id"] if event["event"] == "job.completed": ... elif event["event"] == "job.failed": ... return "", 200 Two non-obvious things: (1) verify the signature on raw bytes — if you serialize the parsed JSON back to string the signature will not match because of whitespace and key order; (2) webhooks have automatic retry policy server-side (1m → 5m → 30m → 2h), so don't worry if your endpoint is briefly down. Step 7: Pick the right model for the job The SDK exposes the same six prebuilt models the REST API offers. Pick the most specific one for your document type: invoice — supplier invoices, line items, totals, VAT. What this tutorial uses. receipt — POS receipts, merchant, items, total. Good for expense reports. bank_statement — transactions, dates, debit/credit, running balance. business_card — contact, company, phones, emails, addresses. tables — generic table extraction for documents without a specialized model. text — plain OCR text, no structure. Using a specialized model is the difference between getting an unstructured table that needs cleanup and getting Vendor / Total / Line Items already parsed into typed Excel columns. Step 8: Production checklist Before shipping this to a real workload: Use sk_test_* keys in dev — they're rate-limited but don't consume your billable quota Set MYOCR_API_KEY via env var, never hardcode — the SDK reads it automatically Configure your secrets manager for the webhook signing secret on both ends (myocr server and your receiver) Log request_id on every call so you can correlate with server-side logs if you open a support ticket Set a sensible timeout on Job.wait() — the default 600s is fine for invoices, raise it for bank statements with 50+ pages Monitor QuotaExceeded in your error tracker — that's your signal to upgrade the plan before it bites Where to go next The full API reference is in Scalar UI with copy-paste examples for every endpoint. The SDK source code is open source on GitHub — read the client.py if you want to extend it, or open an issue if you hit a missing feature. If you're integrating into a SaaS that needs OCR, the SDK is also the recommended path for our Zapier, QuickBooks, and Xero integrations (all use it internally). One-screen summary # pip install myocr-client import os from myocr_client import MyOCRClient, QuotaExceeded client = MyOCRClient() # MYOCR_API_KEY env var # Sync (≤5MB, ≤10 pages): client.convert("doc.pdf", model="invoice").save("out.xlsx") # Async + batch (≤20 files/call, ≤50MB each): batch = client.batch( ["a.pdf", "b.pdf", "c.pdf"], model="invoice", webhook_url="https://my.app/webhooks/myocr", ) for job in batch.wait_all(timeout=600): if job.is_done: job.download(f"out/{job.request_id}.xlsx") Same in Node.js / TypeScript Prefer JavaScript? The official myocr-client Node SDK has the same surface, identical method names (camelCase), typed exceptions, and zero runtime dependencies (Node 18+ uses native fetch / FormData / crypto). // npm install myocr-client import { MyOCRClient } from 'myocr-client'; const client = new MyOCRClient(); // MYOCR_API_KEY from env // Sync: const r = await client.convert('invoice.pdf', { model: 'invoice' }); await r.save('invoice.xlsx'); // Async + batch: const batch = await client.batch(['a.pdf', 'b.pdf', 'c.pdf'], { model: 'invoice' }); const done = await batch.waitAll({ timeoutMs: 600_000 }); for (const job of done) { if (job.isDone) await job.download(`out/${job.requestId}.xlsx`); } Same API behind the scenes, same error codes (mapped to TS exception classes), same webhook delivery and signature verification. Pick the language that fits your stack. Try it today: get a free API key at myocr.app/account/api (no credit card), pip install myocr-client (or npm install myocr-client), and process your first batch in 5 minutes. Get your API key and SDK in 30 seconds Free tier: 100 conversions per month, all 6 prebuilt models, no credit card. pip install myocr-client and ship OCR this afternoon. Get API key (free)