Przejdź do treści
KSeF Kit

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:

  1. Zbuduj XML FA(3) i zwaliduj go lokalnie względem XSD.
  2. Wylosuj klucz AES-256 i IV, zaszyfruj klucz algorytmem RSA-OAEP certyfikatem SymmetricKeyEncryption.
  3. POST /sessions/online z formCode FA(3) i materiałem szyfrującym. W odpowiedzi numer referencyjny sesji.
  4. POST /sessions/online/{ref}/invoices z zaszyfrowaną fakturą oraz czterema wartościami: skrótem i rozmiarem tekstu jawnego oraz skrótem i rozmiarem szyfrogramu.
  5. POST /sessions/online/{ref}/close. To zamknięcie uruchamia generowanie UPO.
  6. 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.
  7. GET /sessions/{ref}/invoices/{invoiceRef}/upo zwraca 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.

Najczęstsze pytania

Czy KSeF ma publiczne API?

Tak. KSeF 2.0 udostępnia REST-owe API pod adresem api.ksef.mf.gov.pl/api/v2 (produkcja) oraz api-test.ksef.mf.gov.pl/api/v2 i api-demo.ksef.mf.gov.pl/api/v2 dla środowisk nieprodukcyjnych. Jest darmowe i otwarte dla każdego podatnika, wymaga tylko uwierzytelnienia tokenem KSeF albo podpisem XAdES.

Jak uwierzytelnić się w API KSeF?

Pobierasz challenge z POST /auth/challenge, a potem albo szyfrujesz token KSeF kluczem publicznym MF i wysyłasz go na /auth/ksef-token, albo podpisujesz dokument AuthTokenRequest podpisem XAdES i wysyłasz na /auth/xades-signature. W obu przypadkach odpytujesz GET /auth/{ref} aż do statusu 200 i wymieniasz jednorazowy token na parę access i refresh przez POST /auth/token/redeem.

Czym różnią się środowiska TEST, DEMO i PROD?

TEST działa na fikcyjnych tożsamościach: bierzesz wymyślony NIP i generujesz token, nikt go nie weryfikuje. To środowisko na pierwsze próby. DEMO wymaga uwierzytelnienia prawdziwym NIP-em, ale nie wywołuje skutków prawnych. PROD to jedyne środowisko, w którym faktura zostaje wystawiona.

Ile trwa własna integracja z KSeF?

Samo wysłanie pierwszej faktury na TEST to kwestia kilku dni. Sześć do dziesięciu tygodni zajmuje reszta: podpis XAdES, szyfrowanie sesji, pełne mapowanie FA(3), korekty, mapowanie kodów odrzuceń, odporność na awarie i ponowienia oraz archiwizacja UPO.

Czy schemat FA(2) jeszcze działa?

Nie. Obowiązującym schematem jest FA(3) w wersji 1-0E, wymagany od 1 lutego 2026. Struktury z FA(2), na przykład opakowanie FaWiersze wokół wierszy faktury, są w FA(3) błędem i kończą się odrzuceniem 21401.

Czy jest gotowa biblioteka do KSeF 2.0 w Ruby?

Tak. Publikujemy gem ksef na licencji Apache-2.0: uwierzytelnianie tokenem i XAdES, szyfrowanie RSA-OAEP i AES-256-CBC, sesje online i wsadowe, budowanie oraz walidacja FA(3) i parsowanie UPO. Kod jest na GitHubie i możesz go użyć niezależnie od tego, czy korzystasz z KSeF Kit.