← Powrót na blog

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:

  1. 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.
  2. 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:

  1. Faktura jest zapisywana do bazy z idempotency key.
  2. Background worker pobiera ją i wysyła przez anti-corruption layer.
  3. Retry i rate limiting są w polityce Polly.
  4. Polling UPO działa osobno.
  5. Job reconciliacji weryfikuje ogólny stan.
  6. Każde przejście stanu generuje strukturyzowany event w audit logu.
  7. 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.

Masz podobny problem? Napisz do nas.

Umów konsultację