Batch invoice processing API: webhooks and limits
Process thousands of invoices without blocking a request. Async jobs, signed webhooks, rate limits and retries, with the exact numbers from the docs.
5 min readProcessing one invoice is a request. Processing a month of them is a system. A batch invoice processing API has to survive three things a single call hides: slow documents, rate limits and duplicate deliveries.
This guide covers the design that holds up, using the actual limits from the Veralens docs, so your numbers are not guesses.
Do not hold a connection open
The first mistake in batch work is a loop that sends an invoice and waits. A long document then blocks the loop, and on a serverless host it can hit the platform timeout before the answer arrives. The failure mode is described in your function times out before the document finishes.
Veralens avoids it by design. Every extraction is a job. POST /jobs/create returns 201 with the job already in pending, and a job always ends in completed or failed. Nothing waits on an open connection. Source: Jobs and queue.
So the shape of a batch is two phases: create jobs, then receive results.
Phase 1: create within the limits
Rate limits apply per API key, with a short and a long window, and you must stay under both:
Source: API reference. A producer that sends 60 invoices a minute is at the ceiling. For a bigger backlog, throttle on your side and honor the Retry-After header when you do get 429 rate_limit_exceeded.
Each file may be up to 20 MB and each PDF up to 200 pages. A bigger file returns job_document_too_large or job_document_too_many_pages as a 400 before anything is held, so split it first.
Phase 2: receive with webhooks, not polling
Polling a batch uses the scarcest limit. GET /jobs/:jobId/get allows 30 requests a minute per key, so 100 pending jobs cannot be polled one by one. The docs say it plainly: use webhooks and do not poll.
Pass webhook_url when creating each job. Webhooks need API-key auth. The delivery is one of:
An error delivery carries no reason; the job ended failed and its hold was refunded.
Make the handler safe
Three rules, all from the delivery behavior in the docs:
- Verify the signature.
x-veralens-signatureist=<unix seconds>,v1=<hex>, wherev1is the HMAC SHA-256 of<t>.<raw body>, keyed with the secret of the API key that created the job. Compute it over the raw body before you parse, compare in constant time, and refuse an oldt. - Acknowledge fast. Reply
2xxwithin 10 seconds. Redirects are not followed and count as a failure. Queue the work and return. - Be idempotent on
job_id. After a failed delivery Veralens retries up to 6 more times, waiting 20, 40, 80, 160, 320 and 320 seconds. A retry can deliver the same event twice.
Keep a ledger
For a batch, store three things per invoice: your own reference, the job_id, and the state (created, completed, failed). Then "what is missing" is a query, not a mystery. A job that stays pending after a long time is worth fetching once with GET /jobs/:jobId/get, or finding with GET /jobs/list filtered by status.
Failed jobs refund in full, and jobs are retried before they are marked failed. If one invoice keeps failing, send a smaller file or ask for fewer fields. Before sending the whole batch, run twenty invoices and total usage.cost from the responses; that measured number beats any estimate for planning your balance (pricing).
A working example
One invoice, created with a webhook. Run it for each file in your backlog, within the limits above.
For the schema side, see invoice PDF to JSON. To start, create an API key and read the API reference.