Guides
Batch Processing
Process folders of receipts efficiently using parallel requests, rate limit handling, and automatic retry logic.
Rate limits by plan: Free: 2 req/min · Pro: 30 req/min · Pro Plus: 120 req/min. See rate limits.
Node.js — parallel batch
Process a folder of receipts with controlled concurrency and CSV output:
import { readFileSync, writeFileSync } from "node:fs";
import { readdir } from "node:fs/promises";
import path from "node:path";
const API_KEY = process.env.RECEIPTCONVERTER_API_KEY;
const CONCURRENCY = 5; // adjust to your plan's rate limit
const RETRY_AFTER = 2000; // ms to wait on 429
async function parseOne(filePath, retries = 3) {
const bytes = readFileSync(filePath);
const file = new File([bytes], path.basename(filePath));
const form = new FormData();
form.append("file", file);
const res = await fetch("https://receiptconverter.com/api/v1/convert", {
method: "POST",
headers: { Authorization: `Bearer ${API_KEY}` },
body: form,
signal: AbortSignal.timeout(30_000),
});
if (res.status === 429 && retries > 0) {
await new Promise((r) => setTimeout(r, RETRY_AFTER));
return parseOne(filePath, retries - 1);
}
if (!res.ok) return { error: `HTTP ${res.status}` };
return res.json();
}
// Process in chunks
async function processFolder(folder) {
const files = (await readdir(folder)).filter((f) => /.(jpg|jpeg|png|pdf)$/i.test(f));
const results = [];
for (let i = 0; i < files.length; i += CONCURRENCY) {
const chunk = files.slice(i, i + CONCURRENCY);
const settled = await Promise.allSettled(
chunk.map((f) => parseOne(path.join(folder, f)))
);
for (let j = 0; j < chunk.length; j++) {
const s = settled[j];
results.push({
file: chunk[j],
success: s.status === "fulfilled" && s.value?.success,
data: s.status === "fulfilled" ? s.value?.data : null,
error: s.status === "rejected" ? s.reason?.message : null,
});
}
console.log(`Processed ${Math.min(i + CONCURRENCY, files.length)}/${files.length}`);
}
return results;
}
// Export to CSV
function toCSV(results) {
const header = "file,vendor,date,total,currency,category,error";
const rows = results.map(({ file, data, error }) =>
[`"${file}"`, data?.vendor ?? "", data?.date ?? "", data?.total ?? "", data?.currency ?? "", data?.category ?? "", error ?? ""].join(",")
);
return [header, ...rows].join("\n");
}
const results = await processFolder("./receipts");
writeFileSync("receipts_output.csv", toCSV(results));
console.log(`Done. ${results.filter((r) => r.success).length}/${results.length} succeeded.`);Python — batch with progress bar
import os, time, csv, requests
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
API_KEY = os.environ["RECEIPTCONVERTER_API_KEY"]
CONCURRENCY = 5
def parse_one(path: Path, retries: int = 3) -> tuple[str, dict | None]:
for attempt in range(retries):
try:
with open(path, "rb") as f:
r = requests.post(
"https://receiptconverter.com/api/v1/convert",
headers={"Authorization": f"Bearer {API_KEY}"},
files={"file": (path.name, f)},
timeout=30,
)
if r.status_code == 429:
time.sleep(2 ** attempt)
continue
r.raise_for_status()
return str(path), r.json()
except Exception:
if attempt == retries - 1:
return str(path), None
return str(path), None
receipts = list(Path("./receipts").glob("*.jpg")) + list(Path("./receipts").glob("*.png"))
rows = []
with ThreadPoolExecutor(max_workers=CONCURRENCY) as pool:
futures = {pool.submit(parse_one, p): p for p in receipts}
for i, future in enumerate(as_completed(futures), 1):
file_path, result = future.result()
data = result.get("data") if result else {}
rows.append({
"file": file_path,
"vendor": data.get("vendor", ""),
"date": data.get("date", ""),
"total": data.get("total", ""),
"currency": data.get("currency", ""),
"category": data.get("category", ""),
})
print(f"{i}/{len(receipts)}: {data.get('vendor', 'error')}")
with open("receipts_output.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["file","vendor","date","total","currency","category"])
writer.writeheader()
writer.writerows(rows)
print(f"Saved to receipts_output.csv")Error patterns to handle
| Status | Error | Action |
|---|---|---|
| 429 | rate_limit_exceeded | Wait and retry with exponential backoff |
| 413 | file_too_large | Compress the image and retry |
| 422 | no_receipt_found | Skip — image probably not a receipt |
| 422 | scanned_pdf | Convert PDF to JPG and retry |
| 401 | invalid_key | Abort — check your API key |
| 500 | server_error | Retry up to 3 times |
Next steps: MCP / AI Agents guide · Python guide · JavaScript guide