API KSeF 2.0: jak wygląda integracja od środka
API KSeF 2.0 to REST-owe API Ministerstwa Finansów pod adresem api.ksef.mf.gov.pl/api/v2
(TEST: api-test.ksef.mf.gov.pl/api/v2). Żeby wysłać jedną fakturę, musisz uwierzytelnić się tokenem
albo podpisem XAdES, otworzyć sesję z kluczem AES zaszyfrowanym kluczem publicznym MF, przesłać
zaszyfrowany XML FA(3), zamknąć sesję i odpytywać o status, aż pojawi się numer KSeF i UPO.
Ten tekst jest dla osoby, która wycenia własną integrację. Nie znajdziesz tu obietnicy, że to proste, bo nie jest, ani straszenia, że to niewykonalne, bo też nie jest. Znajdziesz to, co faktycznie trzeba zbudować, i przykłady w Ruby oparte na naszym gemie, który jest otwarty i możesz go po prostu wziąć.
Czym jest API KSeF 2.0
KSeF 2.0 to druga generacja Krajowego Systemu e-Faktur. Pierwsza (KSeF 1.0) została wyłączona:
jej hosty testowe odpowiadają dziś błędem 503, a ksef.mf.gov.pl to adres starego systemu. Jeśli
trafisz na poradnik używający tamtych adresów albo schematu FA(2), jest nieaktualny.
Aktualne API to JSON po HTTPS, z uwierzytelnianiem Bearer i jednym wyjątkiem: sam podpis XAdES i odbierane UPO to XML. Całość obraca się wokół pojęcia sesji. Nie wysyłasz faktury pojedynczym żądaniem. Otwierasz sesję, wkładasz do niej dokumenty, zamykasz ją, a dopiero zamknięcie uruchamia generowanie UPO.
Są dwa rodzaje sesji. Online to jedna faktura albo kilka wysyłanych po kolei, każda osobnym żądaniem. Wsadowa (batch) to paczka ZIP z wieloma fakturami, dzielona na części i wgrywana pod adresy przekazane przez KSeF. Sesja żyje 12 godzin.
Trzy środowiska i po co każde z nich
| Środowisko | API | Aplikacja podatnika | Tożsamość |
|---|---|---|---|
| TEST | api-test.ksef.mf.gov.pl/api/v2 |
ap-test.ksef.mf.gov.pl |
fikcyjna, dowolny wymyślony NIP |
| DEMO | api-demo.ksef.mf.gov.pl/api/v2 |
ap-demo.ksef.mf.gov.pl |
prawdziwy NIP, bez skutków prawnych |
| PROD | api.ksef.mf.gov.pl/api/v2 |
ap.ksef.mf.gov.pl |
prawdziwa, faktura zostaje wystawiona |
TEST to piaskownica na jednorazowe tożsamości. Wchodzisz do aplikacji podatnika, podajesz wymyślony NIP i generujesz token. Nikt niczego nie weryfikuje, więc to najszybsza droga do pierwszego UPO. DEMO wygląda i zachowuje się jak produkcja, uwierzytelnia prawdziwym NIP-em i prawdziwym certyfikatem, ale faktury nie mają mocy prawnej. Tam sprawdzasz integrację przed przełączeniem. PROD to jedyne miejsce, gdzie faktura naprawdę powstaje.
Token wygenerowany na TEST nie zadziała na PROD i odwrotnie. To najczęstsza przyczyna błędu 450 przy pierwszym uruchomieniu, zaraz obok tokena z doklejonym znakiem nowej linii przy kopiowaniu.
Łatwo za to przeoczyć, że weryfikacja kodów QR nie należy do API. To osobna
usługa pod adresami qr-test.ksef.mf.gov.pl, qr-demo… i qr.ksef.mf.gov.pl.
Uwierzytelnianie: tutaj zatrzymuje się większość integracji
Uwierzytelnienie w KSeF nie jest wymianą loginu na token. To krótka, asynchroniczna operacja z odpytywaniem o wynik.
Zaczyna się tak samo niezależnie od metody: POST /auth/challenge zwraca challenge i znacznik czasu
w milisekundach. Dalej rozchodzą się dwie drogi.
Token KSeF (uwierzytelnienie maszynowe). Sklejasz "{token}|{timestampMs}", szyfrujesz ten napis
algorytmem RSA-OAEP z SHA-256 kluczem publicznym MF o zastosowaniu KsefTokenEncryption i wysyłasz
na /auth/ksef-token razem z identyfikatorem klucza. Klucze pobierasz z
/security/public-key-certificates i wybierasz najnowszy dla danego zastosowania, bo są rotowane.
Uwaga: to inny certyfikat niż ten, którym owijasz klucz sesji.
Podpis XAdES. Budujesz dokument AuthTokenRequest w przestrzeni nazw auth/token/2.1, wkładasz
w niego challenge, NIP kontekstu i informację o tym, po czym KSeF ma rozpoznać certyfikat (po polu
subject albo po odcisku palca), podpisujesz go otoczonym podpisem XAdES-BES i wysyłasz jako application/xml
na /auth/xades-signature. W produkcji z włączoną weryfikacją łańcucha certyfikatu, na TEST i DEMO
bez niej, bo tam używa się certyfikatów samopodpisanych.
Dalej obie drogi się schodzą. Odpytujesz GET /auth/{referenceNumber}, aż status wskaże 200 (100
oznacza „w toku”), a potem POST /auth/token/redeem wymienia jednorazowy token operacji na parę
JWT: access i refresh. Access wygasa szybko, refresh służy do przedłużania bez powtarzania całej
procedury. Jeśli refresh zostanie odwołany, jedyne wyjście to pełne uwierzytelnienie od nowa.
W naszym gemie cały ten cykl siedzi w jednym module, a metoda różniąca obie ścieżki to dosłownie jedna funkcja. Wywołanie wygląda tak:
require "ksef"
env = Ksef::Environment.new(:test)
authenticator = Ksef::Authenticator::Token.new(
environment: env,
ksef_token: ENV["KSEF_TOKEN"],
context_nip: "1111111111"
)
client = Ksef::Client.new(env: env, authenticator: authenticator)
Z certyfikatem zamiast tokena zmienia się tylko authenticator:
authenticator = Ksef::Authenticator::Xades.new(
environment: env,
certificate: OpenSSL::X509::Certificate.new(File.read("cert.pem")),
private_key: OpenSSL::PKey.read(File.read("key.pem")),
context_nip: "1111111111",
subject_identifier_type: "certificateSubject"
)
Sesja daje dokładnie tyle uprawnień, ile ma kontekst (NIP), w którym się uwierzytelniłeś. Token bez nadanego w aplikacji podatnika uprawnienia do wysyłki faktur uwierzytelni się poprawnie i dopiero potem dostaniesz 415. Warto to rozróżnić w obsłudze błędów, bo dla użytkownika to zupełnie inna instrukcja niż „token wygasł”.
Wysyłka faktury: sesja, szyfrowanie, UPO
Pełna droga jednej faktury:
- Zbuduj XML FA(3) i zwaliduj go lokalnie względem XSD.
- Wylosuj klucz AES-256 i IV, zaszyfruj klucz algorytmem RSA-OAEP certyfikatem
SymmetricKeyEncryption. POST /sessions/onlinezformCodeFA(3) i materiałem szyfrującym. W odpowiedzi numer referencyjny sesji.POST /sessions/online/{ref}/invoicesz zaszyfrowaną fakturą oraz czterema wartościami: skrótem i rozmiarem tekstu jawnego oraz skrótem i rozmiarem szyfrogramu.POST /sessions/online/{ref}/close. To zamknięcie uruchamia generowanie UPO.GET /sessions/{ref}w pętli: 100 (przyjęta, w kolejce), 170 (przetwarzanie), 200 (gotowe). Dla sesji wsadowej kody w toku to 100 i 150.GET /sessions/{ref}/invoices/{invoiceRef}/upozwraca podpisany XML UPO z numerem KSeF.
W gemie tych siedem kroków to dwie linie, ale rozdzielone celowo:
receipt = client.open_and_send(invoice)
# zapisz receipt.session_reference, receipt.invoice_reference,
# receipt.invoice_hash i receipt.generated_at ZANIM zaczniesz odpytywać
result = client.await(
session_reference: receipt.session_reference,
invoice_reference: receipt.invoice_reference
)
result.ksef_number # => "1111111111-20260820-..."
result.upo.received_at # => czas nadania numeru KSeF
result.upo.xml # => podpisany XML do archiwum
Rozdzielenie wysyłki od odpytywania to nie stylistyka, tylko jedyny sposób, żeby proces przeżył restart. Jeśli zapiszesz numery referencyjne, przerwane odpytywanie da się wznowić bez ponownej wysyłki. Jeśli ich nie zapiszesz, po awarii nie odróżnisz faktury niewysłanej od wysłanej, a ponowienie w tym drugim przypadku oznacza podwójne wystawienie dokumentu księgowego.
Sam obiekt faktury jest zwykłą strukturą danych, bez zależności od jakiegokolwiek systemu źródłowego:
invoice = Ksef::Fa3::Invoice.new(
number: "FV/2026/08/1",
issue_date: Date.new(2026, 8, 20),
currency: "PLN",
seller: Ksef::Fa3::Party.new(
name: "Moja Firma sp. z o.o.", nip: "1111111111",
country: "PL", address_line1: "ul. Przykładowa 1, 00-001 Warszawa"
),
buyer: Ksef::Fa3::Party.new(
name: "Klient sp. z o.o.", nip: "2222222222",
country: "PL", address_line1: "ul. Druga 2, 30-001 Kraków"
),
vat_buckets: [ Ksef::Fa3::VatBucket.new(rate: :standard_23, net: "1000.00", vat: "230.00") ],
lines: [ Ksef::Fa3::Line.new(name: "Abonament, sierpień 2026", net: "1000.00", rate: :standard_23) ],
total: "1230.00"
)
errors = Ksef::Fa3::Builder.validate(Ksef::Fa3::Builder.new(invoice).to_xml)
errors.empty? # => true
Co zaskakuje przy wycenie
Tego nie widać w dokumentacji przy pierwszym czytaniu.
Podpis XAdES. Nie wystarczy „podpisać XML”. Trzeba złożyć otoczony podpis XAdES-BES z dwiema referencjami: jedną do dokumentu bez podpisu, drugą do bloku SignedProperties, obie po kanonikalizacji C14N 1.0, z blokiem QualifyingProperties niosącym czas podpisu i skrót certyfikatu. Nazwa wystawcy w IssuerSerial musi być w formacie RFC 2253, a nie w domyślnej formie OpenSSL-a. A jeśli używasz klucza EC, co jest typowe dla certyfikatu wydanego przez KSeF, podpis trzeba przekonwertować z DER-owego SEQUENCE na surową parę r‖s o stałej szerokości. Inaczej dostaniesz odrzucenie bez wskazówki, co konkretnie jest nie tak. Podpis może się przy tym poprawnie weryfikować u Ciebie, bo jest wewnętrznie spójny, i mimo to zostać odrzucony przez KSeF.
Szyfrowanie sesji. AES-256-CBC z dopełnieniem PKCS#7, świeży klucz i IV na sesję. W sesji online IV jedzie osobnym polem, w sesji wsadowej jest doklejany na początek każdej części. Intuicja podpowiada odwrotnie, a do tego oba tryby tego samego API robią to inaczej. Do tego cztery wartości, skrót i rozmiar liczone raz na tekście jawnym, raz na szyfrogramie; pomyłka daje 21402 albo 21403. A ponieważ klucze publiczne MF są rotowane, trzeba obsłużyć 21470, czyli „szyfrowałeś nieaktualnym kluczem”, pobierając certyfikaty ponownie.
Rygor schematu FA(3). XSD ustala kolejność elementów, a nie tylko ich obecność. Nabywca
(Podmiot2) wymaga znaczników JST i GV, których sprzedawca mieć nie może. Wiersze faktury w FA(3) są
bezpośrednimi dziećmi elementu Fa, bez opakowania FaWiersze znanego z FA(2); zostawienie starej
struktury to gwarantowane 21401. Stawki mają swoje przegrody: 23, 8, 5 i 4 procent wraz z kwotą VAT,
a pozycje niepodlegające opodatkowaniu i odwrotne obciążenie tylko z kwotą netto. Przy walucie obcej
dochodzi kwota VAT w złotych, po kursie NBP z dnia poprzedzającego
sprzedaż. I drobiazg, który wychodzi dopiero w praktyce: faktura, na której NIP nabywcy równa się
NIP-owi sprzedawcy, zostaje odrzucona, a sesja zamyka się z kodem 445.
Korekty. Faktura korygująca (KOR) to nie zwykła faktura z minusem. To dokument z typem korekty (1, 2 albo 3), przyczyną i blokiem danych faktury korygowanej, w którym musi znaleźć się numer KSeF oryginału. Bez tego numeru nie ma czego korygować, więc w praktyce potrzebujesz trwałego powiązania między swoją fakturą a numerem nadanym przez KSeF.
Kody odrzuceń. KSeF zwraca kod, nie zdanie po polsku. Bez mapowania kodów na sensowną instrukcję zbudujesz interfejs, w którym użytkownik widzi „błąd 21401” i nic z tego nie wynika. My utrzymujemy własną mapę kodów na przyczynę i konkretny następny krok, i traktujemy ją jak część produktu, a nie jak dodatek.
Życie po wdrożeniu. KSeF potrafi odpowiedzieć kodem 429 z nagłówkiem Retry-After albo
zwyczajnie błędem 5xx, a Twoja pętla odpytywania może się skończyć, zanim sesja przejdzie w stan
końcowy. Wtedy potrzebujesz procesu uzgadniania, który wraca do zapisanych numerów referencyjnych
i czyta status jeszcze raz, zamiast wysyłać dokument ponownie. Do tego dochodzi drobiazg, który
kosztuje najwięcej nerwów: zamknięcie sesji uruchamia UPO, więc sesja, której zamknięcie się nie
udało, będzie odpytywana w nieskończoność i nigdy nie wyda poświadczenia. Trzeba to zamknięcie
ponowić przed dalszym odpytywaniem.
Skrót dokumentu jest przywiązany do czasu. XML FA(3) zawiera znacznik DataWytworzeniaFa,
a skrót SHA-256 liczy się z dokładnie tych bajtów, które wysłałeś. Ten sam skrót jest wyróżnikiem
w kodzie QR i wraca na UPO. Zbuduj tę samą fakturę sekundę później, a dostaniesz inny skrót. Dlatego
znacznik trzeba zapisać razem ze skrótem, inaczej wysłanego dokumentu nie da się odtworzyć.
Kontrakt produkcyjny jest zamrożony
Jedna dobra wiadomość dla planowania. Ministerstwo Finansów zamroziło kontrakt produkcyjny API KSeF 2.0 22 grudnia 2025 roku. Endpointy, formaty i reguły walidacji są ustalone. To znaczy, że integracja napisana dziś nie rozsypie się przy najbliższej aktualizacji, a praca, którą w nią włożysz, będzie pracowała dalej.
Terminy pozostają bez zmian: obowiązek od 1 lutego 2026 dla dużych podatników i od 1 kwietnia 2026 dla pozostałych, kary od 1 stycznia 2027.
Pisać samemu czy wziąć gotowe
Uczciwa odpowiedź brzmi: to zależy od tego, ile faktur wystawiasz i skąd one pochodzą.
Pisz samemu, jeśli faktury powstają w Twoim własnym systemie, masz zespół, który utrzyma tę integrację przez lata, i traktujesz fakturowanie jako część produktu. Protokół jest skończony, kontrakt zamrożony, a specyfikacja publiczna. To wykonalne. Licz na sześć do dziesięciu tygodni pracy jednej osoby do stanu produkcyjnego i na stały koszt utrzymania: kolejki, ponowienia, uzgadnianie stanu po awarii, archiwum UPO, obsługa odrzuceń.
Weź gotowe, jeśli faktury już powstają gdzie indziej, na przykład w Stripe albo w systemie sprzedażowym, a KSeF jest dla Ciebie obowiązkiem do odhaczenia, nie przewagą. Wtedy kilka tygodni pracy inżyniera zwykle kosztuje wielokrotnie więcej niż rok korzystania z gotowego narzędzia.
Jest też droga pośrednia i szczerze ją polecamy. Weź sam protokół, a resztę napisz po swojemu. Nasz
gem ksef (github.com/startupkit-app/ksef) jest na licencji Apache-2.0 i robi dokładnie to:
uwierzytelnianie tokenem i XAdES, szyfrowanie, sesje online i wsadowe, budowanie i walidację FA(3),
parsowanie UPO, walidację NIP i kursy NBP. Bez Rails, bez Stripe, sam protokół. Możesz go użyć
i nigdy nie zapłacić nam ani złotówki. Wolimy to, niż udawać, że napisanie tego samodzielnie jest
trudniejsze, niż jest naprawdę.
Obsługa błędów w gemie jest typowana, więc rozróżnienie „ponów” od „powiedz użytkownikowi” wygląda tak:
begin
client.submit(invoice)
rescue Ksef::SchemaInvalid => e # 21401, popraw dane i wyślij ponownie
report_to_user(e.code)
rescue Ksef::RateLimited => e # 429, KSeF podaje Retry-After
retry_in(e.retry_after)
rescue Ksef::AuthError => e # 450, 415, 460: problem z połączeniem, nie z fakturą
reconnect(e.code)
end
Jeśli fakturujesz przez Stripe
Jest jeden przypadek, w którym własna integracja rzadko się opłaca. Kiedy faktury powstają w Stripe, praca nie polega na wysyłce do KSeF, tylko na mapowaniu: rozpoznaniu typu transakcji, przeliczeniu waluty, złożeniu stawek w przegrody FA(3), obsłudze not kredytowych jako korekt i zapisaniu numeru KSeF z powrotem na fakturze.
KSeF Kit robi to od strony Stripe, a pod spodem stoi na tym samym gemie, o którym mowa wyżej. Jeśli wolisz trzymać wszystko u siebie, tę samą aplikację uruchomimy na Twoim serwerze. Zobacz też, jak działa integracja Stripe z KSeF i co dokładnie dostajesz w UPO.