Docs/Batch Processing
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

StatusErrorAction
429rate_limit_exceededWait and retry with exponential backoff
413file_too_largeCompress the image and retry
422no_receipt_foundSkip — image probably not a receipt
422scanned_pdfConvert PDF to JPG and retry
401invalid_keyAbort — check your API key
500server_errorRetry up to 3 times