KSeF API v .NET: nejčastější chyby a jak je ošetřit
KSeF je asynchronní, rate-limited, stavový státní systém. Tady jsou kategorie chyb, na které narážíme v .NET integracích, a konkrétní vzory, jak je ošetřit.
KSeF je asynchronní, rate-limited a má vlastní session a stav. To jsou čtyři vlastnosti, které běžné REST API nemá najednou. Výsledek: integrace, která prošla testem, selže v produkci způsobem, který není vidět — dokud nepřijde kontrola.
Tady jsou kategorie chyb, na které narazíme v .NET integracích nejčastěji, a vzory, jak každou z nich ošetřit. Není to výčet z dokumentace — je to seznam problémů, které jsme reálně viděli a museli řešit.
1. Session expirace — tichá smrt
KSeF session má omezenou platnost. Problém nastane, když aplikace session vytvoří, uloží token do paměti a předpokládá, že platí donekonečna. Při prvním volání po expiraci dopadne request chybou autorizace, retry ho zopakuje s tím samým token a cyklus pokračuje.
Vzor ošetření: session token nikdy neukládat jen v paměti. Platnost tokenu se ověřuje před každým voláním, ne jednou při startu. Refresh proběhne proaktivně — ne až po první chybě. Stav session (token + expirační čas) leží v DB nebo sdíleném cache, aby ho viděly všechny instance aplikace.
2. Rate limit — HTTP 429 přijde vždy ve špatný čas
KSeF omezuje počet requestů za časovou jednotku. Při dávkovém odeslání faktur na konci měsíce se na tento limit narazí spolehlivě. Bez ošetření se odeslání zastaví uprostřed dávky a část faktur se nikam nedostane.
Vzor ošetření v .NET: Polly RateLimiter policy nebo vlastní token-bucket. Klíčové jsou dvě věci:
- Rate limiter musí být sdílený stav — in-memory v jednom procesu nestačí, pokud běžíte na víc instancích. Sdílený token-bucket přes Redis zajistí, že instance nepřebíjejí navzájem kvótu.
- HTTP 429 response musí vrátit request zpět do fronty, ne skončit chybou. Chybová odpověď KSeF obvykle obsahuje informaci, jak dlouho počkat — tento interval respektujeme.
3. Retry bez idempotence = duplikáty ve státním systému
Přechodná chyba, timeout nebo restart aplikace. Aplikace retry pošle fakturu znovu. KSeF ji přijme jako novou. Výsledek: jedna faktura v účetnictví, dvě v KSeF.
To je problém, který se špatně opravuje zpětně. Polský finanční úřad faktury v KSeF nezkouší srovnávat s vaším ERP — narazíte na to až při auditu.
Vzor ošetření: každá faktura dostane deterministický idempotency key před prvním odesláním. Key vychází z dat faktury (číslo, datum, IČO) — ne z náhodného UUID. Když se odeslání zopakuje, systém klíč rozpozná a neodešle fakturu podruhé. KSeF vlastní idempotency mechanismus na úrovni API nemá — tuto logiku musíte implementovat sami na straně integrační vrstvy.
4. Validační chyby faktury — příliš pozdě odhalené
KSeF odmítne fakturu, která neodpovídá FA(2) schématu nebo porušuje jeho business pravidla. Odmítnutí přijde asynchronně — ne ihned při odeslání, ale až při polling výsledku. To znamená, že chyba validace je odložená: odeslání projde, fakturu zapíšeme jako „odesláno" a teprve při dalším pollingu zjistíme, že je odmítnuta.
Kategorie, které potkáváme nejčastěji: nesprávný formát DIČ (NIP), chybějící nebo nevalidní časová razítka, nekonzistentní DPH sazby, nebo nesprávná struktura řádků faktury.
Vzor ošetření: validace před odesláním na straně .NET kódu, ne jen spoléhání na odpověď KSeF. Schéma FA(2) je dostupné jako XSD — validujeme XML dokument před odesláním do API. Odmítnutí ze strany KSeF zachytíme při pollingu, zapíšeme do audit logu a vyvolá alert, ne jen LogWarning.
5. Čekání na UPO — polling, který nikdo nenapsal
Odeslání faktury je krok 1. KSeF zpracování je asynchronní — výsledek (UPO, Urzędowe Poświadczenie Odbioru) přijde až po určité době. Bez aktivního pollingu se stav faktury nikdy neaktualizuje. Faktura leží ve stavu „odesláno" a nikdo neví, jestli ji stát přijal.
Vzor ošetření: po každém odeslání se stav faktury zapíše do DB jako Pending. Background job periodicky polluje KSeF na výsledek. Jakmile dorazí UPO (nebo odmítnutí), stav se aktualizuje a uloží. Teprve UPO je důkaz. Faktura bez UPO není hotová faktura.
6. Timeout a výpadek státního systému — bez fronty to nesejde
KSeF má výpadky. Není to hypotéza — je to fakt, který platí pro každý státní systém. Při synchronním volání v HTTP requestu timeout nebo výpadek = ztracená faktura. Aplikace vyhodí výjimku, uživatel vidí chybu, faktura nikam nedorazila.
Vzor ošetření: každé volání KSeF probíhá asynchronně mimo HTTP request. Faktura se nejdřív zapíše do DB jako úloha k odeslání. Background worker ji vyzvedne a odešle. Při výpadku KSeF se úloha nechá ve frontě a zopakuje po uplynutí backoff intervalu. Polly RetryPolicy s exponenciálním backoffem + jitter zajistí, že se instance nezahltí při obnovení dostupnosti.
// Příklad Polly policy s exponenciálním backoffem
var retryPolicy = Policy
.Handle<KSeFApiException>(ex => ex.IsTransient)
.WaitAndRetryAsync(
retryCount: 5,
sleepDurationProvider: attempt =>
TimeSpan.FromSeconds(Math.Pow(2, attempt))
+ TimeSpan.FromMilliseconds(Random.Shared.Next(0, 500)));
7. Chybějící reconciliation — nevíte co nevíte
Toto je díl, který každá první integrace vynechá. A je to ten nejnebezpečnější výpadek — ne proto, že selže hlučně, ale proto, že selže tiše.
Reconciliation job se ptá jednou denně (nebo jednou za hodinu): „Jsou všechny faktury v konzistentním stavu?" Má každá odeslaná faktura buď UPO, nebo zdůvodněné odmítnutí? Neuvízla žádná v Pending déle, než je rozumné čekat? Nevznikl orphan — dokument, který odešel do KSeF, ale ztratil se v naší DB?
Bez reconciliation spoléháme na to, že všechno proběhlo správně. V distribuovaném systému to není dost.
8. Bez anti-corruption layer SDK ovlivní celý kód
KSeF .NET SDK má své zvláštnosti — vlastní error stringy, numerické kódy, session model, který se mění mezi verzemi. Pokud volání SDK rozsypete napříč celou aplikací, každá změna SDK je refaktor celého projektu.
Vzor ošetření: SDK obalte do vlastní vrstvy (anti-corruption layer). Tato vrstva překládá KSeF koncepty (session, UPO, odmítnutí) na vaše doménové typy. Zbytek aplikace nezná SDK — zná jen váš interface. Změna SDK = změna jednoho souboru.
Jak to dohromady funguje
Výsledná architektura není složitá, ale musí mít všechny části:
- Faktura se zapíše do DB s idempotency key.
- Background worker ji vyzvedne a odešle přes anti-corruption layer.
- Retry + rate limiting jsou v Polly policy.
- Polling na UPO běží zvlášť.
- Reconciliation job kontroluje celkový stav.
- Každý přechod stavu generuje strukturovaný event v audit logu.
- Na terminal-failure a stuck stavy jde alert.
Tohle není over-engineering. Je to minimální architektura pro systém, který je asynchronní, rate-limited a občas nedostupný.
Ověřeno v produkci
V produkci zpracováváme přes 40 000 dokumentů. Při forenzní recovery jsme obnovili 15 141 faktur, které předchozí fire-and-forget pipeline tiše ztratila. Každá z výše popsaných kategorií chyb odpovídá reálnému incidentu z té obnovy.
KSeF integrace, která přežije konec měsíce bez ztráty dokladů, vypadá přesně takhle.
KSeF integraci v .NET stavíme — od jednoduché napojení přes API až po produkční robustní pipeline s durable stavem, reconciliation a alertingem. Napište nám — řekneme vám, kde je vaše integrace zranitelná.
Časté otázky
Potřebujeme opravdu reconciliation job, nebo stačí retry?
Retry ošetří přechodné chyby při odeslání. Reconciliation řeší stav celé dávky z pohledu celého systému — stuck záznamy, orphany, chyby, které prošly přes retry ale skončily v termination state bez alertu. Jedno nahrazuje druhé. Bez reconciliation zjistíte ztrátu faktury až při auditu.
Jak velký problém je sdílený rate limiter, pokud máme jen jednu instanci?
Při jedné instanci stačí in-memory. Problém nastane ve chvíli, kdy přidáte druhou instanci pro vysokou dostupnost nebo load balancing — pak oba procesy konzumují kvótu nezávisle a součet snadno překročí limit KSeF. Sdílený limiter (Redis) je bezpečnější výchozí bod než zpětná migrace, když narazíte na 429 v noci.
Co se stane, když KSeF odmítne fakturu s validační chybou a my ji znovu odešleme se stejným idempotency key?
Idempotency key chrání před duplicitami při úspěšném průchodu. Odmítnutou fakturu je třeba opravit a odeslat znovu — s novým klíčem, protože obsah faktury se změnil. Klíč by měl vycházet z dat faktury, takže opravená verze automaticky dostane jiný klíč.