Extract data from a scanned invoice as JSON

Send a scanned invoice PDF or phone photo to one API call and get typed JSON back: totals, dates and line items, with a check you can run yourself.

6 min read

A scanned invoice has no text layer. A PDF from a flatbed scanner, a fax gateway or a phone camera is a stack of pictures, so copying text out of it returns nothing, and a PDF text extractor returns an empty string. To extract data from a scanned invoice you need something that reads the picture and then puts the result in the shape your code expects.

This guide shows that as one API call: a scan in, a typed JSON object out.

Why scans break the usual tools

Three things go wrong with scanned invoices, and they are different problems.

  • No text to read. A text extractor has nothing to work with. You need a reader that works on page images.
  • Layout noise. Skew, shadows, a stamp over the total, a fold across a line item. The text can be found, but the field it belongs to is ambiguous.
  • Untyped output. Even good OCR gives you characters. 1.234,56 is a string until your code decides it is 1234.56.

Veralens reads PDFs page by page as images, so a scan and a digital PDF take the same path. You do not tell it which one you have. Photos go in as image sources instead, and any raster format works (PNG, JPEG, WebP, HEIC and others). Source: API reference.

For when plain OCR is enough and when it is not, read OCR vs LLM extraction.

Describe the output first

Start from what your system stores, not from what the invoice looks like. A JSON Schema is the contract:

json
{
  "type": "object",
  "properties": {
    "supplier": { "type": "string", "description": "Name of the company that issued the invoice." },
    "invoice_number": { "type": "string" },
    "issue_date": { "type": "string", "format": "date" },
    "currency": { "type": "string", "format": "currency" },
    "subtotal": { "type": "number" },
    "tax": { "type": "number" },
    "total": { "type": "number", "description": "Amount due, tax included." }
  },
  "required": ["supplier", "total", "currency"]
}

Keep required short. A field that is not on the page comes back null instead of guessed, and a smaller schema means fewer output tokens, which is the expensive side of the bill (pricing). For the full field-by-field design, see extract invoice data as typed JSON.

Handling the bad scan

Some scans are worse than others. Three habits help.

  1. Send the original file. Do not re-save a scan as a smaller JPEG first. Limits are 20 MB per file and 200 pages per PDF, so most scans fit as they are.
  2. Use global_prompt for known quirks. If your supplier prints the total in a box labelled "Amount payable", say so. It applies to the whole extraction.
  3. Check, then route. Never write an extracted invoice to your books without a test. The next section gives one.

Check the numbers

Extraction output is data you do not control, so treat it like user input. For an invoice, arithmetic is the cheapest validation you have:

ts
const inv = JSON.parse(job.output)
const expected = Math.round((inv.subtotal + inv.tax) * 100)
const actual = Math.round(inv.total * 100)
const ok = expected === actual
// ok === false: send the scan to a human instead of the ledger

If the check fails, the scan goes to a review queue with the original attached. If it passes, it still is not proof that every field is right, but it catches the failure that costs the most: a wrong total. Veralens also runs checks of its own on numbers and identifiers, described in extraction that verifies.

Cost and failure

Billing is prepaid and per token. Creating a job holds an estimate on your balance, the hold settles to the real token cost when the job completes, and a failed job refunds the hold in full. Each job reports its own usage.cost, so you can read what a scan cost from the response and measure it on your own invoices instead of trusting an average. There is no free balance, so top up (from $5) before the first job.

A working example

This request sends a scanned PDF and registers a webhook, so your service is called when the result is ready. Replace the data URL with your own base64 file and vl_your_secret_key with a key from the dashboard.

bash
curl -X POST https://veralens.ai/api/v1/jobs/create \
  -H "Authorization: Bearer vl_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "sources": [{ "type": "document", "data": "data:application/pdf;base64,JVBERi0..." }],
    "schema": {
      "type": "object",
      "properties": {
        "supplier": { "type": "string" },
        "invoice_number": { "type": "string" },
        "issue_date": { "type": "string", "format": "date" },
        "currency": { "type": "string", "format": "currency" },
        "subtotal": { "type": "number" },
        "tax": { "type": "number" },
        "total": { "type": "number" }
      },
      "required": ["supplier", "total", "currency"]
    },
    "webhook_url": "https://yourapp.com/hooks/veralens"
  }'

You get 201 with the job in pending. When it finishes, the webhook receives { "status": "success", "job_id": "...", "data": { ... } }, or { "status": "error", "job_id": "..." } if the job failed. Without a webhook, poll GET https://veralens.ai/api/v1/jobs/:jobId/get every 2 to 3 seconds. Both paths are in Jobs and queue.

Next steps: create an API key, read the API reference, then run the request on one of your own scans.

Try it on your own documents

Add funds from $5, create a key, and send your first document. Every job reports exactly what it cost.

Add $5, get an API key

Keep reading