02.10.2026

KSeF API w Node.js / TypeScript – integracja dla nowoczesnych aplikacji webowych i mikroserwisów

KSeF API w Node.js / TypeScript – integracja dla nowoczesnych aplikacji webowych i mikroserwisów

Node.js, dzięki asynchronicznemu modelowi wykonania i naturalnej obsłudze JSON, sprawdza się dobrze wszędzie tam, gdzie trzeba komunikować się z zewnętrznymi API — a integracja z KSeF nie jest tu wyjątkiem. TypeScript dokłada do tego bezpieczeństwo typów, które ma realne znaczenie przy pracy ze strukturą faktury FA(3): łatwiej wyłapać brakujące lub źle nazwane pole na etapie kompilacji, niż dopiero po odrzuceniu dokumentu przez KSeF.

Dlaczego Node.js/TypeScript pasuje do integracji z KSeF

  • Natywna obsługa JSON i fetch — komunikacja z API pośredniczącym (jak KSeFService) odbywa się w formacie JSON lub multipart/form-data, co Node.js obsługuje bez dodatkowych bibliotek, dzięki wbudowanemu fetch (Node.js 18+) i FormData.
  • Model asynchroniczny — wysyłka faktury, oczekiwanie na status i pobranie UPO to naturalnie sekwencja operacji asynchronicznych, którą async/await wyraża czytelniej niż w wielu innych środowiskach.
  • Typy statyczne w TypeScript — zdefiniowanie interfejsów dla struktury faktury i odpowiedzi API pozwala wyłapać niezgodności już w IDE, zanim dokument trafi do wysyłki.
  • Dopasowanie do architektury mikroserwisowej — integrację z KSeF łatwo wydzielić jako osobny, niewielki serwis (np. „invoice-service"), komunikujący się z resztą systemu przez kolejkę lub wewnętrzne API, zamiast wplatać logikę fakturowania w monolit.

Definiowanie typów dla faktury i odpowiedzi API

Pierwszy krok w integracji typowanej to zdefiniowanie kształtu danych — zarówno tego, co wysyłasz, jak i tego, czego oczekujesz w odpowiedzi:

interface InvoiceParty {
  nip: string;
  name: string;
  address: string;
}

interface InvoiceItem {
  name: string;
  quantity: number;
  netPrice: number;
  vatRate: number;
}

interface InvoiceRequest {
  clientId: string;
  login: string;
  password: string;
  api_env: "TEST" | "DEMO" | "PROD";
  seller: InvoiceParty;
  buyer: InvoiceParty;
  items: InvoiceItem[];
  paymentMethod: string;
  issueDate: string;
}

interface KsefApiResponse {
  success: boolean;
  stage: string;
  data: T;
}

interface SendInvoiceData {
  ksefNumber: string;
  sessionReference: string;
}

Dzięki temu każda kolejna funkcja operująca na tych strukturach ma zapewnioną kontrolę typów na etapie kompilacji — literówka w nazwie pola albo brakująca wartość zostaną wychwycone, zanim dojdzie do wywołania API.

Generowanie faktury FA(3) i wysyłka

async function generateFa3(payload: InvoiceRequest): Promise<string> {
  const response = await fetch("https://ksefservice.pl/api/v1/invoices/fa3", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(payload),
  });

  const result: KsefApiResponse<{ xml: string }> = await response.json();

  if (!result.success) {
    throw new Error(`Generowanie FA(3) nie powiodło się: ${result.stage}`);
  }

  return result.data.xml;
}

async function sendInvoice(
  xml: string,
  credentials: Pick<InvoiceRequest, "clientId" | "login" | "password" | "api_env">
): Promise<SendInvoiceData> {
  const form = new FormData();
  form.append("clientId", credentials.clientId);
  form.append("login", credentials.login);
  form.append("password", credentials.password);
  form.append("api_env", credentials.api_env);
  form.append("file", new Blob([xml], { type: "application/xml" }), "invoice_FA3.xml");

  const response = await fetch("https://ksefservice.pl/api/v1/invoices/send", {
    method: "POST",
    body: form,
  });

  const result: KsefApiResponse<SendInvoiceData> = await response.json();

  if (!result.success) {
    throw new Error(`Wysyłka do KSeF nie powiodła się: ${result.stage}`);
  }

  return result.data;
}

Pobranie UPO

async function fetchUpo(
  ksefNumber: string,
  sessionReference: string,
  credentials: Pick<InvoiceRequest, "clientId" | "login" | "password" | "api_env">
): Promise<string> {
  const response = await fetch("https://ksefservice.pl/api/v1/invoices/upo", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ ...credentials, ksefNumber, sessionReference }),
  });

  const result: KsefApiResponse<{ upoXml: string }> = await response.json();

  if (!result.success) {
    throw new Error(`Pobranie UPO nie powiodło się: ${result.stage}`);
  }

  return result.data.upoXml;
}

Obsługa błędów i ponawianie żądań

W środowisku produkcyjnym pojedyncze try/catch wokół wywołania fetch to za mało — warto rozróżnić błędy warte ponowienia (np. chwilowa niedostępność KSeF, kod 5xx) od błędów, które ponowienie tylko powtórzy (błąd walidacji danych, kod 4xx):

async function withRetry<T>(
  fn: () => Promise<T>,
  maxAttempts = 3,
  delayMs = 1000
): Promise<T> {
  let lastError: unknown;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      lastError = err;
      if (attempt < maxAttempts) {
        await new Promise((resolve) => setTimeout(resolve, delayMs * attempt));
      }
    }
  }

  throw lastError;
}

// użycie:
const result = await withRetry(() => sendInvoice(xml, credentials));

Integracja w architekturze mikroserwisowej

W środowisku mikroserwisowym integrację z KSeF warto wydzielić jako osobny, niewielki serwis odpowiedzialny wyłącznie za komunikację z API fakturowym — reszta systemu (obsługa zamówień, panel klienta, system księgowy) komunikuje się z nim przez wewnętrzne API albo kolejkę wiadomości (np. RabbitMQ, Kafka), a nie bezpośrednio z KSeF. Taki podział ma kilka zalet:

  • Izolacja zmian. Aktualizacja logiki integracji z KSeF (np. po zmianie specyfikacji FA(3)) dotyczy tylko jednego serwisu, nie całego systemu.
  • Łatwiejsze skalowanie. Serwis odpowiedzialny za wysyłkę faktur można skalować niezależnie od reszty aplikacji, w zależności od obciążenia.
  • Naturalne miejsce na kolejkowanie. Zamiast wysyłać każdą fakturę synchronicznie w momencie zdarzenia biznesowego, zdarzenie trafia do kolejki, a dedykowany konsument przetwarza je w tempie uwzględniającym limity API KSeF.
// przykładowy konsument kolejki (pseudokod, niezależny od konkretnej biblioteki)
queue.consume("invoice.to_send", async (message) => {
  const invoiceData: InvoiceRequest = JSON.parse(message.content);

  try {
    const xml = await generateFa3(invoiceData);
    const result = await withRetry(() => sendInvoice(xml, invoiceData));
    await notifySuccess(invoiceData, result);
  } catch (err) {
    await notifyFailure(invoiceData, err);
  }
});

Podsumowanie

Node.js i TypeScript dają solidny fundament pod integrację z KSeF — asynchroniczny model dobrze odwzorowuje naturalną sekwencję operacji (generowanie, wysyłka, status, UPO), a typowanie ogranicza klasę błędów wynikających z niezgodności struktury danych, zanim dojdzie do faktycznej wysyłki dokumentu. W architekturze mikroserwisowej integrację warto wydzielić jako osobny serwis, komunikujący się z resztą systemu przez kolejkę — co ułatwia skalowanie i izoluje wpływ przyszłych zmian w specyfikacji KSeF.

Chcesz zobaczyć pełny przykład integracji w innych językach? Sprawdź porównanie PHP, .NET, Java, React i Angular lub zapoznaj się z pełną dokumentacją API.