nvNeuralVerge

Invoice and Receipt Extraction: From PDF to Accounting-Ready JSON

An invoice extraction api turns a PDF invoice into typed JSON you can check. A real AI Extract run on a sample invoice, the gap the checks caught, and the cost.

By the NeuralVerge team · 12 min read · Updated October 2026

▸On this page

An invoice extraction api reads a supplier invoice or a receipt and returns its contents as typed JSON: who issued it, what it is for, how much, and in what currency. The hard part is not getting numbers out of a PDF. It is getting numbers out that you can check before they reach a ledger. This walkthrough shows one real call, the validation around it, and the cost.

The short version

  • —What it is: NeuralVerge's AI Extract takes the URL of an invoice and returns the fields you ask for, under names and types you choose: a candidate record, not a posted entry.
  • —The real run: a public sample invoice PDF, read correctly. Our checks then found that the PDF disagrees with itself: its line totals add up to 36.16 and its printed subtotal is 36.00.
  • —Who it is for: finance and operations developers who want typed output they can validate. For documents in general, see turning PDFs into typed JSON.
  • —What it costs: a flat 5 points per call, including a call whose URL did not load.
  • —Reach it over: REST or the hosted MCP server, as the run_extract tool. The captured run was made through MCP.

The workflow, step by step

Most invoices carry a supplier, a number, a date, line items, tax and a total, but no two suppliers lay them out alike. The workflow below turns that into a record your code can check.

1. Get the invoice to an address

The request takes a url, and no file-upload route is documented. If invoices arrive as mail attachments or sit in private storage, intake is the first problem to solve; confirm what is supported in the documentation. Check that the link resolves before you spend a call on it.

2. Describe the fields once

Send plain-language instructions and, for a pipeline, a JSON Schema as a string in settings.extract_schema_json. Name fields for the job (total_due beats total), keep amounts as plain numbers, and allow null for anything a document may not carry. The real run below used this outline: invoice number and issue date (string or null), seller and buyer names, currency, an array of line items with description, quantity, unit price and line total, and the subtotal, tax and total due as numbers or null. Add fields your process needs, such as a tax rate, a purchase-order number or a due date, as long as each has a type and a rule for being empty.

3. Extract

One call returns one JSON object for one document, under the machine key. Store the result together with the URL and the schema version as provenance.

4. Validate with your own rules

Validation is what separates an extraction experiment from something you let near a ledger. "Accounting-ready" here means a typed schema plus a validation pass you run yourself: line items add up to the subtotal, subtotal plus tax equals the total, required fields are present. It does not mean the values are guaranteed correct. As the run below shows, a record can be perfectly extracted and still describe a document that does not add up.

5. Route and store

Send clean records to approval and everything else to a clerk with the original attached.

Receipts

A receipt is the same job with different fields: a merchant, a date, a total, often a tax line, and fewer or no line items. Use a separate, smaller schema with every field nullable, since many receipts omit things an invoice carries, such as a tax ID. Many receipts are scans or photos, which this article does not cover; test your own.

Versus OCR, templates and e-invoices

OCR converts the picture of a page into characters; extraction by meaning asks for "the total due" and finds it wherever it is. Whether an OCR step happens inside is not described on the product pages, so this article does not claim it. Template parsers are built per supplier and can be reliable for a fixed set of senders; instructions plus a schema replace "draw the zones" with "describe the fields once", at the price of more validation. If a supplier sends machine-readable invoice data, read that directly.

A real run on a sample invoice

On 29 October 2026 we ran AI Extract against a public sample invoice published by the vendor of a PDF tool, a text-based PDF at https://www.princexml.com/samples/invoice-colorful/invoicesample.pdf. The seller and items are the sample vendor's fictional names. Nothing here is a real transaction. The call was made through the MCP tool run_extract, with these arguments:

{
  "url": "https://www.princexml.com/samples/invoice-colorful/invoicesample.pdf",
  "instructions": "Extract the invoice: invoice number, issue date, seller and buyer names, each line item with description, quantity, unit price and line total, and the subtotal, tax and total due. Use null for anything not in the document.",
  "settings": {
    "extract_schema_json": "{\"type\":\"object\",\"properties\":{\"invoice_number\":{\"type\":[\"string\",\"null\"]},\"issue_date\":{\"type\":[\"string\",\"null\"]},\"seller_name\":{\"type\":[\"string\",\"null\"]},\"buyer_name\":{\"type\":[\"string\",\"null\"]},\"currency\":{\"type\":[\"string\",\"null\"]},\"line_items\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"description\":{\"type\":\"string\"},\"quantity\":{\"type\":[\"number\",\"null\"]},\"unit_price\":{\"type\":[\"number\",\"null\"]},\"line_total\":{\"type\":[\"number\",\"null\"]}}}},\"subtotal\":{\"type\":[\"number\",\"null\"]},\"tax\":{\"type\":[\"number\",\"null\"]},\"total_due\":{\"type\":[\"number\",\"null\"]}}}"
  }
}

The response, with the human field shortened (marked [...]) and the echoed schema left out of settings:

{
  "session_id": "922f4574-fe3b-4da6-9db1-d65a87178289",
  "kind": "extract",
  "url": "https://www.princexml.com/samples/invoice-colorful/invoicesample.pdf",
  "used_url": "https://www.princexml.com/samples/invoice-colorful/invoicesample.pdf",
  "instructions": "Extract the invoice: invoice number, issue date, seller and buyer names, each line item with description, quantity, unit price and line total, and the subtotal, tax and total due. Use null for anything not in the document.",
  "settings": { "has_schema": true, "extract_schema_json": "[the schema above, expanded]" },
  "human": "# Sunny Farm Invoice Sample\n\n## Seller Information\n\n**Denny Gunawan**\n\n[...]\n\n## Line Items\n\n| Description | Quantity (kg) | Unit Price | Line Total |\n|---|---|---|---|\n| Apple | 1 | $5.00 | $5.00 |\n[...]\n\n## Summary\n\n**Subtotal:** $36.00\n\n**GST (10%):** $3.60\n\n**Total Due:** $39.60",
  "machine": {
    "invoice_number": "20130304",
    "issue_date": null,
    "seller_name": "Denny Gunawan",
    "buyer_name": null,
    "currency": "$",
    "line_items": [
      { "description": "Apple", "quantity": 1, "unit_price": 5, "line_total": 5 },
      { "description": "Orange", "quantity": 2, "unit_price": 1.99, "line_total": 3.98 },
      { "description": "Watermelon", "quantity": 3, "unit_price": 1.69, "line_total": 5.07 },
      { "description": "Mango", "quantity": 2, "unit_price": 9.56, "line_total": 19.12 },
      { "description": "Peach", "quantity": 1, "unit_price": 2.99, "line_total": 2.99 }
    ],
    "subtotal": 36,
    "tax": 3.6,
    "total_due": 39.6
  },
  "total_points": 5
}

Reading the envelope:

  • —machine holds the extracted fields, shaped by your instructions and schema. This is the object your code should consume.
  • —human is a readable rendering of the document, useful for a reviewer to see what the extractor saw.
  • —url and used_url are the address that was read; session_id identifies the call; kind names the capability.
  • —total_points is what the call cost: 5.

The schema bought typed numbers: 5 rather than "$5.00", ready for arithmetic. The currency came back as the symbol $, not an ISO code, because the document shows a symbol; ask for a code in the instructions or map it yourself.

What the validation caught

The rules are yours, not the service's. This response contains four lessons.

Line totals versus subtotal: a gap that is in the PDF

Add up the five line_total values: 5 + 3.98 + 5.07 + 19.12 + 2.99 = 36.16. The extracted subtotal is 36. The two disagree by 0.16.

This is not an extraction error. We checked the PDF's own text: it prints the five lines exactly as extracted and prints "Subtotal $36.00" beneath them. The source document is internally inconsistent, and the extractor reported what the page says. Only a rule in your own code notices that. In a real accounts-payable flow this invoice would go to a person: the question is "which number does the supplier mean", not "did the extraction fail".

Subtotal plus tax equals total due

The second rule passes: 36.00 + 3.60 = 39.60, which matches total_due. The total is consistent with the printed subtotal, not with the line items, so validating only this rule would let the 0.16 gap through. Run both rules.

Null for fields the document does not carry

issue_date and buyer_name came back null. The PDF has no issue date, and it shows a buyer address (221 Queen St Melbourne VIC 3000) but no buyer name. The model did not invent either. The instruction "Use null for anything not in the document" plus nullable schema types gave it room to behave that way. Your validation still has to decide what a null means: an invoice without an issue date usually goes to a person. This was one document, so it shows the behaviour can be right, not that it always is.

Tax shown as 3.6 on a stated 10% GST

The tax value is 3.6, and the PDF states GST at 10%. The implied rate, 3.6 divided by 36, is 10%. The stated rate appears only in human, because the schema had no field for it; add a tax_rate field if you need to compare it.

A validation sketch for exactly this object

A Python sketch for the machine object above: an illustration, not a library, and the tolerance is something to choose with your accountants.

def validate_invoice(rec: dict) -> dict:
    errors, notes = [], []

    for key in ("invoice_number", "issue_date", "seller_name", "buyer_name"):
        if rec.get(key) in (None, ""):
            notes.append(f"{key} is null (not in the document, or not read)")

    items = rec.get("line_items") or []

    for n, i in enumerate(items, 1):
        q, p, t = i.get("quantity"), i.get("unit_price"), i.get("line_total")
        if None not in (q, p, t) and abs(q * p - t) > 0.005:
            errors.append(f"line {n}: quantity x unit price != line total")

    line_sum = round(sum(i.get("line_total") or 0 for i in items), 2)
    subtotal = rec.get("subtotal")
    if subtotal is not None and abs(line_sum - subtotal) > 0.005:
        errors.append(f"line totals sum to {line_sum:.2f}, subtotal is {subtotal:.2f}")

    tax, total = rec.get("tax"), rec.get("total_due")
    if None not in (subtotal, tax, total) and abs(subtotal + tax - total) > 0.005:
        errors.append("subtotal plus tax does not equal total due")

    rate = round(100 * tax / subtotal, 2) if subtotal and tax is not None else None
    return {"errors": errors, "notes": notes, "implied_tax_rate_pct": rate}

We ran it on the machine object from the real response. The result:

  • —errors: one entry, "line totals sum to 36.16, subtotal is 36.00".
  • —notes: issue_date and buyer_name are null.
  • —implied_tax_rate_pct: 10.0.

The quantity-times-price check passed on all five lines, and subtotal plus tax equalled the total due, so the only error is the 0.16 gap from the document: one precise finding, handed to a person with the original attached. An empty error list means the record is internally consistent, not that it matches the paper. Add checks from your own data: known supplier, duplicate invoice number, expected amount range.

REST and MCP

The same capability is reachable both ways. Over REST the request has the shape the product page documents for the extract endpoint: a POST to https://api.neuralverge.ai/functions/v1/run-extract with a bearer key and a JSON body holding url, instructions and settings, with the schema as a string in settings.extract_schema_json, as in the arguments above. We made the captured call through MCP, and the REST product page documents only a company-profile example, so no invoice REST response is shown here.

Over MCP the hosted server exposes the same capability as the run_extract tool, with the same inputs and outputs and the same price. For connecting a client, see the MCP setup guide. An agent that finds an invoice link in a ticket can request the typed fields and then apply your validation rules: let the model read, and let deterministic code decide whether the arithmetic holds. As an illustration only, imagine the finance team at Acme Oy, a made-up Finnish company, feeding an approval queue from billing-portal links, one job per invoice: check the link loads, extract, validate, route.

Cost and limits

AI Extract is a flat 5 points per call. The product page says this applies regardless of page size, and the pricing page lists it in the action costs. The real call above reported total_points: 5. Four consequences follow for invoice work.

  • —A one-page receipt and a ten-page invoice cost the same per call.
  • —You can budget from counts. Invoice count multiplied by 5 points, plus retries. Check the pricing page for what a point is worth in money.
  • —Validation is free. The checks run in your code. A re-extraction is another call, so get the schema right on a sample first.
  • —A URL that did not load still cost 5 points. A second call on a nearby address that returns HTTP 404 gave machine: null and human: "null", and reported total_points: 5 all the same. Count null results as billed, and check that URLs resolve before a batch run.

That is a single observation, so confirm against your own usage. Agents pay the same: the MCP tool is priced as the equivalent REST call.

An invoice extraction api is a reader, and readers make mistakes. Input is a URL, with no file-upload route documented. Scans, photos and handwriting are untested here, and the pages say nothing about structured e-invoice formats. It reads; it does not know: account coding, tax treatment and payment approval are your system's decisions, and this is not accounting or tax advice. One call returns one object for one document, so test long and multi-invoice files on purpose. One run is not a benchmark: there are no measured accuracy or speed numbers to quote, so measure error rates by field on your own documents. Invoices hold personal and financial data, so decide where documents are stored and who can fetch them.

How to decide

Run these checks on a small real batch before you commit:

  • —Can your invoices be reached by URL, or does intake need work first?
  • —Do fifty real invoices pass both arithmetic checks? Read every failure, including those where the document is at fault.
  • —Does a receipt with no tax ID come back empty, or guessed?
  • —Do your ten most awkward suppliers and worst scans behave acceptably?
  • —Does document count times 5 points, plus retries and null results, fit your budget?
  • —Which invoices always go to a person, whatever the validation says?

It fits accounts-payable intake, expense and receipt capture, agent workflows where a person approves, and checking a backlog before an audit.

Frequently asked questions

Can I upload an invoice file directly?

The AI Extract request takes a URL, so the invoice has to be reachable at an address the service can fetch. The product description says to point extraction at a public page or PDF and does not describe a file-upload route. Invoices usually live in private mail or storage, so check the documentation for what is supported before you design the intake, and do not publish sensitive documents at open addresses to work around it.

What does "accounting-ready" mean here?

It means the output has a fixed, typed shape that your accounting system or ledger code can consume, and that you have checked it with rules, for example that line items add up to the subtotal and that subtotal plus tax equals the total. It is not a guarantee that every value is correct, and it is not a substitute for review of high-value documents.

How much does it cost to extract one invoice?

AI Extract is a flat 5 points per call. In the real run shown in this article, a call that read a sample invoice cost 5 points, and a call on a URL that returned a 404 came back null and also cost 5 points. Budget from invoice count times 5 points plus retries, check URLs before a batch, and check the pricing page for the current point value in money.

Does it work on scanned receipts and photos?

The product pages do not make claims about scanned images, photos or handwriting, and the sample run used a text-based PDF, so this article does not either. Test a set of your own receipts, including poor ones, and check that an unreadable field comes back empty rather than filled with a plausible guess.

Can I trust the totals it returns?

Treat them as a claim to verify. In the sample run, the extracted values matched what the PDF printed, and the printed subtotal still did not equal the sum of the printed line totals. Recompute the totals from the line items in your own code, compare them with the extracted subtotal and total, and send any mismatch to a person instead of posting it to the ledger.

Can an AI agent do this as part of a workflow?

Yes. The same capability is exposed on the hosted MCP server as the run_extract tool, with the same inputs and outputs as the REST endpoint and the same price. The sample run in this article was made through that tool. An agent that receives an invoice link can request typed fields and then apply your validation rules.

About NeuralVerge

Give your agents structured, cited, real-world data from 300+ sources and the open web — through one API or MCP server.

AI Extraction on the NeuralVerge blog.