Invoice data extraction API

If you are building invoice handling into your own product, the interface matters as much as the extraction. This page describes the shape of the API rather than its virtues: how a document is submitted, how results come back, what the JSON looks like, and how the awkward parts, retries, duplicates, partial results and rate limits, are handled. It assumes you have integrated an asynchronous API before and would rather read the contract than a feature list.

Reads digital PDFs, scans and phone photos. Exports to Excel, CSV and JSON.

Upload an invoice and see the extracted data

Compare the extracted fields and line items against your own document.

PDF, JPG, PNG, BMP, HEIC, TIFF

Upload your invoices

Asynchronous by design

Extraction is not instantaneous. A single-page native PDF is quick; a 40-page scanned consolidated bill is not. An API that blocked until every document was finished would either time out or force you to hold a connection open for an unpredictable period, so submission and retrieval are separate.

You POST a document and receive a job identifier immediately. From there you have two ways to get the result. Register a webhook and you are called when the job reaches a terminal state, which is the right choice for anything in production. Poll the job endpoint and you control the timing, which is simpler for scripts, batch runs and local development where a public callback URL is inconvenient.

If you poll, back off. A job that has been running for two seconds is unlikely to be finished by second three, and polling every 200 milliseconds mostly generates rate-limit responses. An interval starting around one second and doubling to a ceiling of ten is a sensible default.

A job is in exactly one state: queued, processing, succeeded or failed. There is no partial state to interpret. A succeeded job has a complete result document, and a failed one has an error with a reason.

The response shape

A successful result contains the document metadata, the header fields, the line items and per-field confidence. Fields that could not be read are null rather than absent or empty-string, so that "not present on the invoice" and "present but unreadable" remain distinguishable. The confidence value tells you which.

Amounts are returned as decimal strings rather than floating-point numbers. This is deliberate: binary floating point cannot represent every decimal amount exactly, and an invoice total that arrives as 1482.9999999999998 is a class of bug nobody wants in a payables integration. Parse them into whatever decimal type your language provides.

Dates are ISO 8601. The currency is a three-letter code read from the document, not inferred from the supplier country, because suppliers bill in currencies other than their own more often than integration designs assume.

An abbreviated example of the result body:

{ "id": "job_9f3c2ab41d", "status": "succeeded", "document": { "pages": 2, "source": "pdf", "filename": "apr-invoice.pdf" }, "invoice": { "supplier_name": { "value": "Example Supplier LLC", "confidence": 0.98 }, "invoice_number": { "value": "0004821", "confidence": 0.99 }, "invoice_date": { "value": "2026-04-11", "confidence": 0.97 }, "due_date": { "value": "2026-05-11", "confidence": 0.95 }, "currency": { "value": "USD", "confidence": 0.99 }, "po_number": { "value": null, "confidence": null }, "subtotal": { "value": "1235.00", "confidence": 0.99 }, "tax_total": { "value": "247.00", "confidence": 0.98 }, "total": { "value": "1482.00", "confidence": 0.99 }, "line_items": [ { "description": { "value": "Letter copier paper, 20 lb, case of 10 reams", "confidence": 0.96 }, "quantity": { "value": "20", "confidence": 0.99 }, "unit_price": { "value": "18.75", "confidence": 0.98 }, "line_total": { "value": "375.00", "confidence": 0.99 } } ] }, "validation": { "lines_sum_to_subtotal": true, "subtotal_plus_tax_equals_total": true } }

The validation block is worth reading before you trust the numbers. It reports the arithmetic checks rather than performing them for you, so your own logic can decide whether a failed check means review, rejection or a warning.

Idempotency and duplicate submissions

Networks fail after the server has done the work. Your request times out, you retry, and without protection you have now paid to extract the same invoice twice and created two records downstream.

Submissions accept an idempotency key. Any unique string you generate per logical submission, commonly a UUID or your own internal document identifier. Repeating a request with the same key returns the original job rather than creating a new one, for a bounded window after the first call.

The important discipline is generating the key before the first attempt and reusing it across every retry of that attempt. A key generated inside the retry loop is a new key each time and protects nothing. If you already have a stable document identifier in your system, use it. It is more meaningful than a random string when you are later trying to work out what happened.

Errors, rate limits and retries

Errors divide into two categories that call for opposite responses, and conflating them is the most common integration bug in this kind of API.

Client errors in the 4xx range mean the request itself is wrong: an unsupported file type, a malformed request, an invalid key, a document that is not an invoice. Retrying an identical request will produce an identical error. These belong in a dead-letter queue for inspection, not a retry loop.

Rate limiting and server-side errors are transient. A 429 carries a header telling you when to try again; honour it rather than guessing. For 5xx responses, retry with exponential backoff and jitter. Jitter matters more than people expect, because a batch of clients retrying on the same schedule reconverges into the same thundering herd that caused the problem.

A failed job is distinct from a failed request. Submission can succeed while extraction later fails because the document was encrypted, blank or corrupt. That arrives as a job in the failed state with a reason code, and it is not something a retry will fix without changing the input.

  • Generate the idempotency key before the first attempt, not inside the retry loop.
  • Do not retry 4xx errors; queue them for inspection.
  • Honour the retry-after header on 429 rather than choosing your own interval.
  • Use exponential backoff with jitter for 5xx.
  • Verify webhook signatures before acting on a payload.
  • Treat webhook delivery as at-least-once and make your handler idempotent on the job identifier.

Sandbox and going to production

A sandbox environment accepts the same requests against separate credentials and does not consume production quota. It is where you should exercise the paths that are awkward to reproduce deliberately: a job that fails, a rate-limit response, a webhook retry, a document that is not an invoice.

The recommendation worth taking seriously is to test with your own documents rather than clean samples. Every real invoice corpus contains a supplier whose layout is unusual, a scan someone photographed at an angle, and a consolidated bill with two hundred line items. Those are the documents that will determine how your integration behaves in production, and they are the ones a curated sample set never includes.

Before going live, confirm three things end to end: that your webhook endpoint is reachable from outside your network and verifies signatures, that your handler tolerates the same job identifier arriving twice, and that a failed job produces something visible to a human rather than a silent gap in your data.

Frequently asked questions

Asynchronous. You submit a document and receive a job identifier immediately, then get the result either through a webhook when the job reaches a terminal state or by polling the job endpoint. Blocking until extraction completed would be unworkable for large multi-page scans.

Webhooks for production, because they remove the polling loop entirely and deliver results as soon as they exist. Polling is simpler for scripts, batch jobs and local development where exposing a public callback URL is inconvenient. Use exponential backoff if you do.

Send an idempotency key with each submission and reuse the same key across every retry of that submission. A repeated request with a matching key returns the original job instead of creating a new one. Generate the key before the first attempt, not inside the retry loop.

To avoid floating-point representation errors. Binary floating point cannot represent every decimal value exactly, and a total that deserializes as 1482.9999999999998 is a bug you do not want in a payables integration. Parse the strings into your language's decimal type.

You receive a 429 response with a header indicating when to retry. Honour that value rather than choosing your own interval, and add jitter to your backoff so that concurrent clients do not all retry in the same instant.

Yes, with separate credentials and no effect on production quota. It is the right place to exercise failure paths that are hard to trigger on purpose, failed jobs, rate limits, webhook retries, and it is worth testing with your own awkward documents rather than clean samples.

Run your own invoices through it

Upload a few of your least tidy supplier invoices and compare the extracted fields against the documents. That tells you more than any feature list.