Přes API nahrajete doklady a stáhnete hotový export, aniž byste otevřeli prohlížeč. Hodí se, když už doklady někde sbíráte, třeba v n8n, Make nebo ve vlastní aplikaci, a chcete je posílat rovnou k nám. Dostupné je API na každém tarifu včetně zdarma, doklady se přes něj účtují stejně jako v aplikaci, tedy z kreditů.
Vydání klíče
Klíč vydáte v Tým a nastavení v sekci API klíče. Vydat ho může vlastník kanceláře a kolega s právem spravovat tým. Klient klíč vydat nemůže.
U klíče zaškrtnete, co smí: číst doklady, nahrávat doklady, číst bankovní výpisy a nahrávat bankovní výpisy. Doklady a výpisy jsou schválně oddělené, takže klíč vydaný na faktury se k bankovní historii nedostane. Oprávnění klíče se nedají dodatečně měnit; když k výpisům potřebujete pustit i stávající automatizaci, vydejte nový klíč a starý zneplatněte.
Tajnou část klíče uvidíte jen jednou, hned po vytvoření. Uložte si ji do správce hesel nebo rovnou do nastavení své automatizace. Znovu ji nezobrazíme ani my. Když ji ztratíte, klíč zneplatníte a vydáte nový.
Klíč patří jedné kanceláři a nedostane se k dokladům jiné. Zároveň nikdy neumí více než ten, kdo ho vydal: když někomu odeberete oprávnění, jeho klíče o ně přijdou hned, a když ho z kanceláře odstraníte, přestanou fungovat úplně.
Přihlášení
Klíč posílejte v hlavičce:
Authorization: Bearer cf_live_...
Rychlý start
Od souboru k importu do účetnictví to jsou čtyři volání, z toho tři na naše API:
API="https://ctenifaktur.cz/api/v1"
AUTH="Authorization: Bearer cf_live_..."
# 1. Řekněte si o adresu. Vrátí batchId a uploadUrl.
curl -X POST $API/documents \
-H "$AUTH" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"files":[{"fileName":"faktura.pdf",
"contentType":"application/pdf","sizeBytes":184320}]}'
# 2. Pošlete bajty. Bez hlaviček, na adresu z kroku 1.
curl -X PUT --data-binary @faktura.pdf "UPLOAD_URL"
# 3. Stav. Opakujte, dokud je status pending nebo processing.
curl $API/batches/BATCH_ID -H "$AUTH"
# 4. Export. Id jsou v uploads[].documentIds.
curl -X POST $API/documents/export \
-H "$AUTH" \
-H "Content-Type: application/json" \
-d '{"documentIds":["DOC_ID"],"format":"pohoda"}' \
-o faktura.xml
Nástroj pro příkazovou řádku
Když si kvůli pár dokladům nechcete psát vlastní skript, použijte naše CLI. Vypíše účetní jednotky i dříve nahrané doklady, nahraje nové a stáhne hotový export.
Potřebujete Node 20 nebo novější:
npm install -g ctenifaktur
Klíč zadáte jednou příkazem ctenifaktur login. Při psaní se nevypisuje a neuloží se do historie příkazů, nástroj si ho ověří a schová do vašeho domovského adresáře jen pro vás. Odhlásíte se příkazem ctenifaktur logout.
ctenifaktur login
# Vypíše i dříve nahrané doklady bez čísla dávky.
ctenifaktur documents
# Nahraje soubor, počká na zpracování a vypíše id vzniklého dokladu.
ctenifaktur upload faktura.pdf
# Stáhne hotový export pro Pohodu.
ctenifaktur export DOC_ID --format pohoda --out faktura.xml
Bankovní výpisy mají vlastní dvojici příkazů, protože druh dokumentu se u nás vždy deklaruje, nehádá se podle obsahu:
# Nahraje výpis (PDF, obrázek nebo CSV z platební brány) a počká na zpracování.
ctenifaktur upload-statement vypis-07.pdf
# Stáhne GPC pro účetní program, nebo SEPA XML (camt.053).
ctenifaktur export-statement STMT_ID --format gpc --out vypis.gpc
Kolik toho kancelář ještě zpracuje, řekne ctenifaktur credits, a to zvlášť pro tarifní kvótu, která se k uvedenému datu obnoví, a pro kredity, které ne. Doklad i výpis můžete rovnou zařadit do účetní jednotky přepínačem --unit, její id vypíše ctenifaktur units. Stejným přepínačem omezíte ctenifaktur documents na jednu jednotku, starší stránky projdete přes --page a --limit. Pro běh ze skriptu nebo z cronu přidejte vlastní --idempotency-key a při opakování použijte tentýž, jinak se doklad zpracuje a zaplatí podruhé. Klíč tam místo přihlášení předáte proměnnou CF_API_KEY. Všechny příkazy a přepínače vypíše ctenifaktur --help. Bez instalace to jde přes npx ctenifaktur <příkaz>.
Výstup je ve výchozím stavu česká věta pro člověka. Když chcete dávku zpracovat skriptem, přidejte --json:
# Vypíše soubory, ze kterých se doklad nevytěžil, i s důvodem.
ctenifaktur --json status BATCH_ID \
| jq -r '.uploads[] | select(.errorCode) | "\(.fileName) \(.errorCode)"'
S --json je na standardním výstupu právě jeden JSON dokument a nic jiného, průběh a varování jdou na chybový výstup. Výjimka je jen --help, ten zůstává větami a odchází celý na chybový výstup. U čtení a nahrávání je obsahem tatáž odpověď, kterou vrací API výše, takže documents vypíše stránku metadat a status s upload celou dávku i s id vzniklých dokladů. U exportu je v dokumentu jméno uloženého souboru. Chyba je v tomhle režimu taky JSON, a to tatáž obálka jako u API (viz Časté chyby níž). Export odmítnutý kvůli mixed_accounting_units tak jde rozdělit podle details, ne podle textu. Návratové kódy se nemění. Samotná vytěžená data z dokladů --json nevypisuje, ta z aplikace odcházejí jen exportem do účetního formátu.
Píšete kód v Claude Code, Cursoru nebo Codexu? K CLI patří skill, po jehož instalaci agent ví, jak nástroj ovládat, a nahrání i export udělá sám:
npx skills@latest add ctenifaktur/cli
Příkaz se zeptá, do kterých agentů skill nainstalovat. Už spuštěné sezení ho samo nenačte, v Claude Code na to stačí /reload-skills, jinde agenta restartujte. Zdrojový kód CLI i skillu je na GitHubu.
Kompletní popis endpointů
Všechny endpointy včetně parametrů, odpovědí a chybových kódů najdete v dokumentaci API. U každého je ukázka volání v curl, JavaScriptu, Pythonu a dalších jazycích a panel, ve kterém požadavek rovnou vyzkoušíte. Ten je ale anglický.
Napojení pro vás dělá někdo, kdo neumí česky? Pošlete mu rovnou anglickou verzi. Je to tatáž dokumentace, jen anglicky.
Pro nástroje je tentýž popis ke stažení strojově:
https://ctenifaktur.cz/api/v1/openapi.json
https://ctenifaktur.cz/api/v1/openapi.json?lang=en
Je to standardní OpenAPI dokument, takže si ho naimportujete do Postmanu nebo Insomnie a máte hotovou kolekci se všemi endpointy. Generátory klientů z něj vyrobí kód ve vašem jazyce. Žádná z adres nevyžaduje klíč, popis si tedy prohlédnete ještě dřív, než API začnete používat.
Nástroje, které si popis hledají samy, ho najdou i na dvou obvyklých adresách, https://ctenifaktur.cz/openapi.json a https://ctenifaktur.cz/.well-known/openapi.json. Je to tentýž dokument, jen rovnou anglicky, protože na tyhle adresy chodí generátory a agenti, ne čtenáři. Když z nich potřebujete češtinu, přidejte ?lang=cs.
Pozor na jednu věc: přeložená je dokumentace, ne API samotné. Chybové hlášky v poli message zůstávají české. Programy se ale mají řídit polem code, které je anglické a stabilní.
Nahrání dokladu
Nahrání má jeden endpoint, POST /api/v1/documents, a funguje stejně pro jeden soubor i pro dávku, pro malý sken i pro velký. Dělit velké soubory na části nemusíte, jen se vejděte do 25 MB na soubor.
POST /api/v1/documentspošlete popis souborů (název, typ, velikost). VrátíbatchIda pro každý soubor adresu.- Na tuto adresu pošlete obsah souboru metodou
PUT. Žádné hlavičky nepotřebujete.
Tím to končí. Že soubor dorazil, si všimneme sami a zpracování se rozjede do minuty. Když vám program spadne hned po odeslání souboru, o doklad ani o kredit nepřijdete.
Soubor jde takhle až do 25 MB a jedna dávka pobere až 300 souborů.
Adresa platí 15 minut. Když PUT selže, pošlete ho na tutéž adresu znovu; opakování je pro nás k nerozeznání od pomalého prvního pokusu. Soubor, který se nenahraje vůbec, zhruba po půlhodině zahodíme a kredit vrátíme.
Doklad můžete rovnou zařadit do účetní jednotky, ale nemusíte. Bez ní zůstane nezařazený a přiřadíte ho v aplikaci. Když ho chcete zařadit hned, id jednotky zjistíte z GET /api/v1/accounting-units.
Dříve nahrané doklady
Když už neznáte číslo dávky, zavolejte GET /api/v1/documents. Vrátí uložené doklady od nejnovějších, včetně těch, které jste nahráli dříve v aplikaci. Výsledek stránkujete parametry page a limit, na jednu účetní jednotku ho omezíte parametrem accountingUnitId.
Odpověď obsahuje id, název souboru, stav zpracování, zařazení do účetní jednotky, datum nahrání a informaci, zda je doklad v archivu. Neobsahuje dodavatele, částky ani položky. Vytěžená data veřejné API nevydává, podle id doklad vyexportujete do účetního formátu.
Sledování zpracování
Zpracování běží na pozadí a u jednoho dokladu trvá zpravidla desítky sekund. Stav zjistíte přes GET /api/v1/batches/{id}. Ptejte se opakovaně, dokud je stav pending nebo processing. Konečné stavy jsou tři: completed, completed_with_failures a failed. Kterýkoli z nich znamená konec čekání.
Z jednoho nahraného souboru může vzniknout více dokladů, když v něm rozpoznáme několik faktur pod sebou. Proto odpověď u každého nahrání vrací seznam vzniklých dokladů, ne jeden doklad.
Hotové nahrání přitom ještě neznamená, že z něj vzniklo všechno. Když soubor obsahoval více dokladů, než se stihlo zpracovat nebo vytěžit, přibude u něj pole incomplete s počty: discarded jsou doklady, které jsme v souboru rozpoznali, ale nezpracovali, protože došly kredity nebo soubor přesáhl limit na jedno nahrání, unparsed doklady, ze kterých se nepodařilo nic vytěžit. V documentIds jsou vždy jen doklady, které jdou rovnou do exportu.
Když se část dávky nepovede, zbytek se zpracuje normálně. Dávka pak skončí ve stavu completed_with_failures a neúspěšné položky poznáte podle stavu u konkrétního souboru. Není to důvod posílat celou dávku znovu, hotové doklady se exportují běžným způsobem.
Stažení exportu
POST /api/v1/documents/export vrátí hotový soubor, ne odkaz. Podporuje formáty isdoc, pohoda a money-s3. Pohoda i Money S3 vracejí vždy jedno XML, ISDOC u jednoho dokladu soubor .isdoc a u více dokladů ZIP.
Jeden export musí obsahovat doklady z jedné účetní jednotky. Výsledný soubor totiž nese jedno IČO a smíchané doklady by se do účetního programu naimportovaly špatně. Když pošlete doklady z více jednotek, vrátíme chybu mixed_accounting_units se seznamem, který doklad patří kam, abyste je mohli rozdělit.
Totéž platí pro doklady, které jednotku nemají: musí patřit jedné firmě. Jinak by soubor dostal IČO prvního z nich a zbylé doklady by skončily v účetnictví cizí firmy, takže je odmítneme stejnou chybou. V seznamu je u každého dokladu i jeho IČO, podle kterého je rozdělíte.
Nastavení se bere z účetní jednotky dokladu, ne z požadavku. Export přes API tak vrátí stejný soubor jako stažení z aplikace.
Přes API nevydáme doklad, kde sazbu DPH nese jen část položek: doplnit ji za vás nejde a zbylé položky by odešly s nulovou sazbou, takže by se do účetnictví přenesla jiná daň, než jakou doklad sám uvádí. Ostatní rozpory mezi sazbami na řádcích a rekapitulací export nezastavují, upozorní na ně aplikace před stažením souboru. Vrátíme invalid_request a v details.documents seznam dokladů s popisem, co jim chybí; sazby doplňte v aplikaci a export zopakujte. V aplikaci na totéž upozorňuje hláška před stažením, kterou tam jde odklepnout, integrace ale žádnou obrazovku nemá. Objednávek se to netýká u Pohody a Money S3: tam skončí v objednávkové agendě, ze které se nic neúčtuje. Do ISDOCu odejde tatáž objednávka jako faktura, takže u něj odmítnutí platí dál a availableFormats vrátí jen formáty, které tahle blokace nevyřazuje. Je to předběžná kontrola, ne záruka: generátor může doklad odmítnout i z jiného důvodu.
Bankovní výpisy
Výpisy mají vlastní endpointy, ne přepínač u dokladů:
| Co chcete | Endpoint |
|---|---|
| Nahrát výpis | POST /api/v1/bank-statements |
| Zjistit stav | GET /api/v1/batches/{id} (stejný jako u dokladů) |
| Metadata výpisu | GET /api/v1/bank-statements/{id} |
| Stáhnout GPC nebo SEPA XML | POST /api/v1/bank-statements/export |
Nahrání funguje úplně stejně jako u dokladů: pošlete popis souborů, na vrácenou adresu odešlete obsah metodou PUT a dál se nic potvrzovat nemusí. Přijímáme PDF, obrázek a CSV report z platební brány. ISDOC ani XML výpisem být nemůže, ty patří na /documents.
Výpis poslaný na POST /api/v1/documents nezastavíme, ale zpracuje se jako faktura od banky, prakticky vždy s nulovými částkami. Adresa endpointu je zároveň deklarace, čím soubor je, tak si ji ohlídejte. Když si toho všimneme z vytěženého obsahu, dá GET /api/v1/documents/{id} v poli warnings hodnotu looks_like_bank_statement. Upozornění zmizí, jakmile ho někdo v aplikaci odklikne křížkem u hlášky (Zpracovat jako fakturu), tedy potvrdí, že jde opravdu o fakturu. Platí to ale až od chvíle, kdy se odkliknutí uloží na doklad, takže členovi bez práva doklady upravovat se upozornění vrátí.
Stav se sleduje jedním endpointem pro oba druhy. GET /api/v1/batches/{id} proto vrací pole kind s hodnotou documents, nebo bank-statements, podle toho, kam id z documentIds patří.
Export bere statementIds (ne documentIds) a formát gpc nebo sepa-xml. Více výpisů dá v obou případech jeden soubor. Na rozdíl od dokladů se výpisy nemusí shodovat v účetní jednotce, každý si v souboru nese vlastní hlavičku s vlastním účtem. Formát čísla účtu se bere z nastavení v aplikaci, takže API vydá stejný soubor jako stažení z aplikace.
Které formáty u konkrétního výpisu projdou, řekne availableFormats v GET /api/v1/bank-statements/{id}. Liší se to častěji, než by člověk čekal. Výpis, jehož číslo účtu nejde převést na IBAN, projde do GPC, ale ne do SEPA XML. A výpis, který číslo účtu nemá vůbec, neprojde ani do jednoho: availableFormats je prázdné pole a export skončí chybou invalid_request s polem header.accountNumber.
To je případ každého CSV reportu z platební brány: brána číslo účtu netiskne, takže ho výpis nemá odkud vzít. Nahrání se přitom povede a kredit se strhne. Účet doplníte jen v aplikaci, tlačítkem „Doplnit účet“ u výpisu; přes API to nejde. Sestavu z brány tedy posílejte přes API jen tehdy, když ji někdo v aplikaci doplní, jinak z ní export nedostanete.
Kolik to stojí
Doklad stojí jeden kredit za každý vytěžený doklad. Výpis stojí jeden kredit za každé započaté tři strany, CSV report z platební brány jeden kredit.
Zůstatek si přečtete kdykoli přes GET /api/v1/credits. Vrátí, kolik dokladů ještě projde, rozpadlé na tarifní kvótu a kredity, protože kvóta se k začátku dalšího období obnoví a kredity ne. U výpisů je to odhad, viz níž.
Počet stran zjistíme až při zpracování, takže nahrání zatím konečnou cenu nezná: POST /api/v1/bank-statements ověří jen to, že máte aspoň na ten nejlevnější výsledek. Když kredity dojdou až u konkrétního souboru, dostane v dávce errorCode: "insufficient_credits" a nic se za něj neúčtuje.
Opakované odeslání
Když spojení selže a vy požadavek zopakujete, může se stát, že první pokus ve skutečnosti prošel. Aby se doklad nezpracoval dvakrát a nestrhly se kredity dvakrát, pošlete u nahrávání hlavičku:
Idempotency-Key: vaše-jedinečné-id
Při opakování použijte stejný klíč. Vrátíme výsledek prvního pokusu místo toho, abychom doklad zpracovali znovu. Nástroje jako n8n a Make opakují požadavky samy, takže tuhle hlavičku doporučujeme používat vždy.
Odpověď vracíme doslova, tedy i s původními adresami pro nahrání. Ty platí 15 minut, takže na opakování během pár minut, kvůli kterému hlavička existuje, to nemá vliv. Když se k požadavku vrátíte až za hodinu, adresy už soubor nepřijmou; založte nahrání znovu s novým klíčem. Kredit z prvního pokusu se vrátí sám.
Časté chyby
| Kód | Co s tím |
|---|---|
invalid_key | Klíč je špatně opsaný, zneplatněný nebo jeho tvůrce už do kanceláře nemá přístup. |
insufficient_scope | Klíč nemá potřebné oprávnění, například klíč jen pro čtení nemůže nahrávat a klíč na doklady nedosáhne na výpisy. |
insufficient_credits | Došly kredity. Doplňte je a pošlete požadavek znovu. |
mixed_accounting_units | Export míchá doklady z více jednotek, rozdělte ho po jednotkách. |
file_too_large | Soubor je nad 25 MB, takový nezpracujeme. |
rate_limited | Příliš mnoho požadavků. Počkejte podle hlavičky Retry-After. |
Chybu vracíme vždycky jako JSON ve tvaru {"error":{"code","message"}}, i když se spletete v cestě (not_found) nebo v metodě (method_not_allowed). Zavolat na chybovou odpověď res.json() je tedy bezpečné. U některých chyb je navíc pole details s rozpadem, podle kterého se chyba spraví: u mixed_accounting_units je v něm každý doklad se svou jednotkou a IČO, u odmítnutého souboru jeho jméno.
Proč se soubor nezpracoval
GET /api/v1/batches/{id} vrací u každého souboru pole uploads[].errorCode. Nejde o chybu požadavku, ale o důvod, proč neprošel jeden konkrétní soubor z dávky. Ostatní soubory běží dál.
| Kód | Co s tím |
|---|---|
upload_not_received | Bajty do úložiště nikdy nedorazily. Kredit jsme vrátili, soubor pošlete znovu. |
source_missing | Nahraný soubor se nepodařilo stáhnout. Pošlete ho znovu. |
source_rejected | Soubor je nad 25 MB nebo neodpovídá tomu, co jste ohlásili. Kredit vracíme, opakovat tentýž soubor nemá smysl. |
insufficient_credits | Jen u výpisů. Cena se počítá podle stran a ta je známá až při zpracování. Neúčtujeme nic, doplňte kredit a nahrajte výpis znovu. |
processing_failed | Vytěžení selhalo. Zkuste to znovu, a pokud selže i podruhé, napište nám. |
Seznam se může časem rozšířit. Kód, který v tabulce nenajdete, berte jako processing_failed, ať vám nová hodnota nerozbije integraci.
Verzování a ukončování
Cesta začíná /v1 a uvnitř téhle verze přibývá jen to, co nic nerozbije: nová pole v odpovědi, nové endpointy, nové hodnoty code. Výčet kódů v popisu API je seznam těch dnešních, ne uzavřená množina: neznámý kód berte jako obecné selhání a větvete jen na ty, které znáte. Integrace, která si čte jen to, co potřebuje, a na neznámou hodnotu nespadne, tedy vydrží.
Kdyby některý endpoint měl skončit, řekne to sám. Jeho odpovědi začnou nosit hlavičku Deprecation s datem, kdy jsme ho označili za zastaralý, a Sunset s datem, kdy přestane odpovídat. V popisu API bude navíc označený jako deprecated. Mezi oběma daty necháme aspoň šest měsíců, aby bylo kdy přejít. Dnes není zastaralé nic.