Wysyłka faktur do KSeF
Wystawienie faktury i wysłanie jej do KSeF (Krajowego Systemu e‑Faktur) to dwa osobne kroki. Najpierw tworzysz dokument, potem go wysyłasz. Nie ma jednego wywołania, które tworzy i wysyła naraz, i jest to celowe: dzięki temu możesz utworzyć, sprawdzić lub zwalidować dokument, zanim stanie się prawną e‑fakturą.
Wysyłka jest „natychmiastowa", ale może zakończyć się asynchronicznie. Endpoint przez chwilę czeka, aż KSeF przyjmie fakturę; jeśli KSeF potrzebuje więcej czasu, wywołanie i tak zwraca sukces, a ostateczny wynik pojawia się nieco później. Dlatego najważniejsze jest prawidłowe oczekiwanie na wynik, przez odpytywanie endpointu statusu albo przez webhook.
Przepływ w skrócie
Zanim zaczniesz
- Na koncie musi być skonfigurowane autoryzowane połączenie z KSeF (robi się to w aplikacji). Bez niego wysyłka jest odrzucana.
- Token potrzebuje zakresów
documents:create(tworzenie) idocuments:send-to-ksef(wysyłka). Odpytywanie wymagadocuments:read. - Dokument musi być typem obsługiwanym przez KSeF, na przykład
FakturaVAT. Zobacz Typy dokumentów i kwoty.
Testuj najpierw w sandboxie — bez skutków prawnych. Zanim zaczniesz wysyłać prawdziwe faktury, przećwicz cały przepływ w środowisku testowym (
https://sandbox.api.fakturnia.pl). Wymaga ono testowego certyfikatu KSeF: zaloguj się na testowy podmiot w portalu Ministerstwa Finansówhttps://ap-test.ksef.mf.gov.pl/web/, wygeneruj certyfikat i podłącz go do konta sandboxowego. Certyfikat produkcyjny nie zadziała wobec sandboxa.
Opcjonalnie: najpierw zwaliduj. Aby wychwycić problemy, zanim cokolwiek zostanie zapisane, wyślij treść faktury do
POST /documents/validate. Uruchamia pełny proces (numeracja, sumy, dopasowanie kontrahenta) i zwraca podgląd bez zapisu, więc możesz od razu pokazać użytkownikowi ewentualne błędy walidacji. Zobacz Typy dokumentów i kwoty.
1. Utwórz fakturę
Zwykłe utworzenie dokumentu. Nic specyficznego dla KSeF.
curl -s -X POST https://prod.api.fakturnia.pl/external/v1/documents \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "documentType": "FakturaVAT", "issueDate": "2026-06-05", "buyerId": "681a…", "products": [ … ] }'
W odpowiedzi 201 dostajesz document.id. Zachowaj go. Wszystkie kolejne kroki używają tego id.
Daty i terminy z dokumentu trafiają do XML FA(3) tak:
| Pole dokumentu | Element FA(3) |
|---|---|
issuePlace |
P_1M (miejsce wystawienia) |
subDate, subDateTo |
P_6 albo OkresFa (P_6_Od, P_6_Do) |
paymentDeadlineDate, paymentDeadlineTerm |
TerminPlatnosci z Termin i TerminOpis |
paymentInstallments |
jeden TerminPlatnosci na ratę, kwoty rat w DodatkowyOpis |
advanceReceipts (faktura zaliczkowa) |
ZaliczkaCzesciowa z P_6Z, P_15Z i KursWalutyZW dla każdej wpłaty |
exchangeRate |
kurs w pozycjach; gdy go brakuje, Fakturnia przy wysyłce bierze kurs NBP z dnia roboczego przed obowiązkiem podatkowym (korekta: kurs faktury korygowanej) |
Daty z godziną zamieniamy na dzień w Polsce, a datę, której nie da się odczytać, zgłaszamy jako błąd walidacji zamiast wysyłać dzisiejszą.
Pozwól użytkownikowi sprawdzić fakturę przed wysyłką. Dokument staje się ostateczny po trafieniu do KSeF, a późniejsza poprawka wymaga formalnej korekty (
FakturaKorygujaca). Dlatego między utworzeniem a wysyłką warto pokazać fakturę użytkownikowi, na przykład jej PDF, aby potwierdzić poprawność danych. Wychwycenie błędnego NIP-u nabywcy lub kwoty w tym miejscu oszczędza późniejszej korekty.
2. Wyślij do KSeF
curl -s -X POST https://prod.api.fakturnia.pl/external/v1/documents/{id}/send-to-ksef \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)"
Zawsze przekazuj Idempotency-Key. Jeśli połączenie zerwie się w trakcie wysyłki, możesz bezpiecznie ponowić dokładnie to samo żądanie bez ryzyka podwójnego wysłania. Zobacz Idempotentność.
Endpoint zwraca 200 w dwóch postaciach. Rozróżniaj je po tym, czy obecne jest data.ksefNumber:
Przyjęta od razu (KSeF odpowiedział w oknie oczekiwania):
{
"success": true,
"data": {
"ksefNumber": "1234567890-20260605-AABBCC-DD",
"referenceNumber": "20260605…",
"invoiceNumber": "1",
"ordinalNumber": 1,
"status": { "code": 200, "description": "…", "details": [] },
"sessionReferenceNumber": "…",
"hasUpo": true,
"sendMode": "instant"
},
"executionTime": 8200
}
Wciąż przetwarzane (KSeF potrzebuje więcej czasu; system dokończy wysyłkę w tle):
{
"success": true,
"message": "Faktura została wysłana do KSeF. Przetwarzanie trwa dłużej niż zwykle; status zaktualizuje się automatycznie.",
"data": {
"status": "processing",
"sendMode": "instant",
"documentNumber": "FV/1/06/2026",
"sessionReferenceNumber": "…",
"invoiceReferenceNumber": "…"
},
"executionTime": 61000
}
Gdy dostaniesz status: "processing", faktura została już przekazana do KSeF. Nie wysyłaj jej ponownie. Przejdź do kroku 3 i poczekaj na ostateczny wynik.
Gdy wysyłka zostanie odrzucona
Te odpowiedzi przychodzą, zanim cokolwiek zostanie wysłane, więc można je bezpiecznie poprawić i ponowić:
| Status | Kod | Znaczenie |
|---|---|---|
409 |
KSEF_CONNECTION_REQUIRED |
brak skonfigurowanego połączenia z KSeF na koncie |
409 |
KSEF_ALREADY_PROCESSING |
wysyłka tego dokumentu jest już w toku |
409 |
KSEF_SEND_NOT_ALLOWED |
dokument nie jest w stanie pozwalającym na wysyłkę (już wysłany, zły typ, usunięty) |
422 |
KSEF_CONNECTION_NOT_AUTHORIZED |
połączenie z KSeF istnieje, ale jego token jest nieprawidłowy lub wygasł |
400 |
VALIDATION_ERROR / BAD_REQUEST |
dokument nie spełnia wymagań KSeF |
3. Poczekaj na ostateczny wynik
Gdy odpowiedź wysyłki mówi processing, nie jest to koniec. Źródłem prawdy jest endpoint statusu. Wynik możesz poznać na dwa sposoby.
Sposób A — odpytywanie endpointu statusu
curl -s https://prod.api.fakturnia.pl/external/v1/documents/{id}/ksef-status \
-H "Authorization: Bearer $TOKEN"
{
"success": true,
"data": {
"documentId": "681a…",
"documentNumber": "FV/1/06/2026",
"state": "sent",
"ksefNumber": "1234567890-20260605-AABBCC-DD",
"ksefReferenceNumber": "20260605…",
"ksefStatus": { "code": 200, "description": "Przyjęto", "details": [] },
"lastError": null,
"sentAt": "2026-06-05T10:35:00.000Z",
"processedAt": "2026-06-05T10:35:14.000Z",
"updatedAt": "2026-06-05T10:35:14.000Z"
}
}
Decyzje podejmuj na podstawie pola state:
state |
Znaczenie | Co zrobić |
|---|---|---|
not_sent |
wysyłka jeszcze nierozpoczęta | (po kroku 2 nie powinno wystąpić) |
processing |
przekazane do KSeF, oczekuje na przyjęcie | odpytuj dalej |
sent |
przyjęte; ksefNumber jest ustawione |
gotowe, zapisz ksefNumber |
failed |
KSeF odrzucił; lastError jest ustawione |
odczytaj lastError, popraw, wyślij ponownie |
Odpytuj, aż state będzie sent lub failed. Oba są stanami końcowymi. Ten endpoint ma limit 5 zapytań na minutę, więc odpytuj mniej więcej co 15 sekund, nie częściej, i poddaj się po około 10 minutach. Wysyłka w stanie processing zwykle finalizuje się w kilka sekund; jeśli potrzebujesz mniejszego opóźnienia niż odpytywanie co 15 sekund, użyj webhooka poniżej zamiast odpytywać częściej. Jeśli przekroczysz limit, dostaniesz 429 z nagłówkiem Retry-After. Uszanuj go.
async function waitForKsef(id: string, token: string, intervalMs = 15_000, timeoutMs = 600_000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const res = await fetch(`${API}/documents/${id}/ksef-status`, {
headers: { Authorization: `Bearer ${token}` },
});
if (res.status === 429) { // limit przekroczony: odczekaj
const retry = Number(res.headers.get('Retry-After') ?? 15);
await new Promise((r) => setTimeout(r, retry * 1000));
continue;
}
const { data } = await res.json();
if (data.state === 'sent') return data.ksefNumber as string;
if (data.state === 'failed') throw new Error(data.lastError?.message ?? 'KSeF odrzucił fakturę');
await new Promise((r) => setTimeout(r, intervalMs));
}
throw new Error('Przekroczono czas oczekiwania na KSeF');
}
Sposób B — subskrypcja webhooka
Zamiast odpytywać, subskrybuj document.sentToKSeF i document.ksefRejected. Pierwsze zdarzenie przychodzi, gdy KSeF przyjmie fakturę (także gdy przetwarzanie dokończy się w tle), drugie, gdy ją odrzuci albo wysyłka zakończy się błędem (walidacja, timeout, utracona sesja). Dzięki temu unikasz odpytywania i nie przegapisz odrzucenia. Zobacz Webhooki po format dostarczania i weryfikację podpisu.
Gdy zdarzenie dotrze, potwierdź wynik przez endpoint statusu, zamiast ufać samemu zdarzeniu: wywołaj GET /documents/{id}/ksef-status, sprawdź, czy state === 'sent', i stamtąd weź ksefNumber. Jeśli state to wciąż processing, poczekaj na kolejne zdarzenie albo wróć do odpytywania. Webhooki mogą się też opóźnić lub zostać ponowione, dlatego większość integracji traktuje zdarzenie jako wyzwalacz, a odpytywanie zostawia jako zabezpieczenie.
Zalecany wzorzec
Zwykle chcesz mieć jedno i drugie: szybką odpowiedź, gdy KSeF działa szybko, i brak ciągłego odpytywania, gdy jest wolny. Połącz serię odpytań z webhookiem i zostaw sobie siatkę bezpieczeństwa.
- Odpytaj seryjnie zaraz po wysyłce. Sprawdź endpoint statusu do 3 razy, co 3 sekundy. Wiele wysyłek kończy się w tym oknie, a 3 sprawdzenia mieszczą się z zapasem w limicie 5/min. Jeśli dostaniesz
sentlubfailed, to koniec. - Wciąż
processing? Przerwij i przekaż dalej. Pokaż użytkownikowi, że faktura jest w trakcie przetwarzania, przestań odpytywać i poczekaj na webhookdocument.sentToKSeFalbodocument.ksefRejected, który dostarczy wynik. Bez pętli odpytywania i bez napierania na limit. - Miej mechanizm awaryjny. Webhook można przeoczyć: serwer był offline, w trakcie wdrożenia albo za chwilowym problemem sieciowym. Uruchom więc cykliczne zadanie, które ponownie sprawdza dokumenty wciąż w stanie
processingi uzgadnia je z endpointu statusu, na przykład raz na minutę. Dzięki temu faktura nigdy nie zawiśnie w oczekiwaniu na webhook, który nie dotarł.
// 1. seryjne odpytanie po szybki wynik (maks. 3 sprawdzenia, co 3 s)
for (let i = 0; i < 3; i++) {
const { state, ksefNumber } = await getKsefStatus(id, token);
if (state === 'sent') return ksefNumber;
if (state === 'failed') throw new Error('KSeF odrzucił fakturę');
await sleep(3000);
}
// 2. wciąż processing: poinformuj użytkownika, niech webhook dokończy
markProcessing(id); // UI: „faktura jest w trakcie przetwarzania"
// 3. document.sentToKSeF (lub backstop raz na minutę) finalizuje później
Limit jest na aplikację, nie na dokument. Endpoint statusu pozwala na 5 zapytań na minutę dla jednej aplikacji (konta API), wspólnie dla wszystkich dokumentów, niezależnie od tego, z ilu serwerów aplikacja wysyła zapytania. Jeśli Twój mechanizm awaryjny może napotkać więcej niż kilka przetwarzanych faktur naraz, ogranicz go: uzgadniaj kilka na minutę, rozkładaj wywołania w czasie i respektuj
429/Retry-After. Backstop to siatka bezpieczeństwa, nie szybka ścieżka.
Obsługa niepowodzenia
Gdy state to failed, pole lastError zawiera kod i komunikat z KSeF. Dokument z niepowodzeniem jest odblokowany: ponieważ niesie błąd KSeF, możesz go edytować lub usunąć i wysłać ponownie. (Dokument przyjęty, lub wciąż oczekujący na potwierdzenie, jest zablokowany. Zobacz Usuwanie i edycja dokumentów.) Wyślij ponownie z nowym Idempotency-Key, bo to nowa próba.
Wszystko razem
// 1. utwórz fakturę
const created = await api.post('/documents', invoiceBody, { idempotencyKey: uuid() });
const id = created.data.document.id;
// 2. wyślij do KSeF
const sent = await api.post(`/documents/${id}/send-to-ksef`, null, { idempotencyKey: uuid() });
// 3. ustal wynik
let ksefNumber = sent.data.ksefNumber; // obecne, gdy przyjęto synchronicznie
if (!ksefNumber) { // w przeciwnym razie data.status === 'processing'
ksefNumber = await waitForKsef(id, token); // odpytuj aż sent lub failed
}
// ksefNumber to teraz prawny identyfikator faktury w KSeF