InvoiceHub dokumentace

Co s dokladem po vystavení – tiskový výstup na 80mm pásce pro termotiskárnu, reprint (kopie) a odeslání dokladu e-mailem přímo z pokladny – popisuje samostatná stránka Pokladny.

Vlastní základna URL, mimo /api/v1. Zatímco zbytek API InvoiceHub žije pod https://invoicehub-api.zeabur.app/api/v1/…, POS endpointy jsou schválně mimo tenhle prefix:

POST https://invoicehub-api.zeabur.app/api/pos/documents
POST https://invoicehub-api.zeabur.app/api/pos/documents/batch
POST https://invoicehub-api.zeabur.app/api/pos/documents/cancel
POST https://invoicehub-api.zeabur.app/api/pos/documents/credit-notes

Správa samotných pokladen (založení, rotace a zneplatnění tokenu) naopak žije pod obvyklým /api/v1/pos-devices a volá ji přihlášený člověk v administraci firmy — viz Autentizace níž.

Autentizace pokladny

Pokladna se neautentizuje API klíčem ani OAuth tokenem uživatele — má vlastní typ přihlašovacího údaje, token pokladního zařízení, vázaný na jeden konkrétní kus hardwaru (pokladnu) ve firmě.

Kdo token zakládá

Token si pokladna sama nevydává. Založí ho člověk přihlášený v aplikaci InvoiceHub — vlastník firmy nebo administrátor se sekcí Nastavení — přes endpointy pod /api/v1/pos-devices, které vyžadují běžné přihlášení (JWT/session), stejně jako správa API klíčů:

OperaceEndpoint
Seznam pokladenGET /api/v1/pos-devices
Založení pokladny POST /api/v1/pos-devices — tělo { "name": "...", "prefix": "P01" }
Úprava názvu/prefixuPATCH /api/v1/pos-devices/:id
Rotace tokenuPOST /api/v1/pos-devices/:id/rotate-token
Zneplatnění pokladnyPOST /api/v1/pos-devices/:id/revoke

Odpověď na založení i na rotaci vrací token v plaintextu — jen tento jednou. Databáze si drží pouze bcrypt otisk, takže ho podpora ani vlastník firmy později znovu nezobrazí; token proto zapište do pokladny hned při instalaci. Rotace starý token okamžitě zneplatní (druhý platný token pro tutéž pokladnu neexistuje) a zneplatnění (revoke) je trvalé — pokladna dostane po obojím 401 a obsluha musí zadat nový token ručně.

Tvar hlavičky

Authorization: Bearer pos_<64 hex znaků>

Token má vlastní prefix pos_ (na rozdíl od API klíčů), takže ho nejde omylem zaměnit s API klíčem ani s OAuth access tokenem. Neplatný i zneplatněný token vrací stejnou hlášku 401 Neplatný nebo zneplatněný token pokladny — rozlišení schválně nedostanete, aby uniklý token nešel jinak ověřit.

Co token smí a nesmí

Token pokladny funguje výhradně na /api/pos/*. Autentizační middleware pokladny je zavěšený přímo a jen na tomto routeru — žádná jiná cesta API (ani /api/v1/documents, ani /api/v1/pos-devices, kterým se pokladny spravují) ho neumí ověřit. Poslat token pokladny na jakoukoli jinou cestu tedy skončí 401, ne úspěchem s omezenými právy — token nemá žádný seznam oprávnění, který by šlo obejít, prostě mimo /api/pos/* nikde neplatí.

V praxi to znamená: pokladna může vystavovat doklady (jednotlivě i dávkově), stornovat je a vystavovat k nim opravný daňový doklad, ale nemůže je číst zpět, upravovat, mazat, stahovat PDF, spravovat kontakty ani cokoli jiného. Čtení a další práci s vystavenými doklady dělá člověk v aplikaci InvoiceHub (nebo integrace s API klíčem/OAuth tokenem), ne pokladna sama.

POST /api/pos/documents

Vystaví jeden doklad. Vyžaduje hlavičku Idempotency-Key (viz Idempotence níž) — bez ní vrátí server rovnou 400, ještě než se podívá na tělo požadavku.

POST /api/pos/documents
Authorization: Bearer pos_...
Idempotency-Key: <unikátní klíč pokladny>
Content-Type: application/json

Pole těla požadavku

  • number (povinné, řetězec) — číslo dokladu. Číslo vždy zadává pokladna, InvoiceHub ho nepřiděluje ani nedopočítává.
  • documentType (povinné) — jedna z hodnot SIMPLIFIED, TAX_DOCUMENT, INVOICE.
  • issuedAt (povinné, ISO 8601) — datum a čas vystavení / den uskutečnění zdanitelného plnění (DUZP). U dokladu uhrazeného na místě je to zároveň datum úhrady.
  • dueAt (ISO 8601) — splatnost. Povinné a používané jen u INVOICE; u ostatních dvou typů se ignoruje, server tam interně dosadí issuedAt (doklad je uhrazen ihned).
  • paymentMethod (řetězec, např. CASH, CARD) — povinné u SIMPLIFIED a TAX_DOCUMENT, nepoužívá se u INVOICE.
  • roundingEnabled (bool, výchozí false) — viz Zaokrouhlení.
  • currency (volitelné) — pokud je uvedeno, musí to být "CZK". Pokladna zatím neumí vystavovat v jiné měně.
  • customer (objekt, volitelný podle typu) — name, street, city, zip, country, taxId, vatId. Přítomnost customer.name je to, co rozhoduje o „identifikovaném odběrateli“ ve validaci níž.
  • items (pole, min. 1 položka) — u každé položky name (povinné), quantity, unit (výchozí ks), unitPrice, vatRate.
  • note (volitelné) — poznámka na dokladu.

Ceny na pokladně jsou vždy s DPH. unitPrice je cena tak, jak je na cenovce — včetně daně. Server z ní základ daně dopočítá zpětně (rozdělením, ne druhým násobením), tenhle režim není u POS dokladu možné vypnout.

Tvar odpovědi

Úspěch je 201 Created (nový doklad) nebo 200 OK (opakované volání se stejným Idempotency-Key, viz níž). Tělo je v obou případech stejné:

{
  "data": {
    "id": "clz1a2b3c0001qw3f7h2n5m1a",
    "number": "P01-2026-000842",
    "status": "PAID",
    "posDeviceId": "clz0z9y8x0000qw3f6g1m4l0z",
    "posDocumentKind": "SIMPLIFIED",
    "issuedAt": "2026-09-03T07:12:00.000Z",
    "dueAt": "2026-09-03T07:12:00.000Z",
    "paidAt": "2026-09-03T07:12:00.000Z",
    "currency": "CZK",
    "subtotal": "110.01",
    "vatTotal": "19.89",
    "roundingAmount": "0.10",
    "total": "130.00",
    "clientName": null,
    "paymentMethod": "CASH",
    "variableSymbol": null,
    "createdAt": "2026-09-03T07:12:01.348Z"
  },
  "replayed": false
}

replayed: true znamená, že žádný nový doklad nevznikl — server vrátil ten, který už z dřívějšího volání se stejným klíčem existuje.

Vystavený doklad se z pokladny nikdy needituje. POS API nemá PATCH ani DELETE — obsah dokladu se po vystavení nemění. Vrácení zboží řeší pokladna dvěma vlastními akcemi, storno nebo opravný daňový doklad — nikdy mínusovou položkou v nové účtence, tu server odmítne.

Příklady pro tři typy dokladu

Všechny tři typy se ukládají interně jako Document.type = INVOICE — InvoiceHub jejich rozdíl vidí přes posDocumentKind. Rozdíl mezi nimi je v odběrateli, splatnosti, způsobu úhrady a limitu částky, viz validační pravidla níž.

SIMPLIFIED — zjednodušený daňový doklad

Bez odběratele, uhrazeno na místě, do 10 000 Kč včetně DPH.

POST /api/pos/documents
Idempotency-Key: P01-local-000842

{
  "number": "P01-2026-000842",
  "documentType": "SIMPLIFIED",
  "issuedAt": "2026-09-03T07:12:00.000Z",
  "paymentMethod": "CASH",
  "roundingEnabled": true,
  "items": [
    { "name": "Espresso", "quantity": 2, "unit": "ks", "unitPrice": 45.00, "vatRate": 21 },
    { "name": "Croissant", "quantity": 1, "unit": "ks", "unitPrice": 39.90, "vatRate": 12 }
  ]
}
201 Created
{
  "data": {
    "id": "clz1a2b3c0001qw3f7h2n5m1a",
    "number": "P01-2026-000842",
    "status": "PAID",
    "posDeviceId": "clz0z9y8x0000qw3f6g1m4l0z",
    "posDocumentKind": "SIMPLIFIED",
    "issuedAt": "2026-09-03T07:12:00.000Z",
    "dueAt": "2026-09-03T07:12:00.000Z",
    "paidAt": "2026-09-03T07:12:00.000Z",
    "currency": "CZK",
    "subtotal": "110.01",
    "vatTotal": "19.89",
    "roundingAmount": "0.10",
    "total": "130.00",
    "clientName": null,
    "paymentMethod": "CASH",
    "variableSymbol": null,
    "createdAt": "2026-09-03T07:12:01.348Z"
  },
  "replayed": false
}

Pole customer tu chybí úplně — kdyby obsahovalo name, server by dokument odmítl (SIMPLIFIED nesmí mít identifikovaného odběratele).

TAX_DOCUMENT — plný daňový doklad uhrazený na místě

Identifikovaný odběratel, uhrazeno na místě, žádný limit částky.

POST /api/pos/documents
Idempotency-Key: P01-local-000843

{
  "number": "P01-2026-000843",
  "documentType": "TAX_DOCUMENT",
  "issuedAt": "2026-09-03T09:45:00.000Z",
  "paymentMethod": "CARD",
  "roundingEnabled": false,
  "customer": {
    "name": "Jan Novák",
    "street": "Hlavní 12",
    "city": "Brno",
    "zip": "60200",
    "country": "CZ",
    "taxId": "74638291"
  },
  "items": [
    { "name": "Konzultace", "quantity": 1, "unit": "hod", "unitPrice": 1500, "vatRate": 21 }
  ]
}
201 Created
{
  "data": {
    "id": "clz1a2b3c0002qw3f7h2n5m1b",
    "number": "P01-2026-000843",
    "status": "PAID",
    "posDeviceId": "clz0z9y8x0000qw3f6g1m4l0z",
    "posDocumentKind": "TAX_DOCUMENT",
    "issuedAt": "2026-09-03T09:45:00.000Z",
    "dueAt": "2026-09-03T09:45:00.000Z",
    "paidAt": "2026-09-03T09:45:00.000Z",
    "currency": "CZK",
    "subtotal": "1239.67",
    "vatTotal": "260.33",
    "roundingAmount": "0.00",
    "total": "1500.00",
    "clientName": "Jan Novák",
    "paymentMethod": "CARD",
    "variableSymbol": null,
    "createdAt": "2026-09-03T09:45:01.221Z"
  },
  "replayed": false
}

INVOICE — faktura na splatnost

Identifikovaný odběratel, neuhrazeno při vystavení, vyžaduje dueAt. paymentMethod se u tohoto typu neposílá.

POST /api/pos/documents
Idempotency-Key: P01-local-000844

{
  "number": "P01-2026-000844",
  "documentType": "INVOICE",
  "issuedAt": "2026-09-03T14:05:00.000Z",
  "dueAt": "2026-09-17T00:00:00.000Z",
  "customer": {
    "name": "Novák s.r.o.",
    "street": "Obchodní 8",
    "city": "Praha",
    "zip": "10000",
    "country": "CZ",
    "taxId": "25596641",
    "vatId": "CZ25596641"
  },
  "note": "Odběr na fakturu dle rámcové smlouvy",
  "items": [
    { "name": "Catering – firemní akce", "quantity": 1, "unit": "ks", "unitPrice": 12100, "vatRate": 21 }
  ]
}
201 Created
{
  "data": {
    "id": "clz1a2b3c0003qw3f7h2n5m1c",
    "number": "P01-2026-000844",
    "status": "ISSUED",
    "posDeviceId": "clz0z9y8x0000qw3f6g1m4l0z",
    "posDocumentKind": "INVOICE",
    "issuedAt": "2026-09-03T14:05:00.000Z",
    "dueAt": "2026-09-17T00:00:00.000Z",
    "paidAt": null,
    "currency": "CZK",
    "subtotal": "10000.00",
    "vatTotal": "2100.00",
    "roundingAmount": "0.00",
    "total": "12100.00",
    "clientName": "Novák s.r.o.",
    "paymentMethod": null,
    "variableSymbol": "2026000844",
    "createdAt": "2026-09-03T14:05:01.009Z"
  },
  "replayed": false
}

Všimněte si variableSymbol: u INVOICE ho server dopočítá sám z číslic v number (posledních až 10 číslic) — kvůli párování s bankou. U SIMPLIFIED a TAX_DOCUMENT zůstává vždy null, protože se platí ihned a párovat s bankou není co.

Validační pravidla

Validace běží v pevném pořadí a server se zastaví na první chybě, kterou najde — odpověď 400 vždy nese jen jednu srozumitelnou hlášku, ne seznam všech prohřešků najednou.

Limit 10 000 Kč u SIMPLIFIED

Zjednodušený daňový doklad smí podle zákona o DPH znít nejvýš na 10 000 Kč včetně daně. Server to počítá až z dopočtené celkové částky (total, po slevách i po případném zaokrouhlení), ne z jednotlivých položek zvlášť — víc levných položek, které dohromady přesáhnou limit, doklad odmítne stejně jako jedna drahá. Limit se týká jen SIMPLIFIED; u TAX_DOCUMENT a INVOICE horní hranice částky není (kromě obecného technického stropu 9 999 999 999,99 daného přesností sloupců v databázi).

Identifikovaný odběratel

„Identifikovaný odběratel“ v kódu znamená prostě neprázdné customer.name. Podle typu dokladu platí přesně opačné pravidlo:

  • SIMPLIFIEDcustomer.name nesmí být vyplněné vůbec. Pošlete-li ho, server doklad odmítne, i kdyby byl obsah jinak v pořádku a doklad pod limitem.
  • TAX_DOCUMENT a INVOICEcustomer.name je naopak povinné. Ostatní pole odběratele (adresa, IČO, DIČ) povinná nejsou, ale doporučujeme je vždy poslat, jsou-li k dispozici — doklad se zákazníkovi bude líp vystavovat zpětně (dobropis, reklamace).

Formát čísla dokladu a prefixu zařízení

number je libovolný neprázdný řetězec, který si volí pokladna — server na jeho tvar neklade žádný formátový požadavek (délku, znaky, pomlčky). Kontroluje jen jedinečnost.

Pozor na sdílený číselný prostor. Všechny tři typy POS dokladu se v databázi ukládají jako Document.type = "INVOICE" a jedinečnost čísla se hlídá právě na dvojici (firma, type) — tedy napříč SIMPLIFIED/TAX_DOCUMENT/INVOICE z pokladny i napříč běžnými fakturami vystavenými ve webové aplikaci nebo přes /api/v1/documents. Dvě různé pokladny stejné firmy, nebo pokladna a účetní vystavující fakturu ručně, si tak číslo mohou omylem přebít. Zabráníte tomu tím, že do number vždy zahrnete prefix vlastní pokladny (pole prefix nastavené při založení zařízení, např. P01) — server to nekontroluje ani nevynucuje, je to čistě konvence, ale bez ní kolize je jen otázka času.

Prefix zařízení sám o sobě je jen popisek pro člověka v administraci (vidíte ho u pokladny v seznamu) — API ho nikde nečte ani neporovnává s number, zodpovědnost za to, že se ve výsledném čísle skutečně objeví, je na pokladně.

Sazby DPH

Každá položka musí mít vatRate rovné jedné z hodnot 21, 12 nebo 0 (procenta). Jiná hodnota — třeba starší sazba 15 %, desetinné číslo nebo záporná sazba — vrátí 400 s odkazem na konkrétní položku (items[N].vatRate).

Ostatní validace položek

  • Doklad musí mít aspoň jednu položku a každá položka neprázdné name.
  • quantity nesmí být nula — nulové množství by vyrobilo řádek, který nic nefakturuje, ale na dokladu by zůstal.
  • quantity i unitPrice musí být číslo v rozsahu daném přesností databázových sloupců (množství nejvýš 99 999 999,9999, cena nejvýš 9 999 999 999,99) a dopočtené částky (základ, DPH, celkem) nesmí tento rozsah přetéct ani součtem přes víc položek.

Idempotence a opakování

Pokladna běží v prostředí s nespolehlivou sítí — požadavek může dojít na server a odpověď se ztratit cestou zpět. Hlavička Idempotency-Key řeší přesně tenhle případ: umožňuje bezpečně zopakovat stejné volání, aniž by vznikl doklad dvakrát.

  • Klíč je vázaný na konkrétní pokladnu, ne na firmu jako celek. Dvě různé pokladny stejné firmy smí nezávisle použít stejnou hodnotu klíče — typicky ho obě odvozují ze svého vlastního lokálního ID účtenky.
  • Zvolte klíč tak, aby byl stabilní napříč pokusy o odeslání jednoho a téhož prodeje — např. UUID vygenerované v okamžiku vzniku účtenky lokálně na pokladně, ne nově při každém pokusu o odeslání.

Co se stane při opakování

Server nejdřív vždy zkontroluje, jestli pod dvojicí (pokladna, klíč) už doklad existuje — dřív, než se pustí do validace a zakládání. Pokud ano, nic nezakládá a vrátí ten existující doklad s "replayed": true a stavovým kódem 200 (na rozdíl od 201 u nově založeného). Vlastní obsah požadavku (položky, částky) se při replay vůbec neporovnává — server věří, že stejný klíč = stejný pokus o tentýž prodej.

Co znamená 409

409 Conflict nastane, když číslo dokladu (number) v dané firmě už existuje, ale pod jiným Idempotency-Key u téže pokladny (nebo vůbec u dokladu, který nevznikl z POS). Jinými slovy: stejné číslo, odlišný obsah/kontext. To je vždy chyba na straně pokladny — buď se číslo omylem zopakovalo (přetekl lokální čítač, dvě zařízení sdílí prefix), nebo někdo mezitím vystavil ruční fakturu se stejným číslem z webové aplikace.

409 se nemá automaticky opakovat se stejným tělem — opakování dopadne stejně, dokud se nezmění number. Řešení je přidělit dokladu nové, dosud nepoužité číslo a poslat ho znovu s novým Idempotency-Key.

Offline fronta a dávkové dosílání

Pokladna musí umět prodávat i bez připojení k internetu — výpadek sítě v prodejně nesmí zastavit prodej. POS API na to nemá zvláštní „offline endpoint“: řešením je vystavit doklad lokálně a dosílat frontu později přes stejný tvar požadavku, jen hromadně.

1. Vystavení při výpadku sítě

Pokladna si při prodeji bez připojení lokálně uloží kompletní podklad pro doklad:

  • number — přidělené z vlastní lokální číselné řady (s prefixem zařízení, viz výš);
  • issuedAtpřesný okamžik prodeje, ne okamžik pozdějšího odeslání;
  • zvolený documentType, položky, případně odběratele a způsob úhrady;
  • vlastní Idempotency-Key (resp. idempotencyKey u dávky) — vygenerovaný hned při vystavení, ne až při pokusu o odeslání, aby opakované pokusy o dosílání jednoho dokladu nesly pořád stejnou hodnotu.

Doklad se zákazníkovi vytiskne / zobrazí z tohoto lokálního záznamu okamžitě — čeká se jen s odesláním na server, ne s obsluhou zákazníka.

2. Dosílání fronty přes dávkový endpoint

Jakmile se síť obnoví, pokladna pošle nahromážděnou frontu jedním voláním:

POST /api/pos/documents/batch
Authorization: Bearer pos_...
Content-Type: application/json

{
  "documents": [
    {
      "idempotencyKey": "P01-local-000841",
      "number": "P01-2026-000841",
      "documentType": "SIMPLIFIED",
      "issuedAt": "2026-08-30T08:03:00.000Z",
      "paymentMethod": "CASH",
      "roundingEnabled": true,
      "items": [
        { "name": "Bageta", "quantity": 1, "unit": "ks", "unitPrice": 89.90, "vatRate": 21 }
      ]
    },
    {
      "idempotencyKey": "P01-local-000842",
      "number": "P01-2026-000842",
      "documentType": "SIMPLIFIED",
      "issuedAt": "2026-08-30T08:11:00.000Z",
      "paymentMethod": "CARD",
      "items": [
        { "name": "Limonáda", "quantity": 2, "unit": "ks", "unitPrice": 55, "vatRate": 21 }
      ]
    }
  ]
}

U dávky se Idempotency-Key neposílá jako hlavička, ale jako pole idempotencyKey u každé položky zvlášť — dávka jako celek žádný jeden idempotentní klíč nemá, jen jednotlivé doklady v ní.

Odpověď — výsledek za každou položku zvlášť

Dávka se zpracovává postupně, ne paralelně (doklady sdílí číselný prostor firmy) a chyba jednoho dokladu nezastaví zbytek fronty. Odpověď má vždy 207 Multi-Status a pole výsledků ve stejném pořadí a délce, v jaké přišly doklady na vstupu:

207 Multi-Status
{
  "data": {
    "results": [
      { "index": 0, "status": "CREATED", "document": { "...": "stejný tvar jako u POST /documents" } },
      { "index": 1, "status": "REPLAYED", "document": { "...": "doklad z předchozího pokusu o dosílání" } },
      { "index": 2, "status": "ERROR", "error": "Chybí idempotencyKey u položky dávky" }
    ]
  }
}

Stavy jsou tři: CREATED (nově založen), REPLAYED (už existoval z dřívějšího pokusu o dosílání — typicky když se dávka posílala vícekrát, protože spojení uprostřed dosílání znovu vypadlo) a ERROR (validace, nebo 409 stejné jako u jednotlivého volání, jen zabalené do textu chyby). Pokladna projde výsledky, z fronty odstraní vše s CREATED/ REPLAYED a ponechá k dalšímu pokusu jen položky s ERROR, které má smysl opakovat (viz chybové stavy — ne každou chybu má cenu posílat znovu beze změny).

Celý požadavek vrátí obyčejný 400 jen tehdy, když pole documents chybí nebo je prázdné — to je chyba samotného volání, ne obsahu fronty.

Datum prodeje se při dosílání nemění. issuedAt/DUZP zůstává přesně to, co pokladna zapsala v okamžiku prodeje — server ho nikdy nepřepisuje na aktuální čas dosílání. Doklad odeslaný o tři dny později tak na sobě nese datum prodeje, ne datum doručení do systému; to je zákonný požadavek na DUZP a je to i důvod, proč issuedAt musí pokladna posílat sama, ne nechat dopočítat serverem.

Zaokrouhlení

roundingEnabled: true zapíná zaokrouhlení celkové částky dokladu na celé koruny — běžné u plateb v hotovosti, kde se haléře fyzicky nevydávají. Bez tohoto příznaku (výchozí stav, false) doklad zůstává na přesnou částku na dvě desetinná místa.

Rozdíl mezi částkou před zaokrouhlením a po něm se posílá zpět ve vlastním poli roundingAmount — nikdy se nepromítne do subtotal ani vatTotal jednotlivých položek. Zaokrouhlení se počítá na dvě desetinná místa dolů/nahoru k nejbližší celé koruně (půlka nahoru) a je vždy poslední krok, až po slevách.

Zaokrouhlení stojí mimo základ daně. Haléřové vyrovnání na celou korunu není podle zákona o DPH předmětem daně — je to platební zaokrouhlení, ne sleva ani příplatek za zboží. Proto se nezapočítává ani do základu, ani do DPH, a v sestavě dokladu se ukazuje jako samostatný řádek roundingAmount. Součet, který zákazník doopravdy platí, je vždy subtotal + vatTotal + roundingAmount = total.

Konkrétně v příkladu SIMPLIFIED výš: položky dají dohromady 129,90 Kč (základ 110,01 + DPH 19,89). Se zapnutým zaokrouhlením se částka zaokrouhlí na 130,00 Kč, rozdíl 0,10 Kč je roundingAmount a právě o něj je total vyšší než součet subtotal + vatTotal bez něj.

POST /api/pos/documents/cancel — storno dokladu

Zruší doklad jako celek. Číslo zůstává obsazené — díra v číselné řadě je v pořádku, přečíslování nebo zmizelý doklad ne. Stornovaný doklad (status: "CANCELLED") nikdy nevstupuje do tržeb ani do podkladů k DPH.

Použitelné jen v den vystavení (podle českého kalendářního dne, ne UTC) — pozdější vrácení řeší opravný daňový doklad. Adresuje se originalNumber, evidenčním číslem dokladu, ne jeho interním id: pokladna smí frontu dosílat mimo pořadí a storno tak může na server dorazit dřív než vystavení, ke kterému patří (id v tu chvíli ještě nezná).

POST /api/pos/documents/cancel
Authorization: Bearer pos_...
Idempotency-Key: <unikátní klíč pokladny>
Content-Type: application/json

{
  "originalNumber": "P01-2026-000842",
  "canceledAt": "2026-09-03T11:20:00.000Z",
  "reason": "Zákazník odstoupil od nákupu"
}
  • originalNumber (povinné) — evidenční číslo stornovaného dokladu.
  • canceledAt (povinné, ISO 8601) — kdy storno na pokladně skutečně proběhlo. Server ho nikdy nepřepisuje na čas doručení požadavku — nutné pro offline dosílání se zpožděním.
  • reason (volitelné) — uloží se do historie dokladu.
200 OK
{
  "data": {
    "id": "clz1a2b3c0001qw3f7h2n5m1a",
    "number": "P01-2026-000842",
    "status": "CANCELLED",
    "posDeviceId": "clz0z9y8x0000qw3f6g1m4l0z",
    "issuedAt": "2026-09-03T07:12:00.000Z",
    "canceledAt": "2026-09-03T11:20:00.000Z"
  },
  "replayed": false
}

Doklad, který ještě nedorazil. Když server originalNumber nezná, vrátí 409 s "code": "ORIGINAL_NOT_SYNCED" — ne trvalou chybu, ale pokyn zkusit storno dosílat později, až bude mít pokladna jistotu, že původní doklad je v InvoiceHubu už nahraný.

Idempotence je stejná jako u vystavení: opakované volání se stejným Idempotency-Key vrátí replayed: true a doklad nestornuje podruhé. Storno doklad po dni vystavení (jiný kalendářní den než issuedAt) skončí 409 — v tu chvíli patří vrácení peněz opravný daňový doklad, ne storno.

POST /api/pos/documents/credit-notes — opravný daňový doklad (§ 45)

Řeší částečné vrácení, nebo vrácení po dni vystavení, kdy prosté storno už nejde použít. Vzniká jako nový doklad s vlastním číslem — na rozdíl od storna nezneplatňuje původní doklad, jen k němu eviduje opravu.

POST /api/pos/documents/credit-notes
Authorization: Bearer pos_...
Idempotency-Key: <unikátní klíč pokladny>
Content-Type: application/json

{
  "number": "P01-OD-2026-000012",
  "originalNumber": "P01-2026-000842",
  "issuedAt": "2026-09-05T10:05:00.000Z",
  "reason": "Reklamace — vráceno jedno espresso",
  "items": [
    { "name": "Espresso", "quantity": -1, "unit": "ks", "unitPrice": 45.00, "vatRate": 21 }
  ]
}
  • number (povinné) — číslo opravného dokladu z vlastní číselné řady pokladny, stejně jako u vystavení. Vlastní jmenný prostor od faktur/účtenek (typ dokladu je jiný, CREDIT_NOTE).
  • originalNumber (povinné) — evidenční číslo opravovaného dokladu; stejné pravidlo dosílání mimo pořadí jako u storna výš.
  • issuedAt (povinné, ISO 8601) — datum vystavení opravného dokladu, může být zpětné.
  • reason (povinné) — důvod opravy, tiskne se na doklad.
  • items (povinné, min. 1) — rozdílové položky v původních sazbách DPH. Množství i cena smí být záporné (vratka) i kladné (doúčtování) — na rozdíl od /documents tu nula ani mínus nekončí chybou.

Odběratele si doklad bere sám. Pole pro odběratele v těle požadavku není — opravný doklad ho přebírá z původního dokladu, pokud tam byl identifikovaný.

201 Created
{
  "data": {
    "id": "clz1c4d5e0002qw3f9k7p2n3b",
    "number": "P01-OD-2026-000012",
    "status": "ISSUED",
    "correctedDocumentId": "clz1a2b3c0001qw3f7h2n5m1a",
    "creditNoteReason": "Reklamace — vráceno jedno espresso",
    "clientName": null,
    "total": "-54.45"
  },
  "replayed": false
}

Idempotence i chování při nedoručeném originálu (409 ORIGINAL_NOT_SYNCED) jsou stejné jako u storna výš. Opakované volání se stejným Idempotency-Key nikdy nevytvoří druhý opravný doklad.

Chybové stavy

Chybová odpověď má vždy stejný tvar jako zbytek InvoiceHub API: { "error", "message", "code" }, případně "details" u podrobnějších validací. U jednotlivého POST /documents chyba zastaví celý požadavek; u /documents/batch se stejné chyby (kromě chybějícího/ prázdného pole documents) hlásí jen jako výsledek dané položky, viz offline fronta výš.

StavKdy nastaneCo s tím na pokladně
400 Chybí hlavička Idempotency-Key u jednotlivého volání. Chyba integrace, ne obsahu prodeje — doplnit hlavičku a poslat znovu.
400 Validace obsahu (chybějící povinné pole, neplatná sazba DPH, nulové množství, SIMPLIFIED nad limit, chybějící/přebývající odběratel podle typu, chybějící dueAt u faktury…). Nic se nezaložilo. Bezpečně opravit obsah a poslat znovu se stejným Idempotency-Key — validace proběhne nanovo nad opraveným tělem.
401 Token chybí, je neplatný, nebo byl rotací/zneplatněním odvolán. Neopakovat ve smyčce — potřeba nový token od vlastníka firmy, žádný retry to nespraví.
409 Číslo dokladu (number) v této firmě už existuje pod jiným Idempotency-Key (jiný prodej, nebo doklad vystavený mimo POS). Neopakovat se stejným číslem — přidělit nové, dosud nepoužité number, nový Idempotency-Key a poslat znovu.
200/201 Úspěch — 200 jen u replay se stejným idempotentním klíčem. Frontu na pokladně považovat za doručenou, dál neposílat.
207 Dávka /documents/batch zpracována — výsledek je per-item. Z fronty odstranit CREATED/REPLAYED; položky s ERROR vyhodnotit stejně jako výše (400 opravit a zkusit znovu, 409 přečíslovat, jiné neopakovat beze změny).
409
ORIGINAL_NOT_SYNCED
/documents/cancel nebo /documents/credit-notes odkazují originalNumber, který server ještě nezná. Neopakovat hned — počkat, až bude jistota, že původní doklad je nahraný (typicky po úspěšném dosílání fronty), a zkusit znovu se stejným Idempotency-Key.
409 Storno mimo den vystavení, nebo doklad, který je už stornovaný/nejde stornovat ze svého stavu. Neopakovat beze změny — po dni vystavení patří vrácení peněz opravný daňový doklad, ne storno.

Checklist integrace

  • Token pokladny uložen bezpečně při instalaci — podruhé ho nikdo nezobrazí.
  • number vždy obsahuje prefix konkrétní pokladny, ať nekoliduje s jinými pokladnami ani s ručně vystavenými fakturami.
  • Idempotency-Key vzniká při vystavení dokladu na pokladně, ne až při pokusu o odeslání.
  • Ceny položek se posílají s DPH (tak, jak jsou na cenovce).
  • SIMPLIFIED nikdy nenese customer.name; TAX_DOCUMENT a INVOICE ho vždy vyžadují.
  • SIMPLIFIED hlídat proti limitu 10 000 Kč včetně DPH ještě na pokladně, ne až čekat na 400 ze serveru.
  • Offline doklady si nesou původní issuedAt a dosílají se přes /documents/batch.
  • Po dosílání dávky se z lokální fronty maže jen to, co vyšlo jako CREATED nebo REPLAYED.
  • 409 se neopakuje beze změny čísla dokladu; 401 se neopakuje vůbec.
  • Vrácení zboží nikdy neřešit mínusovou položkou v nové účtence — vždy storno, nebo opravný daňový doklad.
  • Storno adresovat originalNumber, ne interním id z odpovědi na vystavení.
  • Storno posílat jen v den vystavení dokladu; po tomto dni vždy opravný daňový doklad.
  • 409 ORIGINAL_NOT_SYNCED na stornu i na opravném dokladu znamená „zkusit později“, ne trvalou chybu.