KSeF API w .NET: najczęstsze błędy i jak je obsłużyć
KSeF jest asynchroniczny, limitowany i stanowy. Oto kategorie błędów, na które napotykamy w integracjach .NET, i konkretne wzorce, jak je obsłużyć.
KSeF jest asynchroniczny, limitowany i ma własną sesję oraz stan. To cztery właściwości, których zwykłe REST API nie ma naraz. Efekt: integracja, która przeszła testy, pada na produkcji w sposób niewidoczny — dopóki nie przyjdzie kontrola skarbowa.
Oto kategorie błędów, na które najczęściej napotykamy w integracjach .NET, i wzorce obsługi każdej z nich. To nie jest wykaz z dokumentacji — to lista problemów, które realnie widzieliśmy i musieliśmy rozwiązać.
1. Wygaśnięcie sesji — cicha śmierć
Sesja KSeF ma ograniczony czas ważności. Problem pojawia się, gdy aplikacja tworzy sesję, zapisuje token w pamięci i zakłada, że jest ważny bezterminowo. Przy pierwszym wywołaniu po wygaśnięciu request kończy się błędem autoryzacji, retry powtarza go z tym samym tokenem i cykl trwa dalej.
Wzorzec obsługi: token sesji nigdy nie trafia tylko do pamięci. Ważność tokenu weryfikujemy przed każdym wywołaniem, nie jednorazowo przy starcie. Odświeżenie następuje proaktywnie — nie dopiero po pierwszym błędzie. Stan sesji (token + czas wygaśnięcia) leży w bazie danych lub współdzielonym cache, żeby widziały go wszystkie instancje aplikacji.
2. Rate limiting — HTTP 429 zawsze przychodzi w złym momencie
KSeF ogranicza liczbę requestów na jednostkę czasu. Przy wsadowym wysyłaniu faktur na koniec miesiąca natrafi się na ten limit niezawodnie. Bez obsługi wysyłka zatrzymuje się w połowie partii i część faktur nigdzie nie dociera.
Wzorzec obsługi w .NET: Polly RateLimiter policy lub własny token-bucket. Kluczowe są dwie rzeczy:
- Rate limiter musi być stanem współdzielonym — in-memory w jednym procesie nie wystarczy, gdy działasz na wielu instancjach. Współdzielony token-bucket przez Redis zapewni, że instancje nie „kradną" sobie nawzajem kwoty.
- Odpowiedź HTTP 429 musi cofnąć request do kolejki, a nie zakończyć się błędem. Odpowiedź błędu KSeF zazwyczaj zawiera informację, jak długo czekać — szanujemy ten interwał.
3. Retry bez idempotencji = duplikaty w systemie państwowym
Przejściowy błąd, timeout lub restart aplikacji. Aplikacja wysyła fakturę ponownie przez retry. KSeF przyjmuje ją jako nową. Wynik: jedna faktura w systemie księgowym, dwie w KSeF.
To problem, który trudno naprawić z mocą wsteczną. Polskie organy podatkowe nie próbują uzgadniać faktur w KSeF z twoim ERP — odkryjesz to dopiero podczas audytu.
Wzorzec obsługi: każda faktura otrzymuje deterministyczny idempotency key przed pierwszym wysłaniem. Klucz wynika z danych faktury (numer, data, NIP) — nie z losowego UUID. Gdy wysyłka zostaje powtórzona, system rozpoznaje klucz i nie wysyła faktury po raz drugi. KSeF nie ma własnego mechanizmu idempotentności na poziomie API — tę logikę musisz zaimplementować samodzielnie po stronie warstwy integracyjnej.
4. Błędy walidacji faktury — wykryte za późno
KSeF odrzuca fakturę, która nie odpowiada schematowi FA(2) lub narusza jego reguły biznesowe. Odrzucenie przychodzi asynchronicznie — nie od razu przy wysłaniu, lecz dopiero przy pollingu wyniku. Oznacza to, że błąd walidacji jest odroczony: wysłanie przechodzi, zapisujemy fakturę jako „wysłana" i dopiero przy kolejnym pollingu dowiadujemy się, że jest odrzucona.
Kategorie, na które napotykamy najczęściej: nieprawidłowy format NIP, brakujące lub nieważne znaczniki czasu, niespójne stawki VAT lub nieprawidłowa struktura wierszy faktury.
Wzorzec obsługi: walidacja przed wysłaniem po stronie kodu .NET, a nie wyłączne poleganie na odpowiedzi KSeF. Schemat FA(2) jest dostępny jako XSD — walidujemy dokument XML przed wysłaniem do API. Odrzucenie ze strony KSeF wychwytujemy przy pollingu, zapisujemy do audit logu i wyzwalamy alert, nie tylko LogWarning.
5. Oczekiwanie na UPO — polling, którego nikt nie napisał
Wysłanie faktury to krok 1. Przetwarzanie przez KSeF jest asynchroniczne — wynik (UPO, Urzędowe Poświadczenie Odbioru) pojawia się po pewnym czasie. Bez aktywnego pollingu stan faktury nigdy się nie aktualizuje. Faktura leży w stanie „wysłana" i nikt nie wie, czy urząd ją przyjął.
Wzorzec obsługi: po każdym wysłaniu stan faktury zapisujemy do bazy jako Pending. Background job periodycznie polluje KSeF w poszukiwaniu wyniku. Gdy pojawi się UPO (lub odrzucenie), stan jest aktualizowany i zapisywany. Dopiero UPO jest dowodem. Faktura bez UPO nie jest gotową fakturą.
6. Timeout i awaria systemu państwowego — bez kolejki się nie uda
KSeF ma awarie. To nie jest hipoteza — to fakt, który dotyczy każdego systemu państwowego. Przy synchronicznym wywołaniu w ramach żądania HTTP timeout lub awaria oznaczają utraconą fakturę. Aplikacja rzuca wyjątek, użytkownik widzi błąd, faktura nigdzie nie dotarła.
Wzorzec obsługi: każde wywołanie KSeF odbywa się asynchronicznie poza żądaniem HTTP. Faktura jest najpierw zapisywana do bazy jako zadanie do wysłania. Background worker pobiera ją i wysyła. Przy awarii KSeF zadanie zostaje w kolejce i jest ponawiane po upływie interwału backoff. Polly RetryPolicy z exponential backoffem i jitterem zapewnia, że instancje nie zostają zalane przy przywróceniu dostępności.
// Przykład polityki Polly z exponential 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. Brak reconciliacji — nie wiesz, czego nie wiesz
To element, który każda pierwsza integracja pomija. I jest to najniebezpieczniejsza awaria — nie dlatego, że zawodzi głośno, lecz dlatego, że zawodzi cicho.
Job reconciliacji pyta raz dziennie (lub raz na godzinę): „Czy wszystkie faktury są w spójnym stanie?" Czy każda wysłana faktura ma UPO albo uzasadnione odrzucenie? Czy żadna nie utknęła w stanie Pending dłużej, niż to uzasadnione? Czy nie powstał sierota — dokument wysłany do KSeF, który zaginął w naszej bazie?
Bez reconciliacji liczymy na to, że wszystko przebiegło prawidłowo. W systemie rozproszonym to za mało.
8. Bez anti-corruption layer SDK wpływa na cały kod
SDK KSeF dla .NET ma swoje osobliwości — własne ciągi błędów, kody numeryczne, model sesji zmieniający się między wersjami. Jeśli wywołania SDK rozproszysz po całej aplikacji, każda zmiana SDK to refaktor całego projektu.
Wzorzec obsługi: opakuj SDK we własną warstwę (anti-corruption layer). Ta warstwa tłumaczy koncepty KSeF (sesja, UPO, odrzucenie) na twoje typy domenowe. Reszta aplikacji nie zna SDK — zna tylko twój interfejs. Zmiana SDK = zmiana jednego pliku.
Jak to razem działa
Wynikowa architektura nie jest skomplikowana, ale musi mieć wszystkie elementy:
- Faktura jest zapisywana do bazy z idempotency key.
- Background worker pobiera ją i wysyła przez anti-corruption layer.
- Retry i rate limiting są w polityce Polly.
- Polling UPO działa osobno.
- Job reconciliacji weryfikuje ogólny stan.
- Każde przejście stanu generuje strukturyzowany event w audit logu.
- Na błędy terminalne i stany zawieszone wychodzi alert.
To nie jest over-engineering. To minimalna architektura dla systemu, który jest asynchroniczny, limitowany i bywa niedostępny.
Zweryfikowane produkcyjnie
Na produkcji przetwarzamy ponad 40 000 dokumentów. Podczas forensic recovery odtworzyliśmy 15 141 faktur, które poprzedni pipeline fire-and-forget po cichu utracił. Każda z opisanych powyżej kategorii błędów odpowiada realnemu incydentowi z tej odbudowy.
Integracja KSeF, która przeżyje koniec miesiąca bez utraty dokumentów, wygląda dokładnie tak.
Budujemy integracje KSeF w .NET — od prostego podłączenia przez API po produkcyjny, solidny pipeline z durable stanem, reconciliją i alertingiem. Napisz do nas — powiemy ci, gdzie twoja integracja jest narażona.
Najczęstsze pytania
Czy naprawdę potrzebujemy job reconciliacji, czy wystarczy retry?
Retry obsługuje przejściowe błędy przy wysyłaniu. Reconciliacja rozwiązuje stan całej partii z perspektywy całego systemu — zawieszone rekordy, sieroty, błędy, które przeszły przez retry, ale zakończyły się w stanie terminalnym bez alertu. Jedno nie zastępuje drugiego. Bez reconciliacji dowiesz się o utracie faktury dopiero podczas audytu.
Jak poważny jest problem ze współdzielonym rate limiterem, gdy mamy tylko jedną instancję?
Przy jednej instancji wystarczy in-memory. Problem pojawia się w chwili, gdy dodasz drugą instancję dla wysokiej dostępności lub load balancingu — oba procesy konsumują wtedy kwotę niezależnie, a suma łatwo przekracza limit KSeF. Współdzielony limiter (Redis) to bezpieczniejszy punkt wyjścia niż migracja wstecz, gdy napotkasz 429 w nocy.
Co się stanie, gdy KSeF odrzuci fakturę z błędem walidacji i wyślemy ją ponownie z tym samym idempotency key?
Idempotency key chroni przed duplikatami przy udanym przejściu. Odrzuconą fakturę trzeba poprawić i wysłać ponownie — z nowym kluczem, bo treść faktury się zmieniła. Klucz powinien wynikać z danych faktury, więc poprawiona wersja automatycznie otrzymuje inny klucz.