Invoice schema
Fields, formats and validation rules for an invoice payload.
The contract every surface validates against. Useful when writing a payload by hand, debugging a rejected automated call, or working out what an error message means.
Conceptual background: an invoice is data.
Top level
{
meta: InvoiceMeta,
issuer: IssuerSnapshot,
client: ClientSnapshot,
vat: InvoiceVat,
payment: Payment,
items: InvoiceItem[], // at least 1
totals: Totals,
notes?: string, // max 2000
customization?: InvoiceCustomization,
}meta
Prop
Type
Rules: credit_note requires correctedInvoiceNumber; dueDate >= issueDate; duzp may be before
or after issueDate.
issuer
Frozen at issue — see snapshots.
Prop
Type
Address = {
street: string, // 1–200
city: string, // 1–100
zip: string, // /^\d{3} ?\d{2}$/
country: 'CZ',
}
BankAccount = {
accountNumber: string, // /^(?:\d{1,6}-)?\d{1,10}\/\d{4}$/
iban: string, // /^CZ\d{22}$/
bic?: string, // 8 or 11 chars
}client
Prop
Type
ClientAddress is looser than the issuer's: country is any ISO 3166-1 alpha-2 code and zip
accepts non-Czech formats, so cross-border invoicing fits.
vat
Prop
Type
Cross-field rules:
reverse_charge→ every item'svatRatemust be0, andlocalReverseChargeCodeis required.osswithsuppliesAbroad: 'none'is invalid — OSS is cross-border by definition.issuer.vatPayer === false→ mode must beregularand everyvatRatemust be0.
localReverseChargeCode is a číselník code such as 4, not a paragraph
reference. 92e here produces an ISDOC importers reject. The §-reference
belongs in legalNote.
payment
Prop
Type
bankAccount defaults from the issuer at issue time, but may be overridden — an issuer with several
accounts can invoice from a different one per invoice.
Only transfer with a bank account produces a payment QR.
items
At least one required.
Prop
Type
Per-line invariants, recomputed server-side:
lineSubtotal === round2(quantity * unitPriceWithoutVat);
lineVat === round2((lineSubtotal * vatRate) / 100);
lineTotal === round2(lineSubtotal + lineVat);totals
Totals = {
subtotal: number,
vatBreakdown: { rate: number, base: number, vat: number }[],
vatTotal: number,
total: number,
}Invariants:
subtotal === sum(items.lineSubtotal)- one
vatBreakdownentry per distinct rate, withbasethe sum of that rate's subtotals vatTotal === sum(vatBreakdown.vat)total === subtotal + vatTotal
vatBreakdown is what the PDF prints as Rekapitulace DPH.
You must supply totals — they are not optional — but the server recomputes and compares them. A mismatch aborts the issue. Treat the values you send as a checksum, not as the source of truth.
customization
{
accentColor: 'neutral' | 'blue' | 'green' | 'amber' | 'rose' | 'violet', // default 'neutral'
showStamp: boolean, // default false
showSignature: boolean, // default false
}Deliberately narrow. There is one PDF template, not a template editor.
Complete example
{
"meta": {
"docType": "invoice",
"number": "20260001",
"issueDate": "2026-05-03",
"dueDate": "2026-05-17",
"duzp": "2026-05-03",
"language": "cs",
"currency": "CZK"
},
"issuer": {
"id": "ca8b8d4e-2e7e-4f6a-9b7d-1f9c1234abcd",
"name": "Filip Ditrich",
"ico": "12345678",
"dic": "CZ12345678",
"address": {
"street": "Na Příkopě 14",
"city": "Praha",
"zip": "110 00",
"country": "CZ"
},
"bank": {
"accountNumber": "1920014539/0800",
"iban": "CZ6508000000192000145399",
"bic": "GIBACZPX"
},
"vatPayer": true,
"contactEmail": "faktury@example.cz"
},
"client": {
"id": "6f1d2b90-3c44-4a1e-8f0b-9d2e5c7a1b23",
"name": "NFCtron s.r.o.",
"ico": "27074358",
"dic": "CZ27074358",
"address": {
"street": "Budějovická 778/3a",
"city": "Praha 4",
"zip": "140 00",
"country": "CZ"
},
"contactEmail": "fakturace@nfctron.com"
},
"vat": { "mode": "regular", "suppliesAbroad": "none" },
"payment": {
"method": "transfer",
"bankAccount": {
"accountNumber": "1920014539/0800",
"iban": "CZ6508000000192000145399",
"bic": "GIBACZPX"
},
"variableSymbol": "20260001"
},
"items": [
{
"position": 1,
"description": "Konzultace a vývoj",
"quantity": 40,
"unit": "h",
"unitPriceWithoutVat": 1800,
"vatRate": 21,
"lineSubtotal": 72000,
"lineVat": 15120,
"lineTotal": 87120
}
],
"totals": {
"subtotal": 72000,
"vatBreakdown": [{ "rate": 21, "base": 72000, "vat": 15120 }],
"vatTotal": 15120,
"total": 87120
}
}Paste it into Invoices → From JSON to see it render.