LOGIN  login to app
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

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:

Total: ~30 lines of business code, plus a webhook receiver if you want push-based delivery.

Prerequisites

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:

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:

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)