Fakturnia Developers Szybki start Dokumentacja API English

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

Wystaw fakturę i wyślij do KSeF: utwórz, wyślij, a następnie poczekaj na wynik przez odpytywanie lub webhook

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) i documents:send-to-ksef (wysyłka). Odpytywanie wymaga documents: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ów https://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.

  1. 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 sent lub failed, to koniec.
  2. Wciąż processing? Przerwij i przekaż dalej. Pokaż użytkownikowi, że faktura jest w trakcie przetwarzania, przestań odpytywać i poczekaj na webhook document.sentToKSeF albo document.ksefRejected, który dostarczy wynik. Bez pętli odpytywania i bez napierania na limit.
  3. 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 processing i 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