InvoiceyDocs
Technical reference

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's vatRate must be 0, and localReverseChargeCode is required.
  • oss with suppliesAbroad: 'none' is invalid — OSS is cross-border by definition.
  • issuer.vatPayer === false → mode must be regular and every vatRate must be 0.

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 vatBreakdown entry per distinct rate, with base the 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.