Claude API Invoice Data Extraction Tool: Build Guide
Extracting structured data from invoices — vendor name, invoice number, line items, totals, due dates — used to mean OCR pipelines, regex, and a lot of manual cleanup. A Claude API invoice data extraction tool replaces most of that with a single model call: send the invoice as text or image, get back clean JSON you can drop into your accounting system or database.
This guide covers the actual implementation: how to structure the prompt, how to enforce a JSON schema so output is parseable every time, how to handle multi-page invoices and scanned images, and where this tends to break in production (and how to fix it).
Why Claude works well for invoice extraction
Invoices are semi-structured. Field names, layouts, currencies, and tax rules vary by vendor and country, which is exactly the kind of variability that breaks rigid OCR+regex pipelines. Claude handles this better because it reads the document like a human would — it understands that "Bill To", "Invoice To", and "Customer" probably mean the same field, and it can reason about which number is the subtotal versus the total after tax.
Claude also accepts images directly, so you can skip a separate OCR step for scanned or photographed invoices and send the PDF page or photo straight to the model.
Defining the extraction schema
Start by deciding exactly what fields you need. A typical invoice schema looks like this:
{
"invoice_number": "string",
"invoice_date": "YYYY-MM-DD",
"due_date": "YYYY-MM-DD or null",
"vendor_name": "string",
"vendor_tax_id": "string or null",
"currency": "ISO 4217 code",
"line_items": [
{ "description": "string", "quantity": "number", "unit_price": "number", "total": "number" }
],
"subtotal": "number",
"tax_amount": "number",
"total_amount": "number"
}
Put this schema in the prompt explicitly, and tell Claude to return null for missing fields rather than guessing or omitting keys. This single instruction eliminates most downstream parsing errors.
Building the extraction call
Here's a working example using the Messages API through SubToAPI, which gives you an HTTPS endpoint and a sub_live_ key on top of your existing Claude access:
const res = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 1024,
messages: [
{
role: "user",
content: [
{
type: "image",
source: {
type: "base64",
media_type: "image/png",
data: invoiceImageBase64
}
},
{
type: "text",
text: `Extract invoice data as JSON matching this schema exactly:
{invoice_number, invoice_date, due_date, vendor_name, vendor_tax_id,
currency, line_items: [{description, quantity, unit_price, total}],
subtotal, tax_amount, total_amount}.
Use null for missing fields. Return only the JSON object, no commentary.`
}
]
}
]
})
});
const data = await res.json();
const invoice = JSON.parse(data.content[0].text);
Full request/response details for sending images and text together are in /docs/messages, and the basics of getting a key working are in /docs/quickstart.
Enforcing structure with tool use
"Return only JSON" works most of the time, but for a production pipeline you want a hard guarantee. Define a tool with the schema as its input and force Claude to call it:
{
"name": "record_invoice",
"description": "Record structured invoice data",
"input_schema": {
"type": "object",
"properties": {
"invoice_number": { "type": "string" },
"total_amount": { "type": "number" },
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": { "type": "string" },
"quantity": { "type": "number" },
"unit_price": { "type": "number" }
}
}
}
},
"required": ["invoice_number", "total_amount"]
}
}
Set tool_choice to force the record_invoice call, and you get a validated JSON object in tool_use blocks instead of parsing free text. This removes an entire class of "model added a sentence before the JSON" bugs. See /docs/tools for the full tool use spec.
Handling multi-page and multi-invoice documents
For multi-page invoices, send each page as a separate image block in the same message — Claude can correlate line items across pages with the totals on the last page. For batches of many invoices in one PDF, split the document first (most PDF libraries can do this cheaply) and run one Claude call per invoice rather than one call for the whole batch. This keeps context focused and avoids the model mixing up fields between invoices.
Validation and confidence checks
Never trust extracted numbers blindly, especially amounts. Add a cheap sanity check after extraction:
const computedSubtotal = invoice.line_items.reduce(
(sum, item) => sum + item.quantity * item.unit_price, 0
);
if (Math.abs(computedSubtotal - invoice.subtotal) > 0.01) {
flagForReview(invoice);
}
This catches OCR misreads, currency confusion, and cases where Claude parsed a discount line as a regular item. Route flagged invoices to a human reviewer instead of silently accepting bad data.
Running this at volume
If you're processing invoices as part of a SaaS product or internal tool rather than one-off scripts, you need usage visibility and team access control — not just a working prompt. SubToAPI turns your Claude access into an application-ready API: per-application sub_live_ keys, streaming responses, usage metadata per request, and team seats so finance, engineering, and ops can each have scoped access without sharing one credential. Plans start at €9/month for solo use, with Team and Scale tiers for per-seat billing — see /pricing. A free trial is available at /signup.
FAQ
Can Claude extract data from scanned (image) invoices, not just PDFs with text?
Yes. Send the scanned page as a base64-encoded image in the message content, the same way you'd send any photo. Claude reads the image directly without a separate OCR step.
How do I guarantee the output is valid JSON every time?
Use tool use with a defined input_schema and force tool_choice to that tool. This returns structured, schema-validated data instead of relying on the model to format free text correctly.
What's the best way to handle invoices with inconsistent layouts from different vendors?
Don't hardcode layout assumptions in the prompt. Describe the fields you need semantically ("the final amount due after tax") rather than positionally, and let Claude map varying vendor terminology onto your schema.