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íčů:
| Operace | Endpoint |
|---|---|
| Seznam pokladen | GET /api/v1/pos-devices |
| Založení pokladny | POST /api/v1/pos-devices — tělo { "name": "...", "prefix": "P01" } |
| Úprava názvu/prefixu | PATCH /api/v1/pos-devices/:id |
| Rotace tokenu | POST /api/v1/pos-devices/:id/rotate-token |
| Zneplatnění pokladny | POST /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 hodnotSIMPLIFIED,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 uINVOICE; u ostatních dvou typů se ignoruje, server tam interně dosadíissuedAt(doklad je uhrazen ihned). -
paymentMethod(řetězec, např.CASH,CARD) — povinné uSIMPLIFIEDaTAX_DOCUMENT, nepoužívá se uINVOICE. -
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řítomnostcustomer.nameje to, co rozhoduje o „identifikovaném odběrateli“ ve validaci níž. -
items(pole, min. 1 položka) — u každé položkyname(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:
-
SIMPLIFIED —
customer.namenesmí 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 INVOICE —
customer.nameje 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. -
quantitynesmí být nula — nulové množství by vyrobilo řádek, který nic nefakturuje, ale na dokladu by zůstal. -
quantityiunitPricemusí 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ýš);-
issuedAt— př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.idempotencyKeyu 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/documentstu 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ýš.
| Stav | Kdy nastane | Co 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).
|
409ORIGINAL_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í.
numbervždy obsahuje prefix konkrétní pokladny, ať nekoliduje s jinými pokladnami ani s ručně vystavenými fakturami.Idempotency-Keyvzniká 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í
issuedAta 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
CREATEDneboREPLAYED. 409se neopakuje beze změny čísla dokladu;401se 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ímidz odpovědi na vystavení. - Storno posílat jen v den vystavení dokladu; po tomto dni vždy opravný daňový doklad.
409 ORIGINAL_NOT_SYNCEDna stornu i na opravném dokladu znamená „zkusit později“, ne trvalou chybu.