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.
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:
- Przenosi zamówienie z Base do ERP jako dokument, z danymi nabywcy i kanałem.
- Decyduje, czy do zamówienia powstaje faktura, paragon, czy nic (reguła po danych, nie po uwagach).
- Waliduje dane przed wystawieniem: NIP z sumą kontrolną, stawki VAT, kompletność adresu, zgodność ze schematem.
- Pilnuje, żeby jedna sprzedaż dała jeden dokument, niezależnie od tego, ile razy zdarzenie przyszło.
- Odbiera z ERP numer faktury, a po wysyłce numer KSeF, UPO i status, i zapisuje je w zamówieniu.
- Obsługuje korekty tą samą drogą (zwrot w Base → korekta w ERP → KSeF → status w Base).
- Rozlicza się codziennie: każda faktura z ERP ma numer KSeF albo jest na liście wyjątków.
- 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 --> [*]
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;
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ć:
| Alert | Pró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łania | przed upływem terminu z przepisów |
| Kolejka outbox | rośnie przez 30 min |
| Faktury w ERP bez numeru KSeF | raport 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ą
- Klucz idempotencji i unikalność w bazie: dwa identyczne webhooki dają jeden dokument.
- Worker ubity w trakcie wysyłki: po restarcie dokument nie wychodzi drugi raz.
- Timeout z odpowiedzią „przyjęto” po stronie KSeF: status odczytany, numer KSeF zapisany, bez ponownej wysyłki.
- KSeF niedostępny 2 godziny: kolejka rośnie, po przywróceniu dokumenty wychodzą w kolejności, żaden nie jest zduplikowany.
- Odrzucenie: zadanie dla obsługi w ciągu minuty, z powodem.
- Rozliczenie dzienne pokazuje zero różnic na danych testowych.
- 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
- Integracja KSeF: sklep, Base i ERP
- KSeF w Comarch Optima przy sprzedaży przez Base i sklep
- KSeF w Subiekcie GT i nexo przy sprzedaży przez Base
- Audit log w wymianie danych B2B
- Synchroniczna czy asynchroniczna integracja Base z ERP
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.