Fide-Soft
Architektura i wdrożenia

Integracja KSeF dla e-commerce: architektura, idempotencja i co robić, gdy KSeF nie odpowiada

Jak zbudować integrację KSeF między sklepem, Base (BaseLinker) i ERP, która nie wyśle faktury dwa razy: maszyna stanów dokumentu, outbox, klucz idempotencji, retry, tryb offline, sekrety i monitoring.

· 9 min czytania

Wysłanie faktury do KSeF to jedno wywołanie API. Integracja, która działa przez trzy lata bez podwójnych faktur, to kilka decyzji projektowych, które trzeba podjąć przed pierwszą linią kodu. Poniżej te decyzje, w kolejności, w jakiej zwykle bolą.

Tekst techniczny. Co jest fakturą, kiedy wolno wystawić w trybie offline i jakie są terminy, rozstrzyga księgowość i aktualne przepisy. Interfejs KSeF zmienia się między wersjami, więc nazwy operacji traktujcie jako klasy operacji, nie dokładne endpointy.

Co integracja ma robić

Przy sprzedaży przez Base (BaseLinker) i ERP (Subiekt, Comarch Optima) sam transport do KSeF zwykle robi ERP swoim modułem. Integracja odpowiada za wszystko dookoła:

  1. Przenosi zamówienie z Base do ERP jako dokument, z danymi nabywcy i kanałem.
  2. Decyduje, czy do zamówienia powstaje faktura, paragon, czy nic (reguła po danych, nie po uwagach).
  3. Waliduje dane przed wystawieniem: NIP z sumą kontrolną, stawki VAT, kompletność adresu, zgodność ze schematem.
  4. Pilnuje, żeby jedna sprzedaż dała jeden dokument, niezależnie od tego, ile razy zdarzenie przyszło.
  5. Odbiera z ERP numer faktury, a po wysyłce numer KSeF, UPO i status, i zapisuje je w zamówieniu.
  6. Obsługuje korekty tą samą drogą (zwrot w Base → korekta w ERP → KSeF → status w Base).
  7. Rozlicza się codziennie: każda faktura z ERP ma numer KSeF albo jest na liście wyjątków.
  8. Alarmuje ludzi, gdy coś wisi za długo albo zostało odrzucone.

Jeśli wysyłkę do KSeF robi własny moduł (bez ERP, albo z kilku systemów naraz), dochodzi punkt 9: sesja, podpis, wysyłka, odbiór UPO. Reszta zostaje bez zmian.

Dokument jako maszyna stanów

Największy błąd to trzymanie faktury jako „wysłana: tak/nie”. Między „nie” a „tak” jest kilka stanów, a każdy ma inną obsługę błędu.

stateDiagram-v2
  direction TB
  [*] --> Draft
  Draft --> Validated: walidacja OK
  Draft --> Blocked: brak danych
  Blocked --> Draft: obsługa poprawia
  Validated --> Issued: ERP nadał numer
  Issued --> Queued: wpis do outbox
  Queued --> Sending: worker, lock
  Sending --> Pending: przyjęto (numer ref.)
  Sending --> Queued: timeout, sprawdź status
  Sending --> Offline: KSeF niedostępny
  Offline --> Queued: KSeF wrócił
  Pending --> Accepted: numer KSeF + UPO
  Pending --> Rejected: odrzucono
  Rejected --> Draft: poprawa, nowa próba
  Accepted --> [*]
Każde przejście zapisuje się w bazie z czasem i identyfikatorem żądania. Stan „Sending” bez odpowiedzi nigdy nie wraca do wysyłki, tylko do sprawdzenia statusu.

Z tej maszyny wynikają trzy reguły:

  • Z „Sending” nie ma drogi prosto do ponownej wysyłki. Timeout nie znaczy, że KSeF nie dostał dokumentu. Najpierw zapytanie o status po numerze referencyjnym.
  • „Blocked” to osobny stan, nie wyjątek w logu. Obsługa widzi w Base listę zamówień, które czekają na dane.
  • „Rejected” nie usuwa dokumentu. Numer faktury został nadany w ERP, więc dalsza droga to korekta albo ponowna wysyłka tego samego dokumentu po poprawie, zależnie od rodzaju błędu i decyzji księgowej.

Idempotencja: jak jedna sprzedaż daje jeden dokument

Zdarzenia przychodzą wielokrotnie: Base wysyła webhook dwa razy, worker pada po wysłaniu, a przed zapisem odpowiedzi, ktoś klika „wystaw” ponownie. Integracja musi być odporna na każdy z tych przypadków.

Klucz idempotencji liczony z danych biznesowych, nie z czasu: NIP sprzedawcy + kanał + id zamówienia + typ dokumentu + numer korekty. Jeden klucz, jeden wiersz w tabeli dokumentów, z ograniczeniem unikalności w bazie. Drugi webhook do tego samego zamówienia trafia na istniejący wiersz i kończy się bez akcji.

Outbox zamiast wysyłki w handlerze. Zdarzenie zapisuje dokument i wpis w tabeli outbox w jednej transakcji. Osobny worker czyta outbox, blokuje wiersz (SELECT … FOR UPDATE SKIP LOCKED albo odpowiednik), wysyła, zapisuje odpowiedź, zwalnia. Jeśli worker padnie w połowie, wiersz wraca do kolejki z informacją, że próba była (numer referencyjny albo jej brak).

Odcisk treści. Przed wysyłką zapisujecie hash XML-a. Jeśli po timeoucie status po numerze referencyjnym mówi „przyjęto”, porównujecie hash: ten sam dokument, więc zapisujecie numer KSeF i kończycie. Inny hash oznacza, że ktoś zmienił dane między próbami, i to jest sytuacja do ręcznego wyjaśnienia, nie do automatycznej wysyłki.

Numer faktury nadaje jeden system, raz. Jeśli ERP nadaje numer dopiero przy wystawieniu, integracja nie może „zarezerwować” numeru po swojej stronie. Wystawia w ERP, odczytuje numer, dopiero potem kolejkuje wysyłkę.

flowchart TB
  w["Webhook z Base
(może przyjść 2×)"] --> h{"Klucz idempotencji
już istnieje?"} h -->|"tak"| x["Koniec, bez akcji"] h -->|"nie"| t[("Jedna transakcja:
dokument + wpis outbox")] t --> wk["Worker: blokada wiersza outbox"] wk --> e[("ERP: wystaw fakturę,
odczytaj numer")] e --> k["KSeF: wyślij"] k -->|"odpowiedź"| s["Zapisz stan, hash XML,
numer referencyjny"] k -->|"timeout"| st["Zapytaj o status
po numerze referencyjnym"] st --> s s --> b["Base: numer FS,
numer KSeF, status"] classDef n fill:#eff6ff,stroke:#2563eb,color:#0f172a; class w,h,t,wk,e,k,s,st,b,x n;
Dwa miejsca chronią przed duplikatem: unikalny klucz przy wejściu i blokada wiersza outbox przy wysyłce.

Gdy KSeF nie odpowiada

Trzy różne sytuacje, trzy różne reakcje:

Timeout pojedynczego żądania. Dokument zostaje w „Sending”. Worker odpytuje status po numerze referencyjnym (jeśli go dostał) albo po własnym numerze faktury. Bez numeru referencyjnego i bez potwierdzenia w statusie, dopiero wtedy ponowna wysyłka. Odstępy rosnące: 30 s, 2 min, 10 min, 1 h, z limitem prób.

KSeF niedostępny dłużej (awaria, przerwa techniczna). Worker przestaje walić w API po kilku błędach z rzędu (circuit breaker), sprawdza dostępność co kilka minut, a kolejka rośnie. Dokumenty są w „Queued” lub „Offline”, nie w „Error”. Nikt nie wystawia drugiej faktury „bo pierwsza nie poszła”.

Tryb offline. Przepisy przewidują wystawianie faktur poza KSeF w czasie niedostępności, z późniejszą wysyłką w określonym terminie i z innym kodem QR na dokumencie. Decyzję, czy korzystacie z trybu offline, podejmuje księgowość. Technicznie integracja musi: oznaczyć dokument jako wystawiony offline, wygenerować właściwy QR, pilnować terminu dosłania i po przywróceniu KSeF wysłać dokumenty w kolejności wystawienia.

Odrzucenie. KSeF przyjął do przetworzenia, a po chwili odrzucił (błąd schematu, NIP, data). Dokument idzie do „Rejected” z treścią błędu, obsługa dostaje zadanie z numerem zamówienia. To jedyny stan, który wymaga człowieka, i ma być widoczny w pierwszej minucie, nie w raporcie miesięcznym.

Walidacja przed wysyłką, nie po

Większość odrzuceń da się złapać lokalnie:

  • NIP nabywcy: suma kontrolna, format, obecność w polu kontrahenta (nie w uwagach),
  • stawki VAT z kartoteki ERP, nie z opisu produktu na marketplace,
  • waluta i kurs przy sprzedaży zagranicznej,
  • walidacja XML wobec aktualnego schematu FA przed wysyłką,
  • data wystawienia nie z przyszłości i nie starsza niż pozwala konfiguracja.

Dokument, który nie przechodzi walidacji, ląduje w „Blocked” z listą braków. Taniej poprawić zamówienie niż korygować fakturę w KSeF.

Sekrety i uprawnienia

  • Token albo certyfikat do KSeF trzymacie w magazynie sekretów (vault, KMS, zaszyfrowana konfiguracja z rotacją), nie w kolumnie bazy ani w polu konfiguracyjnym Base.
  • Osobne poświadczenia per NIP, z uprawnieniami tylko do wystawiania i odczytu własnych faktur. Integracja do sprzedaży nie potrzebuje praw administratora podmiotu.
  • Dostęp do danych kontrahentów pobranych z KSeF (faktury zakupowe) ograniczacie do systemów, które ich potrzebują. To dane podatkowe, nie katalog produktów.
  • Każde wywołanie KSeF zapisuje się w audit logu: kto (który worker, który NIP), co (numer dokumentu, hash), kiedy, z jakim wynikiem. Bez treści tokena.
  • Środowisko testowe KSeF ma osobne poświadczenia i osobną konfigurację. Pomyłka środowisk to faktury testowe w produkcji albo odwrotnie.

Monitoring i rozliczenie dzienne

Minimum, które pozwala spać:

AlertPróg (do ustalenia u Was)
Dokument w „Sending” lub „Pending”dłużej niż 1 h
Dokument w „Queued”dłużej niż 4 h poza awarią
Dowolny dokument w „Rejected”natychmiast
Dokument offline bez dosłaniaprzed upływem terminu z przepisów
Kolejka outboxrośnie przez 30 min
Faktury w ERP bez numeru KSeFraport codzienny

Ostatni wiersz to rozliczenie dzienne: zadanie porównuje listę faktur wystawionych w ERP z listą dokumentów, które mają numer KSeF. Różnica to lista do wyjaśnienia. Bez tego raportu podwójna lub brakująca faktura wychodzi przy deklaracji, nie następnego dnia.

Korekty w tej samej maszynie

Korekta to osobny dokument z własnym kluczem idempotencji (… + numer korekty), własnym wierszem w outbox i własnym przejściem przez stany. Odwołuje się do numeru KSeF faktury pierwotnej, więc nie może zostać wysłana, zanim faktura pierwotna nie będzie w „Accepted”. To zależność, którą worker sprawdza przed pobraniem z kolejki. Szerzej o przepływach: korekty, zwroty i faktury zakupowe w KSeF.

Lista kontrolna przed produkcją

  1. Klucz idempotencji i unikalność w bazie: dwa identyczne webhooki dają jeden dokument.
  2. Worker ubity w trakcie wysyłki: po restarcie dokument nie wychodzi drugi raz.
  3. Timeout z odpowiedzią „przyjęto” po stronie KSeF: status odczytany, numer KSeF zapisany, bez ponownej wysyłki.
  4. KSeF niedostępny 2 godziny: kolejka rośnie, po przywróceniu dokumenty wychodzą w kolejności, żaden nie jest zduplikowany.
  5. Odrzucenie: zadanie dla obsługi w ciągu minuty, z powodem.
  6. Rozliczenie dzienne pokazuje zero różnic na danych testowych.
  7. Token wymieniony w vault bez restartu i bez wpisu w logach.

Jeśli którykolwiek punkt wymaga „to sprawdzimy ręcznie”, integracja nie jest gotowa.

Powiązane

Opiszcie, jak dziś wygląda u Was droga faktury do KSeF - powiemy, których z tych elementów brakuje i co to kosztuje.

Co dalej

Rozpoznajesz problem? Sprawdźmy Wasz przepływ danych.

Audyt konta BaseLinker i wymiany danych z partnerami: 2 500 - 6 000 zł. Wynik to konkretny dokument z rekomendacją architektury i orientacyjnym kosztem kolejnego etapu. Bez zobowiązania kontynuacji u nas.

Umów diagnozę przepływu danych