An invoice JSON schema: the fields you actually need

Anurag Jha ·

Before you can render an invoice you have to decide what one is — as data. Most teams start with whatever their database happens to hold, discover six months later that they cannot produce a compliant tax invoice from it, and retrofit the missing fields onto live records.

Here is the shape worth starting from, and why each part is there.

The minimum

{
  "invoice_number": "INV-2026-0001",
  "issue_date": "2026-08-30",
  "currency": "USD",
  "seller": { "name": "Acme Inc." },
  "buyer":  { "name": "Northstar LLC" },
  "line_items": [
    { "name": "API integration", "quantity": "1", "unit_price": "1200.00" }
  ]
}

Six fields. Everything else is optional until a tax authority says otherwise.

Money must be strings

This is the decision that matters most, and it looks like a style preference.

"unit_price": "1200.00"     // correct
"unit_price": 1200.00       // a float — do not

JSON numbers are IEEE-754 doubles. 0.1 + 0.2 is 0.30000000000000004, and that error compounds across line items, tax rates and rounding. Once money has been through a float it can be off by a cent, and a cent on an invoice is a support ticket.

Strings survive the transport exactly as written, and the receiving end parses them into a decimal type. Every payment API worth using does this — Stripe sends integer minor units, which is the same instinct.

Dates are YYYY-MM-DD. Currency is ISO 4217. Neither is negotiable if anything downstream is automated.

What a tax invoice needs beyond the minimum

If the document has to satisfy VAT or GST rules, the seller and buyer grow:

"seller": {
  "name": "Acme Inc.",
  "tax_id": "27AAAAA0000A1Z5",
  "address": {
    "line1": "12 Industrial Estate",
    "city": "Pune",
    "state": "Maharashtra",
    "postal_code": "411001",
    "country": "IN"
  }
}

And each line carries its own tax, because rates differ per item:

{
  "name": "Movable Alphabet Kit",
  "quantity": "5",
  "unit_price": "1299.00",
  "taxes": [{ "name": "GST", "rate": "12" }]
}

Per-line tax is not over-engineering. A single invoice routinely mixes rates — books at one rate, stationery at another — and a tax invoice is generally required to show the breakup rate by rate, not one combined figure.

Discounts belong in the data, not the price

It is tempting to discount by lowering unit_price. Do not: the discount then does not exist anywhere, and the invoice cannot show what it gave away.

{
  "name": "Notebook",
  "quantity": "40",
  "unit_price": "499.00",
  "discount": { "type": "percentage", "value": "10" }
}

Model it, and the document can print the list price, the discount and the net — which is what a customer expects to see, and in several jurisdictions what the rules require in order for the discount to reduce the taxable value at all.

Validate the payload before you rely on it

A schema is only useful if something enforces it. You can check a payload without rendering anything:

curl -X POST https://invoicepdfs.com/api/v1/documents/validate \
  -H "Authorization: Bearer inv_..." \
  -H "Content-Type: application/json" \
  -d '{"document_type":"invoice","data":{ ... }}'
{ "data": { "valid": true } }

Cheap enough to run on every write, which is where you want to find a bad quantity — not at render time in front of a customer.


The full field list is in the API reference, and the Invoice PDF API turns a payload in this shape into a finished PDF in one call.