Skip to content
Extract an Invoice

Extract an Invoice

The invoice type pulls the parties, the dates, the itemized rows and the money out of a supplier invoice. It is the type to reach for on anything transactional: invoices, bills, receipts with a vendor and a total.

curl -X POST https://cloud.unidoc.io/api/uniai/extract/invoice \
  -H "X-API-KEY: $UNIDOC_LICENSE_API_KEY" \
  -F "[email protected]"

Fields

FieldTypeHolds
invoice_numberstringThe invoice number or code.
invoice_datedate stringWhen the invoice was issued.
due_datedate stringWhen payment is due.
sellerobjectSupplier, issuer, or “from” party.
customerobjectBuyer, recipient, or “billed to” party.
line_itemsarrayThe itemized rows.
subtotalobjectamount and currency, before tax.
taxesarrayOne entry per tax line: type, amount, currency, rate.
totalobjectamount and currency.
notesstringTerms, instructions or trailing notes.

seller, customer and the line_items entries are deliberately open. The schema does not fix their inner keys, so the model returns whatever the invoice actually carries: a name and address on one, plus vat_id, email or id_number on another. Read them defensively rather than binding them to a fixed struct.

Dates come back as strings in YYYY-MM-DD where the model can work out the format. An invoice using an ambiguous numeric format may produce something else, so parse rather than assume.

A worked response

A minimal invoice, one total and two named parties, returns only what is there:

{
  "invoice_number": "2026-014",
  "customer": {
    "name": "Acme Corp."
  },
  "seller": {
    "name": "unidoc.io"
  },
  "total": {
    "amount": 1240.00,
    "currency": "USD"
  }
}

No line_items, no taxes, no dates. Those keys are absent, not null, and line_items is one of the fields the schema marks required. This is normal output, not a failure.

A full commercial invoice fills much more in:

{
  "invoice_number": "506617438",
  "invoice_date": "2025-03-01",
  "due_date": "2025-03-01",
  "seller": {
    "name": "DigitalOcean LLC",
    "address": "105 Edgeview Drive, Suite 425\nBroomfield, CO, 80021",
    "vat_id": "136267"
  },
  "customer": {
    "name": "UniDoc",
    "email": "[email protected]",
    "address": "Bardastadir 71\nReykjavik, 112\nICELAND"
  },
  "line_items": [
    {
      "category": "Product Usage Charges",
      "description": "Droplets",
      "quantity": 672,
      "amount": 108.00
    }
  ],
  "subtotal": { "amount": 284.60, "currency": "USD" },
  "taxes": [
    { "type": "VAT Iceland", "amount": 68.30, "currency": "USD", "rate": "24.00%" }
  ],
  "total": { "amount": 352.90, "currency": "USD" },
  "notes": "Final invoice for the February 2025 billing period"
}

Note rate is a string with the percent sign attached, while amount is a number.

Reading it safely

Every field can be absent, so index defensively. In Go, decoding into a struct of pointers makes the difference between “zero” and “not found” visible:

type Money struct {
    Amount   *float64 `json:"amount"`
    Currency *string  `json:"currency"`
}

type Invoice struct {
    InvoiceNumber *string          `json:"invoice_number"`
    Total         *Money           `json:"total"`
    LineItems     []map[string]any `json:"line_items"`
}

A total.amount of 0 on a real invoice is a red flag worth surfacing; a nil Total means the model did not find one at all. Those are different problems and you want to tell them apart.

Limitations

Three pages maximum. A longer invoice is rejected outright rather than truncated, so split it and send the pages carrying the header and the totals.

Arithmetic is not checked. Nothing verifies that the line items sum to the subtotal or that subtotal plus tax equals the total. If those need to agree, check them yourself; a mismatch is a good signal that the extraction went wrong somewhere.

Multi-currency invoices put a currency on each money object independently, and there is no guarantee they agree.

A scanned invoice needs is_scanned=true or it will come back nearly empty. See scanned documents.

Last updated on