# Fakturnia Developers — pełna dokumentacja Oficjalna dokumentacja deweloperska zewnętrznego API REST Fakturnia: uwierzytelnianie, fakturowanie, KSeF, webhooki, MCP i maszynowo czytelna dokumentacja OpenAPI. Base URL: https://prod.api.fakturnia.pl/external/v1 --- # Szybki start Wystaw swoją pierwszą fakturę przez API Fakturnia w około pięć minut. Uzyskasz token dostępu, utworzysz nabywcę i fakturę w jednym wywołaniu oraz pobierzesz PDF. > **Adres bazowy.** API ma dwa środowiska. Endpointy w tej dokumentacji są względne wobec adresu bazowego wybranego środowiska: > > - **Produkcja** — `https://prod.api.fakturnia.pl/external/v1` (faktury są prawdziwe i trafiają do KSeF ze skutkiem prawnym). > - **Środowisko testowe (sandbox)** — `https://sandbox.api.fakturnia.pl/external/v1` (do nauki i testów, bez skutków prawnych). ## 0. Zacznij od środowiska testowego (sandbox) **Zanim cokolwiek uruchomisz na produkcji, zacznij od sandboxa.** To pełnoprawna, odizolowana kopia Fakturni, w której możesz testować integrację — łącznie z **wysyłką faktur do KSeF** — bez żadnych skutków prawnych ani podatkowych. Jest to szczególnie istotne, gdy Twój kod pisze (albo współtworzy) LLM: najpierw potwierdź, że wszystko działa w sandboxie, a dopiero potem przełącz się na produkcję. 1. **Załóż osobne konto w środowisku testowym** pod adresem [`https://sandbox.app.fakturnia.pl`](https://sandbox.app.fakturnia.pl). Konto produkcyjne nie działa w sandboxie — to dwa odrębne światy z osobnymi danymi. 2. **Wygeneruj sandboxowe dane API** ([Inne → Integracje → REST API](https://sandbox.app.fakturnia.pl/konto/integracje/rest-api/konta)) na tym koncie testowym. Otrzymasz `client_id` i `client_secret`, które działają **wyłącznie** wobec `https://sandbox.api.fakturnia.pl`. 3. **Podłącz testowy certyfikat KSeF.** Aby przetestować wysyłkę do KSeF, potrzebujesz certyfikatu ze **środowiska testowego KSeF**, a nie produkcyjnego — certyfikat produkcyjny **nie zadziała** wobec `https://sandbox.api.fakturnia.pl`. Zaloguj się na testowy podmiot w portalu testowym Ministerstwa Finansów [`https://ap-test.ksef.mf.gov.pl/web/`](https://ap-test.ksef.mf.gov.pl/web/), wygeneruj certyfikat i podłącz go do swojego konta sandboxowego (Ustawienia → KSeF). Gdy integracja działa w sandboxie, przełączenie na produkcję sprowadza się do zmiany adresu bazowego na `https://prod.api.fakturnia.pl` oraz użycia produkcyjnych danych API i produkcyjnego certyfikatu KSeF. ## 1. Uzyskaj dane uwierzytelniające API REST API jest modułem planu **MAX**: konto, na którym tworzysz aplikację API, potrzebuje aktywnego planu MAX (nowe konto ma 14-dniowy okres próbny MAX, więc w sandboxie zaczniesz od razu). Bez planu endpoint tokenu odpowiada `403 access_denied`. Utwórz **aplikację API** na swoim koncie Fakturnia ([Inne → Integracje → REST API](https://app.fakturnia.pl/konto/integracje/rest-api/konta)). Otrzymasz `client_id` i `client_secret` oraz wybierzesz **scope'y** (zakresy uprawnień), z których aplikacja może korzystać (na przykład `documents:create`, `documents:read`). Traktuj `client_secret` jak hasło. Nigdy nie umieszczaj go w przeglądarce, aplikacji mobilnej ani w publicznym repozytorium. Pracujesz z agentem AI? Wyklucz plik z sekretami z jego zasięgu, żeby nie trafił na zewnętrzne serwery. ## 2. Wymień dane uwierzytelniające na token dostępu API korzysta z przepływu **OAuth 2.0 client-credentials**. Wymień swoje `client_id` / `client_secret` na krótkotrwały token typu bearer (JWT). ```bash curl -s https://prod.api.fakturnia.pl/v1/oauth/token \ -H "Content-Type: application/json" \ -d '{ "grant_type": "client_credentials", "client_id": "client_5c62e069...", "client_secret": "secret_..." }' ``` ```json { "access_token": "eyJhbGciOiJIUzI1NiIsInR...", "token_type": "Bearer", "expires_in": 3600, "scope": "documents:create documents:read clients:create" } ``` Wysyłaj token w nagłówku `Bearer` przy każdym żądaniu: ``` Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR... ``` Zobacz [Uwierzytelnianie](/docs/authentication), aby poznać buforowanie tokenów, ich rotację oraz pełną listę scope'ów. ## 3. Utwórz swoją pierwszą fakturę Nie musisz najpierw tworzyć nabywcy. Przekaż osadzony obiekt `buyer`, a Fakturnia odnajdzie lub utworzy kontrahenta za Ciebie. Sprzedawca jest zawsze pobierany z profilu Twojej firmy. ```bash curl -s -X POST https://prod.api.fakturnia.pl/external/v1/documents \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 7c1f1d6e-0b2a-4a1e-9b1a-2a4e9b1a2a4e" \ -d '{ "documentType": "FakturaVAT", "issueDate": "2026-06-04", "buyer": { "nip": "5261040828", "name": "Acme Corp Sp. z o.o.", "street": "Marszałkowska 1", "postalCode": "00-001", "city": "Warszawa", "countryCode": "PL" }, "products": [ { "name": "Usługa programistyczna", "unitOfMeasure": "szt.", "quantity": 1, "netPrice": 100, "vatRate": 23 } ], "paymentMethodId": 6, "paymentStatusId": 1, "paymentDeadlineDate": "2026-06-18" }' ``` Każda odpowiedź jest opakowana w spójną kopertę: ```json { "success": true, "data": { "document": { "id": "681a3b2c4d5e6f7a8b9c0d1e", "documentNumber": "FV/1/06/2026", "documentType": "FakturaVAT", "grossSum": 123, "netSum": 100, "vatSum": 23, "products": [ { "name": "Usługa programistyczna", "quantity": 1, "netPrice": 100, "netValue": 100, "amountOfVat": 23, "grossValue": 123, "vatRate": 23 } ] } }, "message": "DOCUMENT_CREATED", "executionTime": 142 } ``` > **Ty przesyłasz ceny, a API wykonuje obliczenia.** Przekaż `netPrice` *lub* `grossPrice` oraz `vatRate`; backend wyliczy `netValue`, `amountOfVat` i `grossValue` co do grosza. Dokładne reguły opisano w [Typy dokumentów i kwoty](/docs/documents). Więcej szczegółów i porad znajdziesz w [Tworzenie dokumentów](/docs/creating-documents): każde pole żądania, ustalanie nabywcy i odbiorcy, adnotacje oraz logika dla każdego typu dokumentu. ## 4. Pobierz PDF ```bash curl -s -L https://prod.api.fakturnia.pl/external/v1/documents/681a3b2c4d5e6f7a8b9c0d1e/download \ -H "Authorization: Bearer $TOKEN" -o invoice.pdf ``` ## 5. (Opcjonalnie) Wyślij ją do KSeF Jeśli Twoje konto ma skonfigurowane połączenie z KSeF, prześlij fakturę do Krajowego Systemu e‑Faktur: ```bash curl -s -X POST https://prod.api.fakturnia.pl/external/v1/documents/681a3b2c4d5e6f7a8b9c0d1e/send-to-ksef \ -H "Authorization: Bearer $TOKEN" ``` Wywołanie od razu zwraca `ksefNumber`, gdy KSeF działa szybko, w przeciwnym razie status `processing`. Jak prawidłowo poczekać na wynik — seria odpytań, [webhook](/docs/webhooks) `document.sentToKSeF` i limit zapytań — opisuje [Wysyłka do KSeF](/docs/ksef). ## Następne kroki - **[Uwierzytelnianie](/docs/authentication)**: tokeny, scope'y, bezpieczeństwo. - **[Typy dokumentów i kwoty](/docs/documents)**: faktury, zaliczki, rozliczenia, faktury korygujące oraz reguły obliczeń. - **[Wysyłka do KSeF](/docs/ksef)**: wyślij fakturę i prawidłowo poczekaj na wynik. - **[Webhooki](/docs/webhooks)**: reaguj na zdarzenia zamiast odpytywania. - **[Narzędzia](/docs/tools)**: kolekcja Postman, CLI i generowanie klienta z OpenAPI. - **[Dokumentacja API](/api)**: każdy endpoint, generowany z OpenAPI. --- # Uwierzytelnianie API Fakturnia korzysta z uwierzytelniania w modelu **OAuth 2.0 client credentials**. Aplikacja wymienia parę `client_id` / `client_secret` na krótko żyjący **token Bearer** (JWT), a następnie dołącza ten token do każdego żądania. > **REST API wymaga planu MAX.** Konta API, wszystkie endpointy `/external/v1` i webhooki są modułem planu MAX. Bez aktywnego planu endpoint tokenu odpowiada `403` z `error: "access_denied"` i `error_code: "SUBSCRIPTION_REQUIRED"`, a wywołanie z tokenem wydanym wcześniej kończy się `403 SUBSCRIPTION_REQUIRED`. Po wygaśnięciu planu integracja przestaje działać do czasu jego przedłużenia; webhooki nie są wtedy wysyłane. Konta pracownicze i uprawnienia w aplikacji nie mają na to wpływu: token działa zawsze jako właściciel konta. ## Uzyskiwanie poświadczeń Utwórz aplikację API w sekcji **[Inne → Integracje → REST API](https://app.fakturnia.pl/konto/integracje/rest-api/konta)**. Każda aplikacja posiada: - `client_id` (publiczny identyfikator, np. `client_5c62e069…`), - `client_secret` (sekret, przechowuj go wyłącznie po stronie serwera), - zestaw uprawnień **scope**, które ograniczają zakres działań aplikacji. Możesz prowadzić wiele aplikacji (np. po jednej na każdą integrację) i niezależnie unieważniać dowolną z nich. Unieważnienie działa natychmiast: tokeny wydane wcześniej dostają `401 UNAUTHORIZED` przy kolejnym wywołaniu, nie dopiero po wygaśnięciu. Dla aplikacji możesz też ustawić listę dozwolonych adresów IP; wywołanie z innego adresu kończy się `403 FORBIDDEN`. > **Dane API są przypisane do środowiska.** Poświadczenia z [środowiska testowego](/docs/quickstart) (`https://sandbox.app.fakturnia.pl`) działają wyłącznie wobec `https://sandbox.api.fakturnia.pl`, a poświadczenia produkcyjne wyłącznie wobec `https://prod.api.fakturnia.pl`. Zacznij od sandboxa, a na produkcję przełącz się dopiero po przetestowaniu integracji. ## Żądanie tokenu ```bash curl -s https://prod.api.fakturnia.pl/v1/oauth/token \ -H "Content-Type: application/json" \ -d '{ "grant_type": "client_credentials", "client_id": "...", "client_secret": "..." }' ``` ```json { "access_token": "eyJ…", "token_type": "Bearer", "expires_in": 3600, "scope": "documents:create documents:read" } ``` Tokeny są ważne przez `expires_in` sekund. **Buforuj i ponownie wykorzystuj** token aż do momentu zbliżającego się wygaśnięcia, zamiast żądać nowego przy każdym wywołaniu. Po wygaśnięciu API odpowiada `401 TOKEN_EXPIRED`: pobierz nowy token i ponów żądanie. Endpoint tokenu odpowiada zgodnie z OAuth 2.0: `400 invalid_request` / `unsupported_grant_type` przy błędnym żądaniu, `401 invalid_client` przy błędnych danych (bez rozróżniania, czy `client_id` istnieje), `403 access_denied` z `error_code: "SUBSCRIPTION_REQUIRED"` bez planu MAX oraz `403 access_denied` bez `error_code`, gdy żądanie przychodzi z adresu IP spoza listy dozwolonych dla aplikacji. ```ts let cached: { token: string; exp: number } | null = null; async function getToken(): Promise { const now = Date.now(); if (cached && cached.exp - now > 60_000) return cached.token; const res = await fetch('https://prod.api.fakturnia.pl/v1/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ grant_type: 'client_credentials', client_id: process.env.FAKTURNIA_CLIENT_ID, client_secret: process.env.FAKTURNIA_CLIENT_SECRET, }), }); const json = await res.json(); cached = { token: json.access_token, exp: now + json.expires_in * 1000 }; return cached.token; } ``` ## Używanie tokenu ``` Authorization: Bearer ``` Brakujący lub nieprawidłowy token zwraca `401` z kodem `UNAUTHORIZED`, wygasły `401 TOKEN_EXPIRED`. Prawidłowy token bez wymaganego zakresu uprawnień scope zwraca `403` z kodem `NO_PERMISSIONS`, a `errors[0]` mówi, którego scope brakuje. Kolejność sprawdzeń: token, stan konta API, adres IP (lista dozwolonych), plan MAX, dopiero potem scope. ## Zakresy uprawnień (scope) Każdy punkt końcowy deklaruje wymagany przez siebie scope. Żądaj wyłącznie tych zakresów uprawnień, których potrzebuje Twoja integracja. | Zasób | Zakresy (scope) | |---|---| | Dokumenty | `documents:browse` · `documents:read` · `documents:create` · `documents:update` · `documents:delete` · `documents:download` · `documents:send-to-ksef` · `documents:update-payment-status` · `documents:update-external-reference` | | Klienci | `clients:browse` · `clients:read` · `clients:create` · `clients:update` · `clients:delete` | | Produkty | `products:browse` · `products:read` · `products:create` · `products:update` · `products:delete` | | Magazyn | `warehouse:browse` · `warehouse:create` · `warehouse:update` · `warehouse:delete` · `warehouse:stock:read` · `warehouse:adjust` · `warehouse:wz:create` · `warehouse:documents:create` | | Pracownicy | `employees:browse` · `employees:create` · `employees:update` · `employees:delete` | | Dokumenty cykliczne | `recurring:browse` · `recurring:read` · `recurring:toggle` | | KSeF | `ksef:read` · `ksef:configure` · `ksef:test` | | Ustawienia | `settings:read` · `settings:update` | | Webhooki | `webhooks:read` · `webhooks:create` · `webhooks:update` · `webhooks:delete` | | Konta pocztowe | `email-accounts:browse` · `email-accounts:read` | | Aktywność | `activity:read` | | Firma | `gus:company:read` | Dokładny scope dla każdego punktu końcowego jest podany w [dokumentacji API](/api). ## Lista kontrolna bezpieczeństwa - Przechowuj `client_secret` po stronie serwera. Nigdy nie umieszczaj go w przeglądarkach, aplikacjach mobilnych ani publicznych repozytoriach. - Przechowuj tokeny w pamięci lub krótko żyjącym buforze. Wygasają one, więc ich długotrwałe utrwalanie zwiększa ryzyko bez żadnej korzyści. - Używaj wyłącznie protokołu HTTPS. API odrzuca połączenia nieszyfrowane. - Natychmiast rotuj sekret w przypadku jego wycieku (unieważnij aplikację i utwórz nową); unieważnienie odcina także tokeny już wydane. - Ogranicz źródłowe adresy IP aplikacji, jeśli integracja działa z serwera o stałym adresie. - Ograniczaj uprawnienia: integracja, która jedynie odczytuje dane, nie powinna posiadać `documents:create`. ## Sprawdzenie tokenu `GET /auth/me` zwraca tożsamość oraz przyznane zakresy uprawnień powiązane z bieżącym tokenem. Użyj go po wdrożeniu integracji, żeby potwierdzić, że token działa i ma właściwe uprawnienia, albo przy debugowaniu, do której aplikacji należy dany token. Endpoint wymaga jedynie ważnego tokenu, bez dodatkowego scope'a. ```bash curl -s https://prod.api.fakturnia.pl/external/v1/auth/me \ -H "Authorization: Bearer $TOKEN" ``` ```json { "success": true, "data": { "clientId": "507f1f77bcf86cd799439011", "name": "Integracja z e-Sklepem", "scopes": ["documents:read", "clients:browse"], "userId": "507f1f77bcf86cd799439012" } } ``` --- # Typy dokumentów i kwoty To strona, którą warto przeczytać, zanim zaczniesz budować obsługę fakturowania. Wyjaśnia ona, **jakie typy dokumentów istnieją**, **co przesyłasz** oraz dokładnie **jak obliczane są kwoty**, co do grosza. Poprawne wyliczenia pieniężne to sedno sprawy, dlatego reguły są opisane precyzyjnie. ## Złota zasada: ty przesyłasz ceny, my obliczamy wartości Dla każdej pozycji przesyłasz **cenę jednostkową** oraz **stawkę VAT**. Resztę oblicza backend. Nigdy nie wyliczasz `amountOfVat` ani `grossValue` samodzielnie i nie powinieneś, ponieważ serwer jest jedynym źródłem prawdy, a Twoje liczby tylko by się rozjeżdżały. | Przesyłasz | Znaczenie | |---|---| | `quantity` | ilość w pozycji (wymagane) | | `netPrice` **lub** `grossPrice` | cena jednostkowa przed/po VAT (jedna z nich) | | `vatRate` | stawka VAT (liczba taka jak `23` lub kod specjalny) | | `discount` | opcjonalne, procent `0–100` | Backend zwraca, dla każdej pozycji: | Pole | Sposób obliczenia | |---|---| | `netValue` | `round2(quantity × netPrice × (1 − discount/100))` | | `amountOfVat` | `round2(netValue × rate / 100)` | | `grossValue` | `round2(netValue + amountOfVat)` | Jeśli zamiast tego przesyłasz `grossPrice`, obliczenia przebiegają „w dół”: najpierw `grossValue`, następnie `netValue = round2(gross / (1 + rate/100))` oraz `amountOfVat = round2(gross − netValue)`. ### Zaokrąglanie jest w górę (half-up), do grosza Wszystkie kwoty są zaokrąglane do 2 miejsc po przecinku metodą **zaokrąglania połówek w górę (round half-up)**: ``` round2(x) = Math.round((x + Number.EPSILON) * 100) / 100 ``` Zatem `0.345 → 0.35` (a nie `0.34`). Kolejność działań ma znaczenie na granicy zaokrąglenia: VAT to `round2((net × rate) / 100)`, nigdy `net × (rate/100)`. Dla niektórych wartości obie metody różnią się o grosz. Ten sam algorytm działa w naszej bibliotece `fakturnia-shared`, więc dokument utworzony przez API jest identyczny, co do grosza, z dokumentem utworzonym w aplikacji webowej. > **W każdej pozycji `netValue + amountOfVat` zawsze równa się `grossValue`.** Po zsumowaniu w obrębie dokumentu `netSum + vatSum === grossSum`. Jeśli kiedykolwiek zobaczysz rozbieżność o jeden grosz, jest to błąd. Zgłoś go. ## Stawki VAT `vatRate` przyjmuje procentową wartość liczbową ze słownika lub **kod specjalny**. Stawki liczbowe: `23, 22, 8, 7, 5, 4, 3`. Kody specjalne (VAT = 0, netto = brutto): | Kod | Znaczenie | |---|---| | `zw` | zwolnione (zwolnione) | | `np I`, `np II` | nie podlega (nie podlega) | | `oo` | odwrotne obciążenie (odwrotne obciążenie) | | `0 WDT` | wewnątrzwspólnotowa dostawa towarów | | `0 EX` | eksport | | `0 KR` | krajowa 0% | Nie istnieje samo `0` ani samo `np`; używaj powyższych kodów. Nieznana stawka zwraca `400 VALIDATION_ERROR`. ## Typy dokumentów Przez API dostępnych jest dziesięć typów dokumentów. Pięć typów dawnych (legacy) celowo nie zostało udostępnionych (zamiast nich używaj adnotacji, patrz niżej). | Typ | Czym jest | Wymagane dodatkowe pola | |---|---|---| | `Faktura` | Faktura poza KSeF, dla podmiotów zwolnionych z VAT (dostępna do końca 2026) | - | | `FakturaVAT` | Faktura VAT, odpowiednik faktury podstawowej w KSeF | - | | `FakturaProforma` | Proforma | - | | `FakturaProformaBezVAT` | Proforma bez VAT | - | | `FakturaZaliczka` | Faktura zaliczkowa | `linkedOrderId`, `advancePaymentAmount` | | `FakturaRozliczeniowa` | Faktura rozliczeniowa (końcowa) | `linkedAdvanceInvoiceIds` | | `FakturaKorygujaca` | Faktura korygująca | `correctedInvoiceId` | | `FakturaKorygujacaZaliczkowa` | Korekta faktury zaliczkowej | `correctedInvoiceId`, `correctionAdvanceAmountAfter` | | `FakturaKorygujacaRozliczeniowa` | Korekta faktury rozliczeniowej | `correctedInvoiceId`, `correctionSettlementAmountAfter` | | `Zamowienie` | Zamówienie | - | > **KSeF i przyszłość typów faktur.** `FakturaVAT` to odpowiednik *faktury podstawowej* w KSeF. `Faktura` to dokument, który nie trafia do KSeF, dedykowany podmiotom zwolnionym z VAT — dostępny do końca 2026 roku. Od 1 stycznia 2027 typ `Faktura` przestanie być dostępny, a `FakturaVAT` stanie się typem `FakturaPodstawowa`; przez pewien czas będzie można używać obu typów (tryb legacy). Obecnie używaj wyłącznie `FakturaVAT` jako odpowiednika faktury podstawowej. ## Adnotacje Szczególne tryby rozliczenia VAT nie są osobnymi typami dokumentów. Zgodnie z KSeF FA(3) ustawiasz je w obiekcie `annotations` na dokumencie, niezależnie od jego typu. Dostępne opcje: | Adnotacja | Pole | Znaczenie | |---|---|---| | Mechanizm podzielonej płatności (MPP) | `isSplitPayment` | Oznacza fakturę jako MPP. Wyklucza się z metodą kasową. Osobne konto VAT możesz podać w polach płatności (`vatAccountNumber` i pokrewne). | | Odwrotne obciążenie | `isReverseCharge` | Podatek rozlicza nabywca. | | Metoda kasowa | `isCashMethod` | Rozliczenie VAT w momencie zapłaty. Wyklucza się z MPP. | | Samofakturowanie | `isSelfInvoicing` | Fakturę wystawia nabywca w imieniu sprzedawcy. | | Procedura marży | `marginProcedure` | Obiekt `{ enabled, procedureId }`. Identyfikatory procedur (biuro podróży, towary używane, dzieła sztuki, antyki) znajdziesz w słowniku [`marginProcedures`](/docs/dictionaries#procedury-marzy-marginprocedures). | | Zwolnienie z VAT | `vatExemption` | Obiekt `{ enabled, exemptionType, legalBasis, showOnDocument }`. Typy `A`/`B`/`C` opisuje słownik [`vatExemptionTypes`](/docs/dictionaries#typy-zwolnienia-z-vat-vatexemptiontypes). | Sprzeczne kombinacje (np. MPP razem z metodą kasową) zwracają `422 ANNOTATIONS_VALIDATION_ERROR` (patrz [Błędy](/docs/errors)). Pełny opis pól każdej adnotacji wraz z przykładami żądań znajdziesz w [Tworzenie dokumentów → Adnotacje](/docs/creating-documents#adnotacje-annotations). ## Informacje dodatkowe: faktura do paragonu Obok adnotacji KSeF FA(3) przewiduje sekcję „Informacje dodatkowe" (`additionalInfo`). Obecnie API udostępnia w niej jedno pole: | Pole | Znaczenie | |---|---| | `additionalInfo.isInvoiceToReceipt` | Faktura do paragonu (art. 109 ust. 3d ustawy o VAT). | Ustawienie `isInvoiceToReceipt: true` na dokumencie `FakturaVAT` dodaje flagę `1` w dokumencie wysyłanym do KSeF oraz adnotację „Faktura do paragonu" na PDF. To nadal zwykła faktura VAT, a nie osobny typ dokumentu; kwoty liczone są tak samo. KSeF nie przewiduje pola na numer paragonu, więc API również go nie przyjmuje. Szczegóły znajdziesz w [Tworzenie dokumentów → Informacje dodatkowe](/docs/creating-documents#informacje-dodatkowe-additionalinfo). ## Faktury standardowe `Faktura`, `FakturaVAT`, `FakturaProforma`, `FakturaProformaBezVAT`, `Zamowienie` są obliczane bezpośrednio z ich pozycji: `grossSum = Σ grossValue` i tak dalej. Nic zaskakującego. ## Faktury zaliczkowe (`FakturaZaliczka`) Faktura zaliczkowa księguje **przedpłatę na poczet zamówienia** (`Zamowienie`). **Nie** jest sumą pozycji zamówienia; jej wartością jest `advancePaymentAmount`. - Przekazujesz `linkedOrderId` (istniejące `Zamowienie`) oraz `advancePaymentAmount`. - Backend dzieli zaliczkę **proporcjonalnie pomiędzy stawki VAT zamówienia** i ten podział staje się pozycjami faktury. - Dlatego `grossSum === advancePaymentAmount`, przy czym `netSum` / `vatSum` wynikają z podziału proporcjonalnego. ```json { "documentType": "FakturaZaliczka", "issueDate": "2026-06-04", "buyerId": "681a…", "linkedOrderId": "6820…", "advancePaymentAmount": 615 } ``` Zaliczka nie może przekroczyć wartości brutto zamówienia (`422 ADVANCE_EXCEEDS_ORDER_VALUE`). ## Faktury rozliczeniowe (`FakturaRozliczeniowa`) Faktura rozliczeniowa (końcowa) zamyka zamówienie po jednej lub kilku zaliczkach. Jej wartość brutto to **kwota pozostała do zapłaty**, a nie pełna suma pozycji. - Przekazujesz `linkedAdvanceInvoiceIds` (rozliczane faktury zaliczkowe) oraz pełne `products`. - Backend oblicza `settlementAmount = max(0, Σ products.gross − Σ advances.gross)` (chyba że ustawisz `isSettlementAmountManual: true` i podasz `settlementAmount`). - `grossSum === settlementAmount`; `netSum` / `vatSum` pochodzą z proporcjonalnego podziału tej kwoty pomiędzy stawki VAT produktów. Zatem jeśli Twoje pozycje sumują się do 6 150 brutto, a powiązane zaliczki do 2 460, to `grossSum` faktury rozliczeniowej wynosi `3 690`, a nie `6 150`. ## Korekty > **Pełna instrukcja wystawiania korekt przez API** znajduje się w artykule [Korygowanie dokumentów](/docs/correcting-documents) — trzy typy korekt, wyzerowanie faktury i reguły proporcjonalnego VAT. Korekta przedstawia dokument na nowo. **`products` to pełny stan „po” korekcie**, cały dokument w postaci, jaką powinien mieć po korekcie. Backend oblicza różnicę; nie przesyłasz pozycji „+/−”. - **`FakturaKorygujaca`**: sumy to **różnica pozycji**: `Σ after − Σ corrected`. Aby anulować dokument, prześlij te same pozycje z `quantity: 0`. - **`FakturaKorygujacaZaliczkowa`** koryguje zaliczkę. Sumy to **różnica kwoty zaliczki**: `grossSum = correctionAdvanceAmountAfter − advancePaymentAmountBefore`, z VAT wynikającym z różnicy obu podziałów proporcjonalnych. - **`FakturaKorygujacaRozliczeniowa`** koryguje rozliczenie. `grossSum = correctionSettlementAmountAfter − settlementAmountBefore`, VAT proporcjonalnie. ```json { "documentType": "FakturaKorygujaca", "issueDate": "2026-06-04", "correctedInvoiceId": "681a…", "correctionReason": "Zmiana ilości", "products": [ { "name": "Usługa", "unitOfMeasure": "szt.", "quantity": 2, "netPrice": 100, "vatRate": 23 } ] } ``` > **Dlaczego ma to znaczenie dla integratorów.** Wartości `grossSum` dla faktur zaliczkowych, rozliczeniowych i korekt *nie* są równe `Σ products`. Jeśli odwzorowujesz sumy faktur we własnym systemie, pobieraj je z odpowiedzi API, nie wyliczaj ich ponownie z pozycji. ## Płatność i daty | Pole | Uwagi | |---|---| | `paymentMethodId` | `1` Gotówka · `2` Karta · `3` Bon · `4` Czek · `5` Kredyt · `6` Przelew · `7` Płatność mobilna | | `paymentStatusId` | `0` Opłacone (wymaga `additionalPaymentDate`) · `1` Do zapłaty (wymaga `paymentDeadlineDate`) | | `bankAccounts[]` | dla przelewu; pierwsze staje się rachunkiem głównym, pozostałe są wyświetlane jako dodatkowe | | `issueDate`, `subDate`, `subDateTo` | data wystawienia / data albo okres sprzedaży (ISO `YYYY-MM-DD`) | | `paymentInstallments`, `paymentDeadlineTerm` | raty i termin opisowy faktury do zapłaty, patrz [Tworzenie dokumentów](/docs/creating-documents#raty-paymentinstallments) | | `exchangeRate`, `exchangeRateDate`, `exchangeRateTable` | kurs NBP z dnia roboczego przed obowiązkiem podatkowym, jego dzień i tabela | Zapłatę, która wpłynęła po wystawieniu, oznaczasz przez `PATCH /documents/{id}/payment-status` z `receivedPayment: RECEIVED` i opcjonalnym `paymentDate` (dzień wpłaty, `YYYY-MM-DD`; domyślnie dzisiejszy dzień w Polsce, nie może być z przyszłości). Przy metodzie kasowej to dzień powstania obowiązku podatkowego (art. 21 ust. 1 ustawy o VAT). `GET /documents/{id}` zwraca go w `payment.receivedPaymentDate`. ## Usuwanie i edycja dokumentów To, czy dokument można jeszcze zmienić lub usunąć, zależy od jego statusu w KSeF. `DELETE /documents/{id}` oraz aktualizacje dokumentu działają według tej samej reguły: - Gdy dokument ma już **numer KSeF** (został zatwierdzony w KSeF), jest ostateczny. Usunięcie lub edycja zwraca `400 KSEF_CONFIRMED`. - Gdy dokument jest **wysłany do KSeF i oczekuje na potwierdzenie**, jest zablokowany. Usunięcie lub edycja zwraca `400 KSEF_SENT`. - Jeśli wysyłka do KSeF **się nie powiodła** (dokument niesie błąd KSeF), blokada znika. Możesz usunąć dokument albo poprawić go i wysłać ponownie. Dokumenty, które nigdy nie zostały wysłane do KSeF, zawsze można usunąć lub edytować. Stan dokumentu sprawdzisz przez `GET /documents/{id}/ksef-status` zanim spróbujesz go zmienić. ## Zweryfikuj przed zatwierdzeniem `POST /documents/validate` uruchamia pełny proces (numerację, sumy, ustalenie stron) i zwraca podgląd **bez zapisywania**. Użyj go, aby pokazać użytkownikom ekran potwierdzenia lub przeprowadzić próbne uruchomienie integracji. Zobacz [dokumentację API](/api), aby poznać pełny schemat żądania i wszystkie pola. --- # Tworzenie dokumentów To kompletny przewodnik po `POST /external/v1/documents`, jedynym endpoincie, którego potrzebujesz, aby wystawić fakturę, proformę, zaliczkę, rozliczenie, korektę lub zamówienie. Opisuje każde pole żądania, sposób ustalania stron transakcji, logikę zależną od typu dokumentu oraz adnotacje KSeF. Reguły obliczeń kwot i pełną mapę typów znajdziesz w [Typy dokumentów i kwoty](/docs/documents); tutaj skupiamy się na tym, **jak zbudować poprawne żądanie**. Jeśli dopiero zaczynasz, najpierw przejdź [Szybki start](/docs/quickstart). Pełny, generowany maszynowo schemat każdego pola jest w [Dokumentacji API](/api). ## Zanim zaczniesz - **Scope**: tworzenie dokumentów wymaga uprawnienia `documents:create`. Zobacz [Uwierzytelnianie](/docs/authentication). - **Słowniki to źródło prawdy**: zamiast wpisywać wartości enumów na sztywno, pobierz je raz z `GET /external/v1/dictionaries`. Endpoint zwraca aktualne listy: typy dokumentów, metody i statusy płatności, stawki VAT, kody GTU, waluty, kody krajów, procedury marży, typy zwolnienia z VAT oraz nagłówki dat. Dzięki temu Twoja integracja nie rozjedzie się, gdy dodamy nową pozycję. - **Idempotencja**: każde tworzenie warto wysyłać z nagłówkiem `Idempotency-Key`, żeby ponowienie po przerwanej sieci nie utworzyło duplikatu. Szczegóły w [Idempotentność](/docs/idempotency). > **Pełny spis wartości.** Wszystkie enumy (typy dokumentów, stawki VAT, waluty, kody krajów, GTU, procedury marży, …) opisuje strona [Słowniki](/docs/dictionaries), a na żywo pobierzesz je z `GET /dictionaries`. Słownik zwraca dokładnie te typy dokumentów, które przyjmuje ten endpoint. ## Minimalne żądanie Sam endpoint utworzy dokument już przy `documentType`, `issueDate`, nabywcy z `nip` i `name` oraz jednej pozycji. To jednak nie wystarczy do realnego obiegu: **żeby faktura przeszła wysyłkę do KSeF, nabywca musi mieć pełny adres** (`street`, `postalCode`, `city`, `countryCode`). Dlatego za minimum sensowne uznajemy przykład z adresem. Sprzedawca jest dobierany automatycznie z profilu Twojej firmy. ```bash curl -s -X POST https://prod.api.fakturnia.pl/external/v1/documents \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 7c1f1d6e-0b2a-4a1e-9b1a-2a4e9b1a2a4e" \ -d '{ "documentType": "FakturaVAT", "issueDate": "2026-06-04", "buyer": { "nip": "5261040828", "name": "Acme Corp Sp. z o.o.", "street": "Marszałkowska 1", "postalCode": "00-001", "city": "Warszawa", "countryCode": "PL" }, "products": [ { "name": "Usługa programistyczna", "unitOfMeasure": "szt.", "quantity": 1, "netPrice": 100, "vatRate": 23 } ] }' ``` > **Adres nabywcy a KSeF.** Bez adresu dokument utworzy się i pobierzesz go jako PDF, ale wysyłka do KSeF zostanie odrzucona. Jeśli wystawiasz faktury z myślą o KSeF, traktuj adres nabywcy jako wymagany. Dla osoby fizycznej (`type: "person"`) podaj `firstName` i `lastName` zamiast `name`. Wszystko poza tym jest opcjonalne i ma rozsądne wartości domyślne. Poniżej rozkładamy żądanie na części. ## Numerowanie dokumentów Numer dokumentu składa się z trzech części: **symbolu**, **numeru kolejnego** (`documentNo`) i **części datowej** (`numberingFormat`). Najczęstszy układ to numer, a po nim data: ``` {symbol}{documentNo}{numberingFormat} ``` Na przykład symbol `FV/`, numer `4` i format `/MM/YYYY` dla daty wystawienia `2026-05-07` dają `FV/4/05/2026`. Część datowa może też stać **przed** numerem — zależnie od wybranego formatu (patrz „Dostępne formaty" niżej). ### Trzy elementy numeru | Element | Pole | Co to jest | |---|---|---| | Symbol | `symbol` | Prefiks, np. `FV/`, `KOR/`. Domyślnie z ustawień firmy dla danego typu dokumentu. | | Numer kolejny | `documentNo` | Liczba całkowita ≥ 1. To ona rośnie w obrębie okresu. | | Format | `numberingFormat` | Wzór części „datowej", np. `/MM/YYYY`. Domyślnie z ustawień, fallback `/MM/YYYY`. | W formacie podstawiane są **tylko dwa tokeny**: `MM` (miesiąc, dwie cyfry) i `YYYY` (rok), oba z daty wystawienia. Pozostałe znaki (ukośniki, myślniki, litery) wstawiane są dosłownie: | `numberingFormat` | `issueDate` | Część datowa | Pełny numer (symbol `FV/`, nr `4`) | |---|---|---|---| | `/MM/YYYY` | `2026-05-07` | `/05/2026` | `FV/4/05/2026` | | `/YYYY` | `2026-05-07` | `/2026` | `FV/4/2026` | | `-MM-YYYY` | `2026-05-07` | `-05-2026` | `FV/4-05-2026` | ### Dostępne formaty Pozycja części datowej zależy od wzoru: w wariancie **prefiksowym** data stoi po numerze, w **sufiksowym** przed numerem. Oba są wspierane. W kolumnie „Układ" `NUMER` oznacza `documentNo`. **Miesięczne** (zawierają `MM`): | Format | Układ | Przykład (symbol `FV/`, nr `4`, `2026-05`) | |---|---|---| | `/MM/YYYY` | NUMER/MM/YYYY | `FV/4/05/2026` | | `/YYYY/MM` | NUMER/YYYY/MM | `FV/4/2026/05` | | `-MM-YYYY` | NUMER-MM-YYYY | `FV/4-05-2026` | | `-YYYY-MM` | NUMER-YYYY-MM | `FV/4-2026-05` | | `YYYY/MM/` | YYYY/MM/NUMER | `FV/2026/05/4` | | `MM/YYYY/` | MM/YYYY/NUMER | `FV/05/2026/4` | | `YYYY-MM-` | YYYY-MM-NUMER | `FV/2026-05-4` | | `MM-YYYY-` | MM-YYYY-NUMER | `FV/05-2026-4` | **Roczne** (tylko `YYYY`): | Format | Układ | Przykład (symbol `FV/`, nr `4`, `2026`) | |---|---|---| | `/YYYY` | NUMER/YYYY | `FV/4/2026` | | `-YYYY` | NUMER-YYYY | `FV/4-2026` | | `YYYY/` | YYYY/NUMER | `FV/2026/4` | | `YYYY-` | YYYY-NUMER | `FV/2026-4` | Domyślny format to `/MM/YYYY`. `numberingFormat` jest walidowany względem powyższej listy, więc nieznany wzór jest odrzucany z `400 VALIDATION_ERROR`. Gdy podajesz `documentNumber` ręcznie, musi on odpowiadać układowi wybranego formatu (inaczej `422 NUMBER_MISMATCH`). ### Numeracja automatyczna (domyślnie) Gdy **nie** podasz `documentNo`, numer nadaje backend: - `symbol`, `numberingFormat` i tryb numeracji są brane z ustawień Twojej firmy (każde z nich możesz nadpisać w żądaniu). - Tryb numeracji to **miesięczny** (domyślny) albo **roczny** (ustawienie `numberingType`). Decyduje, kiedy licznik się zeruje: miesięczny prowadzi osobny licznik dla każdego miesiąca (okres `YYYY-MM`), roczny dla każdego roku (okres `YYYY`). - Licznik jest atomowy i osobny dla każdej kombinacji `firma + typ dokumentu + okres`. Pierwszy dokument w okresie dostaje `1`, kolejny `2`, i tak dalej. - **Korekty współdzielą jeden licznik**: `FakturaKorygujaca`, `FakturaKorygujacaZaliczkowa` i `FakturaKorygujacaRozliczeniowa` numerują się wspólnie. > **Trzymaj format zgodny z trybem.** Przy numeracji miesięcznej format powinien zawierać `MM` (np. `/MM/YYYY`). Unikalność numeru jest sprawdzana w obrębie okresu wynikającego z formatu, więc użycie `/YYYY` przy trybie miesięcznym zlewa miesiące w jeden okres. ### Numeracja ręczna Aby narzucić własny numer, podaj `documentNo` (≥ 1). Backend złoży pełny numer z symbolu, numeru i części datowej zgodnie z układem wybranego formatu i sprawdzi, czy numer jest wolny: w swojej serii (typ dokumentu, format numeracji, miesiąc albo rok daty wystawienia) i na całym koncie, bo ten sam pełny numer nie może mieć dwóch dokumentów, także różnych typów albo z różnych miesięcy. Opcjonalnie dołóż `documentNumber` (pełny string) jako asekurację: musi **dokładnie** odpowiadać temu, co backend złoży z `symbol`/`documentNo`/`numberingFormat`, inaczej dostaniesz `422 NUMBER_MISMATCH`. To zabezpieczenie przed rozjechaniem się Twojego numeru z naszym. ```json { "documentType": "FakturaVAT", "issueDate": "2026-05-07", "symbol": "FV/", "documentNo": 4, "numberingFormat": "/MM/YYYY", "documentNumber": "FV/4/05/2026" } ``` > **Numeracja ręczna nie rusza licznika automatycznego.** Jeśli mieszasz oba tryby, pilnuj ciągłości numerów po swojej stronie, żeby nie powstały luki ani kolizje. ### Pola numeracji | Pole | Typ | Wymagane | Uwagi | |---|---|---|---| | `symbol` | string | nie | Prefiks. Domyślnie z ustawień dla danego typu dokumentu. | | `documentNo` | number | nie | Numer kolejny ≥ 1. Pominięty → numeracja automatyczna. | | `numberingFormat` | string | nie | Wzór części datowej (tokeny `MM`, `YYYY`). Domyślnie z ustawień. | | `documentNumber` | string | nie | Pełny numer do walidacji 1:1 z `documentNo`. | ### Błędy numeracji | Status | Kod | Przyczyna | |---|---|---| | `422` | `NUMBER_MISMATCH` | `documentNumber` nie pasuje do numeru, jaki API składa dla układu wybranego formatu (PREFIX `{symbol}{documentNo}{date}` albo SUFFIX `{symbol}{date}{documentNo}`). | | `409` | `DOCUMENT_NUMBER_EXISTS` | Numer jest zajęty w swojej serii albo inny dokument na koncie ma już ten sam pełny numer. | ## Anatomia żądania ### Nagłówek dokumentu | Pole | Typ | Wymagane | Uwagi | |---|---|---|---| | `documentType` | string | tak | Jeden z dziesięciu typów (patrz [Typy dokumentów](#typy-dokumentow-i-ich-pola)). Niezmienny przy edycji. | | `issueDate` | string | tak | Data wystawienia, format ISO 8601 `YYYY-MM-DD`. | | `issuePlace` | string | nie | Miejsce wystawienia. Domyślnie z ustawień firmy. | | `currency` | string | nie | Kod waluty, domyślnie `PLN`. Lista w słowniku `currencies`. | | `exchangeRate` | number | nie | Kurs dla walut innych niż PLN: kurs średni NBP z ostatniego dnia roboczego przed powstaniem obowiązku podatkowego (zwykle przed datą sprzedaży), a gdy faktura jest wcześniejsza, przed datą wystawienia (art. 31a ustawy o VAT). | | `exchangeRateDate` | string | nie | Dzień tabeli NBP, z której jest `exchangeRate` (`YYYY-MM-DD`). | | `exchangeRateTable` | string | nie | Numer tabeli NBP, np. `190/A/NBP/2026`. Drukowany na PDF obok kursu. | | `comments` | string | nie | Uwagi widoczne na dokumencie. W KSeF trafiają do stopki faktury: `Stopka/Informacje/StopkaFaktury`. | | `invoiceName` | string | nie | Nazwa wyświetlana dokumentu. Domyślnie generowana z typu i numeru. | ### Strony transakcji: sprzedawca, nabywca, odbiorca To najczęstsze źródło pytań od integratorów, więc opiszemy je dokładnie. **Sprzedawca (`seller`)** nigdy nie jest przesyłany w żądaniu. Zawsze pochodzi z profilu firmy uwierzytelnionego użytkownika (`user.company`): nazwa, NIP, VAT UE, REGON, KRS, BDO, adres oraz dane osobowe dla jednoosobowej działalności. Aby zmienić dane sprzedawcy, zaktualizuj profil firmy, nie żądanie. NIP nabywcy nie może być równy NIP sprzedawcy (`422`). **Nabywca (`buyer`)** jest wymagany i przekazujesz go na jeden z dwóch sposobów: - `buyerId` (string): identyfikator istniejącego kontrahenta. Pobierz go z `GET /external/v1/clients`. - `buyer` (obiekt): osadzone dane. Backend **wyszukuje** zapisanego kontrahenta (po NIP, numerze VAT UE albo nazwie), a gdy go nie znajdzie, **tworzy nowego** kontrahenta z przekazanych pól. Dzięki temu nie musisz osobno zakładać kontrahenta przed wystawieniem faktury. Identyfikatory podlegają [zasadom identyfikatorów kontrahenta](/docs/clients#identyfikatory-kontrahenta): kontrahenta z innego kraju UE podaj jako `"identifierType": "vatEu", "vatUE": "DE123456789"`, nie w `nip`. Ustalony typ i przeniesione numery zwraca `data.warnings`, a sprzeczne dane `422 PARTY_IDENTIFIER_INVALID`. Podaj `buyerId` **albo** `buyer`, nie oba naraz. **Odbiorca (`receiver`)** jest opcjonalny i wskazuje **odrębny podmiot, który faktycznie odbiera fakturę**, inny niż nabywca. Typowy przypadek to struktury jednostek samorządu terytorialnego: nabywcą jest jednostka nadrzędna posiadająca NIP (np. Gmina), a odbiorcą jednostka podległa, która realnie otrzymuje fakturę (np. oddział urzędu albo Przedszkole Publiczne). Przekazujesz go tak samo jak nabywcę: `receiverId` lub osadzony obiekt `receiver`. Gdy pominiesz odbiorcę, faktura nie ma osobnego odbiorcy i odbiorcą pozostaje nabywca. Odbiorca może być dodatkowo identyfikowany przez **identyfikator wewnętrzny**: ustaw `identifierType` na `idWew` i podaj `idWew` (format `NIP-NNNNN`, np. `5252248481-16006`), co przydaje się m.in. dla jednostek samorządu terytorialnego (JST). Sprzedawca takiej opcji nie ma, w KSeF jest zawsze identyfikowany numerem NIP. #### Obiekt kontrahenta (`buyer` / `receiver`) Ten sam zestaw pól obowiązuje dla `buyer` i `receiver`. Wszystkie pola poza wskazaniem typu są opcjonalne, ale dla poprawnej faktury podaj dane identyfikujące i adresowe. | Pole | Typ | Uwagi | |---|---|---| | `id` | string | Identyfikator istniejącego kontrahenta (alternatywa dla `buyerId`/`receiverId`). | | `type` | string | `company` (domyślnie) lub `person`. Określa, czy używasz `name`, czy `firstName` + `lastName`. | | `name` | string | Nazwa firmy (dla `company`). | | `firstName`, `lastName` | string | Imię i nazwisko (dla `person`). | | `nip` | string | Polski NIP (10 cyfr; spacje, myślniki i prefiks `PL` są usuwane, sprawdzana jest cyfra kontrolna). Używany do wyszukania lub utworzenia kontrahenta. Zagraniczny numer VAT podaj w `vatUE`. | | `vatUE` | string | Numer VAT UE z prefiksem kraju, np. `DE123456789`; zapisywany wielkimi literami, bez spacji. | | `pesel` | string | PESEL osoby fizycznej. | | `regon`, `krs`, `bdo` | string | Pozostałe identyfikatory rejestrowe. | | `street`, `postalCode`, `city` | string | Adres. | | `country`, `countryCode` | string | Kod ISO 3166-1 alpha-2 (np. `PL`, `DE`), domyślnie `PL`. Lista w słowniku `countries`. Do KSeF i PDF liczy się sam `countryCode`; nazwa `country` jest opcjonalna (na PDF i tak dobierana z kodu). | | `email`, `phoneNumber`, `www` | string | Dane kontaktowe. | | `identifierType` | string | Który identyfikator wskazuje stronę w KSeF: `nip` (pole `nip`), `vatEu` (`vatUE`), `other` (`countryCode` i `otherTaxIdentifier`), `idWew` (`idWew`, tylko odbiorca), `none` (brak). Bez niego API ustala typ z danych, zob. [Identyfikatory kontrahenta](/docs/clients#identyfikatory-kontrahenta). | | `otherTaxIdentifier` | string | Identyfikator podatkowy przy `identifierType: "other"`, do 50 znaków, razem z `countryCode` (w KSeF `KodKraju` + `NrID`). | | `idWew` | string | Identyfikator wewnętrzny (format `NIP-NNNNN`, np. `5252248481-16006`), używany gdy `identifierType: "idWew"`. **Może go mieć tylko odbiorca**: w schemacie FA(3) `IDWew` istnieje wyłącznie dla odbiorcy (Podmiot3), nie dla nabywcy ani sprzedawcy. | | `fieldsToShow` | string[] | Które pola pokazać na dokumencie, np. `["nip","regon"]`. | Przykład nabywcy będącego osobą fizyczną: ```json { "buyer": { "type": "person", "firstName": "Jan", "lastName": "Kowalski", "street": "Długa 5", "postalCode": "80-001", "city": "Gdańsk", "countryCode": "PL" } } ``` > **Wskazówka.** Jeśli wystawiasz wiele dokumentów temu samemu kontrahentowi, utwórz go raz przez API kontrahentów i potem przekazuj `buyerId`. Szybciej i bez ryzyka rozjechania danych przy literówce w nazwie. ### Pozycje (`products`) Tablica pozycji. Wymagana dla faktur standardowych, rozliczeniowych i korekt. Dla faktury zaliczkowej pozycje są wyliczane z powiązanego zamówienia, więc jej nie przesyłasz (patrz [niżej](#typy-dokumentow-i-ich-pola)). Dla każdej pozycji przesyłasz cenę i stawkę VAT, a backend dolicza wartości. **Nie wyliczaj `netValue`, `amountOfVat` ani `grossValue` samodzielnie** - serwer jest jedynym źródłem prawdy. Dokładne wzory i zasady zaokrąglania opisuje [Typy dokumentów i kwoty](/docs/documents). | Pole | Typ | Wymagane | Uwagi | |---|---|---|---| | `name` | string | tak | Nazwa towaru lub usługi. | | `unitOfMeasure` | string | tak | Jednostka, np. `szt.`, `godz.`, `kg`. Dowolny tekst, sugerowane wartości w słowniku. | | `quantity` | number | tak | Ilość. Dla korekt dozwolone `0` (anulowanie pozycji). | | `vatRate` | number \| string | tak | Stawka VAT (patrz [Stawki VAT](#stawki-vat)). | | `netPrice` | number | tak\* | Cena jednostkowa netto. | | `grossPrice` | number | tak\* | Cena jednostkowa brutto (alternatywa dla `netPrice`). | | `discount` | number | nie | Rabat procentowy `0-100`. | | `gtu` | string | nie | Kod GTU (`GTU_01` do `GTU_13`). Walidowany względem słownika. | | `pkwiu` | string | nie | Symbol PKWiU. | | `marginPurchaseValue` | number | nie | Tylko `vatRate: "marza"`: kwota nabycia całej pozycji brutto w PLN. Patrz [Procedura marży](#procedura-marzy). | | `marginTaxRate` | number | nie | Tylko `vatRate: "marza"`: stawka VAT od marży, `23`, `8`, `5` albo `0`. Domyślnie `23`. | \* Podaj `netPrice` **lub** `grossPrice`. Jeśli przekazujesz `grossPrice`, obliczenia idą „w dół” od brutto. ### Stawki VAT `vatRate` przyjmuje liczbową stawkę procentową albo kod specjalny (string). Pełną listę z opisami zwraca słownik `vatRates`. - **Stawki procentowe**: `23`, `22`, `8`, `7`, `5`, `4`, `3`. - **Kody specjalne** (VAT = 0, netto = brutto): `zw` (zwolnione), `np I` i `np II` (nie podlega), `oo` (odwrotne obciążenie), `0 KR` (krajowa 0%), `0 WDT` (wewnątrzwspólnotowa dostawa), `0 EX` (eksport), `marza` (procedura marży, opis niżej). Nie istnieje samo `0` ani samo `np`. Nieznana stawka zwraca `400 VALIDATION_ERROR`. ### Procedura marży Faktura w procedurze marży (art. 119 ustawy o VAT dla biur podróży, art. 120 dla towarów używanych, dzieł sztuki, przedmiotów kolekcjonerskich i antyków) pokazuje nabywcy tylko kwoty brutto. Pozycję w marży oznaczasz stawką `vatRate: "marza"`: - podaj `grossPrice` (cena brutto); `netValue` = `grossValue`, `amountOfVat` = `0`, - dokument musi mieć `annotations.marginProcedure` z `enabled: true` i `procedureId` (`1` biura podróży, `2` towary używane, `3` dzieła sztuki, `4` przedmioty kolekcjonerskie i antyki); bez tego `422 MARGIN_LINE_WITHOUT_PROCEDURE`, - w KSeF pozycja nie ma stawki (`P_12`), a jej suma trafia do `P_13_11`, - na jednej fakturze możesz łączyć pozycje w marży ze zwykłymi stawkami (np. biuro podróży: usługi nabyte dla turysty w marży i własna usługa 23%). Podatek należy się od marży, czyli różnicy między ceną a kwotą nabycia. Fakturnia liczy go do rejestru VAT i JPK_V7 (pola `K_19` i `K_20` dla 23%, `MR_T` lub `MR_UZ`), dlatego przy pozycji możesz podać: - `marginPurchaseValue`: kwota nabycia całej pozycji brutto w PLN (towar używany: cena zakupu; biuro podróży: koszty usług kupionych bezpośrednio dla turysty). Nie trafia na fakturę ani do KSeF. Bez niej raporty nie liczą VAT od marży tej pozycji i pokazują ostrzeżenie, a MCP ostrzega `MARGIN_PURCHASE_MISSING`. Kwotę możesz dopisać później przez `PUT /documents/{id}`, - `marginTaxRate`: stawka VAT od marży, `23` (domyślnie), `8`, `5` albo `0`. Kwotę nabycia możesz podać także po wystawieniu, również gdy faktura jest już w KSeF: `PATCH /documents/{id}/margin-purchases` z `{ "lines": [{ "index": 0, "marginPurchaseValue": 1500, "marginTaxRate": 23 }] }`, gdzie `index` to pozycja w `products` (od 0). Zmieniają się tylko te dwa pola, treść faktury zostaje. Pominięty klucz zostawia zapisaną wartość, `null` ją czyści. Pozycja spoza procedury marży daje `422 MARGIN_LINE_NOT_FOUND`, zła wartość `422 MARGIN_PURCHASES_INVALID`. To samo robi narzędzie MCP `updateDocumentMarginPurchases`. Gdy w `PUT` albo `PATCH /documents/{id}` wysyłasz pozycję w marży bez `marginPurchaseValue` i `marginTaxRate`, zostają wartości zapisane przy pozycji o tej samej nazwie. VAT od marży = marża × stawka / (100 + stawka). Ujemna marża daje podatek `0` i nie zmniejsza marży z innych pozycji. W korekcie pominięte pola marży są przejmowane z korygowanej pozycji o tej samej nazwie, a kwota nabycia proporcjonalnie do ilości (zwrot jednej z trzech sztuk zmniejsza ją o jedną trzecią). Pozycja w marży nie ma netto, więc w `netSum` dokumentu liczy się kwotą brutto (KSeF wykazuje ją osobno w `P_13_11`). Podgląd `POST /documents/validate` podaje tę sprzedaż w `preview.totals.marginSaleSum`, a netto zwykłych stawek to `netSum` minus `marginSaleSum`. Bez pozycji w marży `marginSaleSum` wynosi `0`. ```json { "documentType": "FakturaVAT", "annotations": { "marginProcedure": { "enabled": true, "procedureId": 3 } }, "products": [ { "name": "Obraz olejny", "unitOfMeasure": "szt.", "quantity": 1, "grossPrice": 5000, "vatRate": "marza", "marginPurchaseValue": 3000, "marginTaxRate": 23 } ] } ``` ### Płatność | Pole | Typ | Uwagi | |---|---|---| | `paymentMethodId` | number | `1` Gotówka · `2` Karta · `3` Bon · `4` Czek · `5` Kredyt · `6` Przelew · `7` Płatność mobilna | | `paymentStatusId` | number | `0` Zapłacono · `1` Do zapłaty | | `paymentDeadlineDate` | string | Termin płatności. **Wymagany**, gdy `paymentStatusId = 1`. | | `additionalPaymentDate` | string | Data zapłaty. **Wymagana**, gdy `paymentStatusId = 0`. | | `additionalPaymentDateHeaderId` | number | Nagłówek nad datą zapłaty (słownik `paymentDateHeaders`). | | `paymentDescription` | string | Dodatkowy opis płatności. | | `showPaymentSection` | boolean | Czy pokazać sekcję płatności na PDF. Domyślnie `true`. | #### Zapłaty częściowe (`partialPayments`) Gdy do dnia wystawienia nabywca zapłacił część kwoty (albo całość w kilku wpłatach), przekaż listę wpłat. Trafiają one do KSeF jako `ZaplataCzesciowa` i na PDF jako informacja o wpłatach. | Pole | Typ | Znaczenie | |---|---|---| | `partialPayments[].amount` | number | kwota wpłaty brutto | | `partialPayments[].date` | string | data wpłaty (`YYYY-MM-DD`), nie późniejsza niż `issueDate` | | `partialPayments[].paymentMethodId` | number | forma wpłaty jak `paymentMethodId` (opcjonalna) | Reguły zależą od `paymentStatusId`: - `1` (Do zapłaty): suma wpłat musi być **mniejsza** niż kwota do zapłaty; reszta ma termin z `paymentDeadlineDate`. - `0` (Zapłacono): co najmniej dwie wpłaty o sumie **równej** kwocie do zapłaty; `additionalPaymentDate` nie jest wtedy wymagane. Niezgodne wpłaty odpowiadają `422 PARTIAL_PAYMENTS_INVALID`. Gdy faktura ma zapłaty częściowe, data zapłaty całości (`additionalPaymentDate`) nie trafia do KSeF: daty są przy wpłatach. W odpowiedzi `payment` zawiera `partialPayments` (pusta lista, gdy ich nie ma), `paidAmount` (zapłacono według treści faktury) i `amountDue` (pozostało do zapłaty), więc integracja nie musi tego liczyć. ```json { "paymentMethodId": 6, "paymentStatusId": 1, "paymentDeadlineDate": "2026-10-10", "partialPayments": [ { "amount": 500, "date": "2026-09-20", "paymentMethodId": 6 } ] } ``` #### Raty (`paymentInstallments`) Faktura płatna w kilku terminach podaje raty: każda z datą i kwotą. W KSeF każda rata to osobny `TerminPlatnosci`, a kwoty rat trafiają do `DodatkowyOpis` (FA(3) nie ma pola na kwotę raty). Na PDF raty zastępują pojedynczy termin. | Pole | Typ | Znaczenie | |---|---|---| | `paymentInstallments[].date` | string | termin raty (`YYYY-MM-DD`) | | `paymentInstallments[].amount` | number | kwota raty brutto, w walucie dokumentu | Reguły: tylko przy `paymentStatusId = 1`, co najmniej dwie raty, najwyżej 100, terminy po kolei i nie wcześniej niż `issueDate`, suma równa kwocie do zapłaty. Bez `paymentDeadlineDate` terminem płatności dokumentu jest pierwsza rata. Niezgodne raty odpowiadają `422 PAYMENT_INSTALLMENTS_INVALID`. Pusta lista (`[]`) w `PUT` albo `PATCH` usuwa raty. Raty równe policzy za Ciebie API: patrz [Równe raty](#rowne-raty-paymentinstallmentssplit). ```json { "paymentStatusId": 1, "paymentMethodId": 6, "paymentInstallments": [ { "date": "2026-10-20", "amount": 615 }, { "date": "2026-11-20", "amount": 615 } ] } ``` #### Równe raty (`paymentInstallmentsSplit`) Gdy raty mają być równe, nie licz kwot u siebie: podaj liczbę rat, a API podzieli kwotę do zapłaty po wyliczeniu sum dokumentu. Dokument dostaje zwykłe `paymentInstallments` (są w odpowiedzi, w podglądzie `validate` i w KSeF), a samego podziału API nie zapisuje. | Pole | Typ | Znaczenie | |---|---|---| | `paymentInstallmentsSplit.count` | number | liczba rat, od 2 do 100 (wymagana) | | `paymentInstallmentsSplit.firstDate` | string | termin pierwszej raty (`YYYY-MM-DD`), nie wcześniej niż `issueDate`; domyślnie `paymentDeadlineDate` z żądania, a bez niego `issueDate` + 14 dni | | `paymentInstallmentsSplit.every` | string | `month` (co miesiąc, domyślnie), `week` (co 7 dni) albo `14days` (co 14 dni) | Jak API liczy raty: - Kwota do zapłaty (brutto, w walucie dokumentu; na fakturze rozliczeniowej kwota po rozliczeniu zaliczek) jest zamieniana na grosze raz, z zaokrągleniem połówki od zera. - Każda rata dostaje tyle samo groszy, a reszta z dzielenia trafia do ostatniej: 100,00 na 3 raty to 33,33 + 33,33 + 33,34. Suma rat zawsze równa się kwocie do zapłaty. - Raty miesięczne liczone są od pierwszej daty: 31 stycznia, 28 lutego, 31 marca. - Terminem płatności dokumentu (`paymentDeadlineDate`) staje się data pierwszej raty. Reguły: tylko przy `paymentStatusId = 1`. Przy tworzeniu bez `paymentStatusId` dokument dostaje status Do zapłaty; status `0` (Zapłacono) odpowiada błędem. Nie łącz z `paymentInstallments`: raty o różnych kwotach (np. większa pierwsza rata) podaj w `paymentInstallments`. `PATCH` bez `products` dzieli kwotę zapisaną w dokumencie, a bez `firstDate` zaczyna od zapisanego terminu płatności. Kwota mniejsza niż grosz na ratę, zła liczba rat, data albo odstęp odpowiadają `422 PAYMENT_INSTALLMENTS_INVALID`, z opisem w `errors[0]`, np. „Kwota jest za mała na tyle rat.”. `null` oznacza brak podziału. Żądanie (fragment `POST /external/v1/documents`, suma brutto 1 230,00 zł): ```json { "documentType": "FakturaVAT", "issueDate": "2026-10-07", "paymentMethodId": 6, "paymentStatusId": 1, "paymentInstallmentsSplit": { "count": 3, "firstDate": "2026-10-31", "every": "month" } } ``` Odpowiedź (fragment `data.document`): ```json { "paymentDeadlineDate": "2026-10-31", "paymentInstallments": [ { "date": "2026-10-31", "amount": 410 }, { "date": "2026-11-30", "amount": 410 }, { "date": "2026-12-31", "amount": 410 } ] } ``` `POST /external/v1/documents/validate` zwraca te same raty w `preview.paymentInstallments`, więc możesz je pokazać przed wystawieniem. #### Termin opisowy (`paymentDeadlineTerm`) Termin płatności można też opisać słowami, np. „14 dni od dnia wystawienia faktury”. W KSeF trafia do `TerminOpis`, na PDF obok daty terminu. | Pole | Typ | Znaczenie | |---|---|---| | `paymentDeadlineTerm.count` | number | liczba jednostek, liczba całkowita od 1 | | `paymentDeadlineTerm.unit` | string | `day`, `week` albo `month` | | `paymentDeadlineTerm.event` | string | od czego liczy się termin: `issue` (wystawienie faktury), `invoice_delivery` (doręczenie faktury), `supply` (dostawa towaru lub wykonanie usługi), `acceptance` (odbiór) | `paymentDeadlineDate` nadal podaje datę terminu; przy `event: issue` powinna wynikać z opisu. `null` w `PUT` albo `PATCH` usuwa opis. #### Kilka wpłat na fakturze zaliczkowej (`advanceReceipts`) Faktura zaliczkowa może dokumentować kilka wpłat otrzymanych przed jej wystawieniem (w KSeF `ZaliczkaCzesciowa`, przykład 16 z broszury MF). Każda wpłata to osobny moment powstania obowiązku podatkowego (art. 19a ust. 8 ustawy o VAT), więc raport VAT i JPK ujmują je osobno. | Pole | Typ | Znaczenie | |---|---|---| | `advanceReceipts[].date` | string | dzień otrzymania wpłaty, nie późniejszy niż `issueDate` | | `advanceReceipts[].amount` | number | kwota wpłaty brutto | | `advanceReceipts[].exchangeRate` | number | w walucie obcej: kurs NBP z dnia roboczego przed otrzymaniem wpłaty (wymagany) | | `advanceReceipts[].exchangeRateDate` | string | dzień tabeli NBP tego kursu (opcjonalny) | Reguły: tylko `FakturaZaliczka` z `paymentStatusId = 0`, od dwóch do 31 wpłat, suma równa `advancePaymentAmount`. Data otrzymania zapłaty dokumentu to data ostatniej wpłaty. Niezgodne wpłaty odpowiadają `422 ADVANCE_RECEIPTS_INVALID`. #### Rachunki bankowe Dla płatności przelewem (`paymentMethodId = 6`) podaj `bankAccounts`, chyba że masz domyślny rachunek w ustawieniach firmy. Pierwszy rachunek staje się głównym, pozostałe są wyświetlane jako dodatkowe. | Pole | Typ | Wymagane | Uwagi | |---|---|---|---| | `accountNumber` | string | tak | Numer rachunku / IBAN. | | `bank` | string | nie | Nazwa banku. | | `swift` | string | nie | Kod SWIFT dla przelewów zagranicznych. | | `accountDescription` | string | nie | Opis rachunku na dokumencie. | | `bankOwnAccountType` | number | nie | `1`, `2` lub `3`. | Dla mechanizmu podzielonej płatności możesz dodatkowo podać osobne konto VAT: `vatAccountNumber`, `vatAccountBank`, `vatAccountSwift`, `vatAccountDescription`. Samą adnotację MPP włączasz przez `annotations.isSplitPayment` (patrz niżej). ### Daty sprzedaży | Pole | Typ | Uwagi | |---|---|---| | `subDate` | string | Data sprzedaży / dostawy / wykonania usługi (KSeF `P_6`), z `subDateTo` początek okresu. Dla typów KSeF i zamówień domyślnie równa `issueDate`. Korekta bez `subDate` przejmuje datę albo okres sprzedaży faktury korygowanej. | | `subDateTo` | string | Koniec okresu sprzedaży, np. usługa za cały miesiąc (KSeF `OkresFa`). `null` w `PUT` albo `PATCH` wraca do jednej daty. | | `subDateHeaderId` | number | Nagłówek nad datą sprzedaży (słownik `additionalDateHeaders`). | | `taxPointRule` | string | Obowiązek podatkowy z chwilą wystawienia faktury (art. 19a ust. 5 ustawy o VAT): `art19a5p3a` usługi budowlane, `art19a5p3b` dostawa książek i prasy, `art19a5p3c` druk książek i prasy, `art19a5p4` media, telekomunikacja, najem, dzierżawa, leasing, ochrona, stała obsługa prawna i biurowa. Nie trafia do KSeF; decyduje o okresie w raportach VAT i o dniu kursu waluty. `null` oznacza zwykłe zasady. | Daty dokumentu nie mogą sobie przeczyć. API odrzuca z `422 DOCUMENT_DATES_INVALID` (komunikaty w `errors`): - okres sprzedaży, którego początek jest późniejszy niż koniec; - datę sprzedaży późniejszą o więcej niż 60 dni od daty wystawienia (art. 106i ust. 7; okresu rozliczeniowego to nie dotyczy); - datę zapłaty (`additionalPaymentDate`) późniejszą niż data wystawienia przy `paymentStatusId = 0`. Zapłatę, która wpłynęła po wystawieniu, oznacz przez `PATCH /documents/{id}/payment-status` z `paymentDate`; - korektę z datą wystawienia wcześniejszą niż faktura korygowana. Termin płatności wcześniejszy niż data wystawienia jest dozwolony (spóźniona faktura może mieć termin, który już minął). ### Adnotacje (`annotations`) Adnotacje to nowoczesny sposób oznaczania szczególnych trybów rozliczenia VAT zgodny z KSeF FA(3). Zastępują dawne, osobne typy faktur (jak `FakturaMPP` czy `FakturaVATMarza`), których API celowo nie udostępnia. | Pole | Typ | Znaczenie | |---|---|---| | `isSplitPayment` | boolean | Mechanizm podzielonej płatności (MPP). Wyklucza się z `isCashMethod`. | | `isReverseCharge` | boolean | Odwrotne obciążenie. | | `isCashMethod` | boolean | Metoda kasowa. Wyklucza się z `isSplitPayment`. | | `isSelfInvoicing` | boolean | Samofakturowanie. | | `marginProcedure` | obiekt | Procedura marży. | | `vatExemption` | obiekt | Zwolnienie z VAT. | **Procedura marży** (`annotations.marginProcedure`): | Pole | Typ | Uwagi | |---|---|---| | `enabled` | boolean | Włącza procedurę. | | `procedureId` | number | Wymagane, gdy `enabled = true`. Wartości w słowniku `marginProcedures` (np. biuro podróży, towary używane, dzieła sztuki, antyki). | **Zwolnienie z VAT** (`annotations.vatExemption`): | Pole | Typ | Uwagi | |---|---|---| | `enabled` | boolean | Włącza zwolnienie. | | `exemptionType` | string | `A` (przepis ustawy), `B` (dyrektywa 2006/112/WE) lub `C` (inna podstawa prawna). | | `legalBasis` | string | Podstawa prawna jako tekst, np. `art. 43 ust. 1 pkt 19 ustawy o VAT`. | | `showOnDocument` | boolean | Czy pokazać adnotację na PDF. Nie wpływa na dokument wysyłany do KSeF (zwolnienie i tak trafia do `P_19`/`P_19A`/`P_19B`/`P_19C`) — steruje wyłącznie wyświetlaniem na PDF. | ```json { "annotations": { "isSplitPayment": true, "vatExemption": { "enabled": true, "exemptionType": "A", "legalBasis": "art. 113 ust. 1 ustawy o VAT" } } } ``` ### Informacje dodatkowe (`additionalInfo`) Sekcja KSeF FA(3) „Informacje dodatkowe” — osobna od adnotacji. Na razie udostępnia jedno pole: | Pole | Typ | Znaczenie | |---|---|---| | `isInvoiceToReceipt` | boolean | Faktura do paragonu (art. 109 ust. 3d ustawy o VAT). | Gdy ustawisz `isInvoiceToReceipt: true`, w dokumencie wysyłanym do KSeF zostaje ustawiona flaga `1`, a na PDF pojawia się adnotacja „Faktura do paragonu, o której mowa w art. 109 ust. 3d ustawy”. Typ dokumentu (`RodzajFaktury`) nie zmienia się — to zwykła faktura VAT z dodatkowym oznaczeniem, a nie osobny typ dokumentu. KSeF nie przewiduje pola na numer paragonu, więc API również go nie przyjmuje. ```json { "additionalInfo": { "isInvoiceToReceipt": true } } ``` ### Podpisy (`signatures`) Opcjonalne bloki podpisu na PDF. ```json { "signatures": { "issuerSignature": { "show": true, "value": "Jan Nowak" }, "receiverSignature": { "show": false } } } ``` ### Pozostałe pola - `ksefJst` (boolean): dokument dla jednostki samorządu terytorialnego na potrzeby KSeF. ## Wartości domyślne z ustawień konta Dokument wystawiony przez API wygląda tak samo jak nowy dokument z edytora w aplikacji: pola, których nie wyślesz, dostają wartości z ustawień konta. Pole wysłane jawnie zawsze wygrywa (także pusty tekst `""` albo pusty obiekt `{}`), a `useAccountDefaults: false` wyłącza wszystkie wartości domyślne konta. | Pole | Wartość domyślna | |---|---| | `comments` | Uwagi z ustawień dokumentów. | | `issuePlace` | Miejsce wystawienia z ustawień, a gdy puste: miasto z adresu firmy. | | `signatures` | Pola podpisów według ustawienia podpisów; wystawia imię i nazwisko właściciela albo pracownika, który wystawia dokument. | | `annotations` | Automatyczne adnotacje (jeśli włączone): mechanizm podzielonej płatności z rachunkiem VAT (`vatAccountNumber` z rachunku oznaczonego jako VAT) i procedura marży. Korekty ich nie dostają. | | `annotations.vatExemption` | Pozycje ze stawką `zw` dostają podstawę zwolnienia z ustawień (typ A/B/C i podstawa prawna). | | `paymentMethodId` | `6` (przelew) z domyślnym rachunkiem. | | `paymentStatusId` | Domyślny status z ustawień: zapłacono (data zapłaty = data wystawienia) albo do zapłaty (termin = data wystawienia). | | `products[].vatRate` | Domyślna stawka VAT konta. | | `taxPointRule` | Domyślna reguła z ustawienia „Obowiązek podatkowy z datą wystawienia faktury” (Ustawienia → Autouzupełnianie), gdy jest włączone i ma wybraną regułę. Dotyczy faktur sprzedaży w KSeF poza korektami i fakturą zaliczkową; jawne `taxPointRule: null` jej nie dostaje. | | sprzedawca | Widoczne pola sprzedawcy z ustawień (`fieldsToShow`). | | PKWiU na wydruku | Według ustawienia wyświetlania PKWiU. | Odpowiedź `POST /documents/validate` wymienia każdą zastosowaną wartość w `preview.appliedDefaults` (kod, pole, zdanie do pokazania użytkownikowi), a `preview.resolved` pokazuje płatność, miejsce, uwagi i adnotacje po ich uwzględnieniu. **Korekty** przejmują z korygowanej faktury nabywcę, odbiorcę, walutę z kursem i adnotacje (z rachunkiem VAT), jeśli ich nie wyślesz, więc `buyerId` jest dla nich opcjonalne. Faktury, do której istnieje korekta, nie zmienisz ani nie usuniesz (`409 DOCUMENT_CORRECTED`): zmianę wprowadza kolejna korekta. Po usunięciu ostatniej korekty faktura znowu jest edytowalna. **`FakturaProformaBezVAT`** ma zawsze VAT 0 na pozycjach, jak w aplikacji, niezależnie od wysłanej stawki. **`FakturaZaliczka`** nie ma daty sprzedaży. ## Typy dokumentów i ich pola Endpoint przyjmuje dziesięć typów. Poniżej tylko pola dodatkowe wymagane dla danego typu; reguły obliczeń sum dla zaliczek, rozliczeń i korekt opisuje [Typy dokumentów i kwoty](/docs/documents). | Typ | Czym jest | Wymagane dodatkowo | |---|---|---| | `Faktura` | Faktura poza KSeF (podmioty zwolnione, do końca 2026) | - | | `FakturaVAT` | Faktura VAT (odpowiednik faktury podstawowej KSeF) | - | | `FakturaProforma` | Proforma | - | | `FakturaProformaBezVAT` | Proforma bez VAT | - | | `FakturaZaliczka` | Faktura zaliczkowa | `linkedOrderId`, `advancePaymentAmount` (bez `products`) | | `FakturaRozliczeniowa` | Faktura rozliczeniowa (końcowa) | `linkedAdvanceInvoiceIds`, `products` | | `FakturaKorygujaca` | Faktura korygująca | `correctedInvoiceId`, `products` (pełny stan „po”); nabywca z korygowanej faktury | | `FakturaKorygujacaZaliczkowa` | Korekta zaliczki | `correctedInvoiceId`, `correctionAdvanceAmountAfter` | | `FakturaKorygujacaRozliczeniowa` | Korekta rozliczenia | `correctedInvoiceId`, `correctionSettlementAmountAfter` | | `Zamowienie` | Zamówienie | - | Pola powiązane z typami specjalnymi: | Pole | Typ | Dla | |---|---|---| | `linkedOrderId` | string | `FakturaZaliczka`: powiązane `Zamowienie`. | | `advancePaymentAmount` | number | `FakturaZaliczka`: kwota zaliczki (≥ 0.01, ≤ wartość zamówienia). | | `linkedAdvanceInvoiceIds` | string[] | `FakturaRozliczeniowa`: rozliczane faktury zaliczkowe. | | `settlementAmount` | number | `FakturaRozliczeniowa`: kwota do rozliczenia (auto, chyba że `isSettlementAmountManual`). | | `isSettlementAmountManual` | boolean | `FakturaRozliczeniowa`: wymusza ręczną `settlementAmount`. | | `correctedInvoiceId` | string | Korekty: korygowana faktura. | | `correctionReason` | string | Korekty: przyczyna (tekst). | | `correctionAdvanceAmountAfter` | number | `FakturaKorygujacaZaliczkowa`: nowa kwota zaliczki po korekcie. | | `correctionSettlementAmountAfter` | number | `FakturaKorygujacaRozliczeniowa`: nowa kwota rozliczenia po korekcie. | > **Sumy nie zawsze równają się `Σ products`.** Dla zaliczek, rozliczeń i korekt `grossSum` wynika z osobnej logiki. Jeśli odwzorowujesz kwoty u siebie, bierz je z odpowiedzi API, nie wyliczaj ponownie. Pełne wzory w [Typy dokumentów i kwoty](/docs/documents). ## Walidacja na sucho `POST /external/v1/documents/validate` uruchamia pełny proces (numeracja, ustalenie stron, sumy) i zwraca podgląd **bez zapisywania dokumentu**. Idealne, by pokazać użytkownikowi ekran potwierdzenia albo przetestować integrację bez zaśmiecania konta. ## Idempotencja Wyślij nagłówek `Idempotency-Key` (zalecany UUID v4). Powtórzenie tego samego żądania z tym samym kluczem zwróci zapisaną odpowiedź zamiast tworzyć duplikat (cache 24 h). To samo żądanie z innym ciałem i tym samym kluczem zwróci błąd. Zobacz [Idempotentność](/docs/idempotency). ## Najczęstsze błędy | Status | Kod | Przyczyna | |---|---|---| | `400` | `VALIDATION_ERROR` | Niepoprawny lub brakujący typ, data, stawka VAT, struktura pól. | | `403` | `NO_PERMISSIONS` | Token bez scope `documents:create`. | | `404` | `DOCUMENT_NOT_FOUND` | Nieznany `buyerId`, `receiverId`, `correctedInvoiceId` lub `linkedOrderId`. | | `409` | `DOCUMENT_NUMBER_EXISTS` | Numer dokumentu już istnieje. | | `422` | `DOCUMENT_DATES_INVALID` | Daty dokumentu przeczą sobie albo ustawie o VAT (patrz [Daty sprzedaży](#daty-sprzedazy)). | | `422` | `PAYMENT_INSTALLMENTS_INVALID` | Raty nie zgadzają się z kwotą, terminami albo statusem płatności, albo `paymentInstallmentsSplit` nie da się wykonać. | | `422` | `ADVANCE_RECEIPTS_INVALID` | Wpłaty zaliczki nie zgadzają się z kwotą zaliczki, datami albo kursami. | | `422` | - | Reguły domenowe: zaliczka większa niż zamówienie, `isSplitPayment` razem z `isCashMethod`, brak wymaganej daty płatności, NIP nabywcy równy NIP sprzedawcy. | Pełna lista kodów i koperta błędu są w [Błędy](/docs/errors). ## Co dalej - **[Typy dokumentów i kwoty](/docs/documents)**: reguły obliczeń, zaokrąglanie, logika zaliczek, rozliczeń i korekt. - **[Wysyłka do KSeF](/docs/ksef)**: wyślij gotowy dokument i poczekaj na wynik. - **[Idempotentność](/docs/idempotency)**: bezpieczne ponawianie zapisów. - **[Limity zapytań](/docs/rate-limits)** i **[Paginacja](/docs/pagination)**: limity tworzenia i pobierania list. - **[Błędy](/docs/errors)**: kody i obsługa. - **[Dokumentacja API](/api)**: pełny schemat każdego pola. --- # 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](/ksef-flow.pl.svg) ## 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](/docs/documents). > **Testuj najpierw w sandboxie — bez skutków prawnych.** Zanim zaczniesz wysyłać prawdziwe faktury, przećwicz cały przepływ w [środowisku testowym](/docs/quickstart) (`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/`](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](/docs/documents). ## 1. Utwórz fakturę Zwykłe utworzenie dokumentu. Nic specyficznego dla KSeF. ```bash 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 ```bash 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ść](/docs/idempotency). 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): ```json { "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): ```json { "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 ```bash curl -s https://prod.api.fakturnia.pl/external/v1/documents/{id}/ksef-status \ -H "Authorization: Bearer $TOKEN" ``` ```json { "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. ```ts 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](/docs/webhooks) 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ł. ```ts // 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](/docs/documents).) Wyślij ponownie z **nowym** `Idempotency-Key`, bo to nowa próba. ## Wszystko razem ```ts // 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 ``` --- # Korygowanie dokumentów Korekty wystawiasz tym samym endpointem co inne dokumenty, `POST /external/v1/documents`, podając jeden z trzech typów: - `FakturaKorygujaca` — korekta zwykłej faktury, - `FakturaKorygujacaZaliczkowa` — korekta faktury zaliczkowej, - `FakturaKorygujacaRozliczeniowa` — korekta faktury rozliczeniowej. Każda korekta wymaga `correctedInvoiceId` (id faktury korygowanej). Może mieć `correctionCause` (powód: zdarzenie po sprzedaży albo błąd na fakturze), od którego zależy typ korekty w KSeF, oraz `correctionReason` (opis przyczyny), patrz [Powód korekty i jej skutek w KSeF](#powod-korekty-i-jej-skutek-w-ksef). Korekty wysyłasz do KSeF jak zwykłe faktury (rodzaj KOR), patrz [Wysyłka do KSeF](/docs/ksef). > **Wspólna numeracja (zgodnie z KSeF).** Wszystkie trzy typy korekt dzielą **jeden licznik numeracji**. Niezależnie od tego, którą korektę wystawiasz, kolejne numery idą z tej samej puli w obrębie okresu (np. miesiąca). Zasady numeracji opisuje [Tworzenie dokumentów](/docs/creating-documents). ## Zasada delty: przesyłasz stan „po" We wszystkich korektach **podajesz docelowy stan dokumentu po korekcie**, a backend sam liczy różnicę względem faktury korygowanej. Nie przesyłasz pozycji „+/−". Sumy korekty (`netSum`, `vatSum`, `grossSum`) to **delta** i mogą być ujemne (gdy korekta zmniejsza wartość). Bierz je z odpowiedzi API, nie wyliczaj samodzielnie. ## Kolejna korekta tej samej faktury Dokument ma najwyżej jedną korektę. Kolejną zmianę wystawiasz jako **korektę ostatniej korekty** (`correctedInvoiceId` to id tej korekty). Stan „przed” to wtedy stan po poprzedniej korekcie, więc kwoty nie liczą jej drugi raz: po fakturze na 1000 zł netto i korekcie do 900 zł kolejna korekta do 700 zł ma netto −200 zł, nie −300 zł. Nowa korekta dokumentu, który ma już korektę, dostaje `409 CORRECTED_INVOICE_HAS_CORRECTION`. Komunikat w `errors[0]` podaje numer i id ostatniej korekty łańcucha, którą trzeba skorygować: ```json { "success": false, "message": "CORRECTED_INVOICE_HAS_CORRECTION", "errors": ["Dokument FV/1/07/2026 ma już korektę. Ostatnia korekta to KOR/1/08/2026 (id 6820a1b2c3d4e5f6a7b8c9d1): kolejną zmianę wystaw jako korektę tej korekty, żeby stan przed korektą uwzględniał poprzednie zmiany."] } ``` Tak samo odpowiadają `PUT` i `PATCH`, gdy zmieniasz `correctedInvoiceId` korekty na dokument, który ma już inną korektę. W KSeF korekta korekty wskazuje w `DaneFaKorygowanej` **fakturę pierwotną** (jej numer, datę wystawienia i numer KSeF, a gdy była wystawiona poza KSeF, znacznik `NrKSeFN`), a wiersze „przed korektą” i kwoty liczą się od stanu po poprzedniej korekcie. Tak każe Podręcznik KSeF 2.0, część II, punkt 2.13.3. Na wydruku „Korygowany dokument” to faktura pierwotna, a „Poprzednia korekta” wskazuje korektę, od której liczy się stan „przed”. ### Faktura pierwotna bez numeru KSeF Gdy faktura pierwotna nie ma numeru KSeF, korekta trafia do KSeF ze znacznikiem `NrKSeFN`, czyli jako korekta faktury wystawionej poza KSeF. Fakturnia nie wstrzymuje takiej wysyłki i jej nie odrzuca. Na koncie połączonym z KSeF `POST /documents/validate` podaje wtedy w `preview.warnings` jeden z kodów: | Kod | Kiedy | Co zrobić | |---|---|---| | `CORRECTION_ORIGINAL_KSEF_PENDING` | Faktura pierwotna została wysłana albo czeka w kolejce i nie ma jeszcze numeru KSeF. | Wyślij korektę, gdy faktura dostanie numer. | | `CORRECTION_ORIGINAL_NOT_SENT_TO_KSEF` | Faktura pierwotna jest wystawiona dzisiaj i nie została wysłana. | Wyślij najpierw fakturę, potem korektę. Korekta wysłana wcześniej nie połączy się w KSeF z fakturą wysłaną później. | | `CORRECTION_ORIGINAL_OUTSIDE_KSEF` | Faktury pierwotnej nie da się już wysłać do KSeF: Fakturnia wysyła fakturę tylko w dniu wystawienia, a niektóre typy nie trafiają do KSeF. | Nic. Znacznik `NrKSeFN` odpowiada stanowi faktycznemu. | Każdy wpis ma `code` i `message` po polsku, który możesz pokazać użytkownikowi. Lista jest pusta, gdy nie ma uwag. ## Powód korekty i jej skutek w KSeF Korekta może powiedzieć, **dlaczego** ją wystawiasz. Od tego zależy pole `TypKorekty` w XML wysyłanym do KSeF oraz okres, w którym korekta wchodzi do rozliczenia w raportach aplikacji (rejestr VAT, JPK_V7, KPiR). | Pole | Wartości | Znaczenie | |---|---|---| | `correctionCause` | `after_sale`, `mistake`, `null` | `after_sale`: zdarzenie po sprzedaży (rabat, zwrot towaru, reklamacja, cena albo ilość zmieniona po sprzedaży). `mistake`: błąd na fakturze korygowanej (cena, ilość, stawka VAT, kwota albo dane, także faktura dla złego nabywcy lub wystawiona dwa razy). | | `correctionType` | `1`, `2`, `3`, `null` | `TypKorekty` ustawiony ręcznie: `1` skutek w dacie ujęcia faktury pierwotnej, `2` w dacie wystawienia korekty, `3` w innej dacie. Pomiń, a Fakturnia wyliczy go sama. | | `correctionReason` | tekst, najwyżej 256 znaków | Opis przyczyny na PDF i w KSeF (`PrzyczynaKorekty`). Opcjonalny. | | `correctionEffectDate` | `YYYY-MM-DD`, `null` | Dzień, od którego korekta działa w VAT, gdy nie jest to ani data faktury korygowanej, ani data korekty. Przy zwiększeniu po sprzedaży to dzień powstania przyczyny, np. uzgodnienia podwyżki (art. 29a ust. 17); przy `correctionType: 3` dzień skutku. KSeF nie ma na nią pola; raport VAT i JPK_V7 ujmują korektę w tym dniu. | | `correctionConfirmationDate` | `YYYY-MM-DD`, `null` | Korekta zmniejszająca wystawiona poza KSeF (np. dla konsumenta): dzień otrzymania potwierdzenia odbioru przez nabywcę. Obniżka wchodzi do rozliczenia za okres potwierdzenia (art. 29a ust. 13a) i tak ujmuje ją raport VAT. | Gdy nie podasz `correctionType`, Fakturnia wylicza `TypKorekty` przy wysyłce do KSeF: | `correctionCause` | Co korekta robi z kwotami | `TypKorekty` | |---|---|---| | `after_sale` albo brak | podnosi podstawę, `correctionEffectDate` w innym miesiącu niż korekta | `3` | | `after_sale` albo brak | każdy inny przypadek | `2` | | `mistake` | podnosi podstawę opodatkowania albo nie zmienia kwot | `1` | | `mistake` | obniża podstawę opodatkowania | `2` | | `mistake` | zeruje całą fakturę (zły nabywca, duplikat) | `1` | | `mistake` | jedną stawkę podnosi, a inną obniża | `3` | Reguła idzie za ustawą o VAT: zwiększenie podstawy należy do okresu, w którym powstała jego przyczyna (art. 29a ust. 17), czyli przy błędzie do okresu faktury pierwotnej, a przy zdarzeniu po sprzedaży do dnia z `correctionEffectDate`, obniżka wystawiona w KSeF do okresu wystawienia korekty (art. 29a ust. 13 i 14), a korekta do zera faktury, która nie powinna istnieć, do okresu faktury pierwotnej (stanowisko MF z 20.05.2026). W podatku dochodowym korekta błędu wraca do okresu faktury pierwotnej (art. 14 ust. 1m ustawy o PIT). ```json { "documentType": "FakturaKorygujaca", "issueDate": "2026-10-06", "correctedInvoiceId": "681a3b2c4d5e6f7a8b9c0d1e", "correctionCause": "mistake", "correctionReason": "Błędna cena jednostkowa", "products": [ { "name": "Usługa", "unitOfMeasure": "szt.", "quantity": 1, "netPrice": 150, "vatRate": 23 } ] } ``` Ta korekta poprawia błąd i podnosi podstawę, więc trafi do KSeF z `TypKorekty` 1. Na co wpływają pola: - **KSeF.** `TypKorekty` w XML korekty. Bez `correctionCause` i `correctionType` wysyłamy `2`, jak dotychczas, więc istniejące integracje działają bez zmian. - **Raporty w aplikacji.** Rejestr VAT i JPK_V7 umieszczają korektę z `TypKorekty` 1 w okresie faktury korygowanej, a KPiR korektę z powodem `mistake` w okresie przychodu faktury korygowanej. Oba okresy dostają uwagę o przeniesieniu. - **Odczyt.** `GET /documents/{id}` i webhooki `document.created` oraz `document.updated` zwracają oba pola: `null`, gdy nie podano, a `correctionType: null` oznacza typ wyliczany. - **Zmiana.** `PUT` i `PATCH` przyjmują oba pola do czasu wysłania korekty do KSeF. Przy dokumentach, które nie są korektą, pola są pomijane. - **Data i okres sprzedaży.** Korekta bez `subDate` przejmuje datę albo okres sprzedaży faktury korygowanej: FA(3) wypełnia się według stanu po korekcie, a dostawa odbyła się wtedy, kiedy na fakturze pierwotnej. Korekta faktury zaliczkowej nie ma daty sprzedaży. - **Kurs waluty.** Korekta bez `exchangeRate` przejmuje kurs faktury korygowanej razem z dniem i numerem tabeli NBP (art. 31b ust. 1), także gdy podasz tę samą walutę. - **Data wystawienia.** Korekta nie może mieć daty wystawienia wcześniejszej niż faktura korygowana (`422 DOCUMENT_DATES_INVALID`). - **Walidacja.** Wartość spoza słownika i przyczyna dłuższa niż 256 znaków dają błąd walidacji: `400 VALIDATION_ERROR` przy `POST`, `422 VALIDATION_ERROR` przy `PUT` i `PATCH`. Wcześniej za długą przyczynę odrzucał dopiero KSeF przy wysyłce. ## Faktura korygująca (`FakturaKorygujaca`) Najczęstsza korekta: zmiana ilości, ceny, stawki VAT lub danych pozycji. - `products` to **pełny stan pozycji po korekcie** (cały dokument taki, jaki ma być po zmianie). - Sumy: `grossSum = Σ(pozycje po) − grossSum faktury korygowanej` (analogicznie `netSum` i `vatSum`). ```json { "documentType": "FakturaKorygujaca", "issueDate": "2026-06-04", "correctedInvoiceId": "681a3b2c4d5e6f7a8b9c0d1e", "correctionReason": "Zmiana ilości", "products": [ { "name": "Usługa", "unitOfMeasure": "szt.", "quantity": 2, "netPrice": 100, "vatRate": 23 } ] } ``` ### Wyzerowanie / anulowanie faktury Aby anulować dokument (np. po anulowaniu zamówienia), wystaw korektę z **tymi samymi pozycjami, ale `quantity: 0`**, zostawiając `netPrice`/`grossPrice` z faktury pierwotnej. Pozycja z ilością 0 ma wartość 0, więc stan „po" wynosi 0, a delta korekty to pełne odwrócenie faktury do zera. ```json { "documentType": "FakturaKorygujaca", "issueDate": "2026-06-04", "correctedInvoiceId": "681a3b2c4d5e6f7a8b9c0d1e", "correctionReason": "Anulowanie zamówienia", "products": [ { "name": "Usługa", "unitOfMeasure": "szt.", "quantity": 0, "netPrice": 100, "vatRate": 23 } ] } ``` > Cenę jednostkową zostawiasz celowo. KSeF oczekuje pozycji „po" z ceną sprzed korekty i ilością 0, a nie pustej pozycji. Wartość pozycji liczy się z ilości, więc przy `quantity: 0` wynosi 0. ## Korekta faktury zaliczkowej (`FakturaKorygujacaZaliczkowa`) Koryguje wcześniej wystawioną fakturę zaliczkową (zmiana kwoty zaliczki lub pozycji powiązanego zamówienia). Wymagane pola: - `correctedInvoiceId` — korygowana faktura zaliczkowa (musi mieć powiązane zamówienie i `advancePaymentAmount`) albo, przy kolejnej zmianie, ostatnia korekta zaliczkowa tej faktury, - `products` — pozycje zamówienia **po korekcie** (baza proporcjonalnego rozkładu), - `correctionAdvanceAmountAfter` — **nowa** kwota zaliczki po korekcie (≥ 0). Sumy: - `grossSum = correctionAdvanceAmountAfter − kwota zaliczki przed korektą` (`advancePaymentAmount` faktury zaliczkowej albo `correctionAdvanceAmountAfter` poprzedniej korekty zaliczkowej; zamówienie przed korektą to wtedy jej pozycje), - `netSum` / `vatSum` to różnica proporcjonalnych rozkładów VAT obu kwot (patrz [Jak liczone są podatki](#jak-liczone-sa-podatki)). ```json { "documentType": "FakturaKorygujacaZaliczkowa", "issueDate": "2026-06-04", "correctedInvoiceId": "6820a1b2c3d4e5f6a7b8c9d0", "correctionReason": "Zmniejszenie zaliczki", "correctionAdvanceAmountAfter": 500, "products": [ { "name": "Towar A", "unitOfMeasure": "szt.", "quantity": 10, "netPrice": 100, "vatRate": 23 } ] } ``` ## Korekta faktury rozliczeniowej (`FakturaKorygujacaRozliczeniowa`) Koryguje fakturę rozliczeniową (końcową). Wymagane pola: - `correctedInvoiceId` — korygowana faktura rozliczeniowa, - `products` — pozycje **po korekcie**, - `correctionSettlementAmountAfter` — **nowa** kwota rozliczenia po korekcie (≥ 0). Sumy: - `grossSum = correctionSettlementAmountAfter − kwota rozliczenia sprzed korekty`, - `netSum` / `vatSum` to różnica proporcjonalnych rozkładów VAT (jak wyżej). ```json { "documentType": "FakturaKorygujacaRozliczeniowa", "issueDate": "2026-06-04", "correctedInvoiceId": "6830a1b2c3d4e5f6a7b8c9d0", "correctionReason": "Korekta rozliczenia", "correctionSettlementAmountAfter": 1230, "products": [ { "name": "Towar A", "unitOfMeasure": "szt.", "quantity": 10, "netPrice": 100, "vatRate": 23 } ] } ``` ## Korekta faktury pobranej z KSeF Faktura sprzedaży wystawiona w innym programie i pobrana z KSeF (`source: "ksef"`, `direction: "income"`) może być korygowana tak samo jak własna: podaj jej `_id` w `correctedInvoiceId`. API samo czyta pozycje, sumy i adres nabywcy z XML faktury (pobiera go z KSeF, jeśli faktura nie była jeszcze otwierana w aplikacji), więc stan „przed" i wiersze `StanPrzed` w KSeF odpowiadają oryginałowi. W `DaneFaKorygowanej` korekta wskazuje numer KSeF faktury pierwotnej. Ograniczenia: tylko faktura sprzedaży (zakupowej nie koryguje jej odbiorca) i tylko zwykła faktura VAT; faktury zaliczkowe, rozliczeniowe i korekty pobrane z KSeF nie mają w Fakturni danych, z których powstałby stan „przed". ## Jak liczone są podatki **Zwykła korekta (`FakturaKorygujaca`)** liczy sumy wprost, jako różnicę pozycji: `(suma pozycji po) − (suma faktury korygowanej)`, osobno dla netto, VAT i brutto. **Korekty zaliczki i rozliczenia** działają inaczej, bo zaliczka i rozliczenie to **jedna kwota brutto**, którą trzeba rozbić na stawki VAT. Robi to **proporcjonalny rozkład**: 1. Pozycje (zamówienia dla zaliczki, faktury dla rozliczenia) grupowane są po stawce VAT i sumowane brutto. 2. Każda stawka dostaje udział w kwocie proporcjonalny do swojego brutto: `udział = brutto stawki / brutto całości`. Ostatnia stawka dostaje resztę, co eliminuje grosze z zaokrągleń. 3. Z przypadającej na stawkę kwoty brutto liczone są netto i VAT: `netto = brutto / (1 + stawka/100)`, `VAT = brutto − netto`. Stawki specjalne (`zw`, `oo`, `np …`, `0 …`) mają VAT = 0, netto = brutto. Korekta to **różnica dwóch takich rozkładów**: rozkład kwoty „po" minus rozkład kwoty „przed". Dlatego `grossSum` to po prostu różnica kwot, a `netSum` / `vatSum` wynikają z różnicy proporcjonalnych podziałów. Przykład: gdy zaliczka 615 zł (stawka 23%) spada do 500 zł, `grossSum = 500 − 615 = −115`, a netto i VAT to różnica rozkładów obu kwot. > Proporcjonalny rozkład jest identyczny jak przy wystawianiu samej faktury zaliczkowej czy rozliczeniowej, więc korekta zawsze domyka się co do grosza względem dokumentu pierwotnego. ## Najczęstsze błędy | Status | Kod | Przyczyna | |---|---|---| | `422` | `CORRECTED_INVOICE_REQUIRED` | Brak `correctedInvoiceId`. | | `409` | `CORRECTED_INVOICE_HAS_CORRECTION` | Dokument korygowany ma już korektę. Skoryguj jego ostatnią korektę, której numer i id są w `errors[0]`. | | `404` | `CORRECTED_INVOICE_NOT_FOUND` | Nie znaleziono faktury korygowanej. | | `422` | `CORRECTION_ADVANCE_AMOUNT_REQUIRED` | Brak `correctionAdvanceAmountAfter` (≥ 0) dla korekty zaliczki. | | `422` | `CORRECTION_ADVANCE_BASE_MISSING` | Korygowana zaliczka nie ma powiązanego zamówienia / `advancePaymentAmount` (albo korygowana korekta zaliczkowa nie ma pozycji lub `correctionAdvanceAmountAfter`). | | `422` | `CORRECTION_SETTLEMENT_AMOUNT_REQUIRED` | Brak `correctionSettlementAmountAfter` (≥ 0) dla korekty rozliczenia. | | `400` / `422` | `VALIDATION_ERROR` | `correctionCause` albo `correctionType` spoza słownika, `correctionReason` dłuższy niż 256 znaków (`400` przy `POST`, `422` przy `PUT` i `PATCH`). | | `422` | `CORRECTED_INVOICE_NOT_INCOME` | Faktura korygowana to faktura zakupu pobrana z KSeF; korektę wystawia jej wystawca. | | `422` | `CORRECTED_INVOICE_TYPE_MISMATCH` | Typ faktury korygowanej nie pasuje do typu korekty; dla faktury pobranej z KSeF dozwolona jest tylko faktura VAT. | | `422` | `CORRECTED_INVOICE_WITHOUT_POSITIONS` | Nie udało się pobrać pozycji faktury korygowanej z KSeF (np. chwilowa niedostępność KSeF); spróbuj ponownie. | ## Co dalej - [Tworzenie dokumentów](/docs/creating-documents) — pełna anatomia żądania i numeracja. - [Typy dokumentów i kwoty](/docs/documents) — reguły obliczeń i zaokrąglanie. - [Wysyłka do KSeF](/docs/ksef) — wyślij korektę do KSeF. - [Błędy](/docs/errors) — kody i obsługa. --- # Połączenie KSeF Zanim wyślesz pierwszą fakturę do KSeF, konto musi mieć skonfigurowane połączenie: certyfikat, którym Fakturnia uwierzytelnia się w KSeF w imieniu Twojej firmy. Ta strona opisuje zarządzanie tym połączeniem przez API. Samą wysyłkę faktur opisuje [Wysyłka do KSeF](/docs/ksef). Wymagane zakresy uprawnień: `ksef:read` (odczyt stanu), `ksef:configure` (zapis i usunięcie), `ksef:test` (test połączenia). ## Stan połączenia ```bash curl -s https://prod.api.fakturnia.pl/external/v1/ksef/connection \ -H "Authorization: Bearer $TOKEN" ``` ```json { "success": true, "data": { "isAuthorized": true, "hasConnectionConfig": true, "connectionType": "CERTIFICATE", "environment": "prod", "nip": "5252248481", "permissions": ["InvoiceWrite", "InvoiceRead"], "certificateExpiresAt": "2027-03-01T00:00:00.000Z", "lastCheckAt": "2026-07-01T08:12:00.000Z", "lastError": null, "lastErrorAt": null } } ``` Sekrety (zawartość certyfikatu i klucza) nie są nigdy zwracane. `isAuthorized: false` z ustawionym `lastError` oznacza, że ostatnia próba uwierzytelnienia w KSeF się nie powiodła. ## Konfiguracja połączenia ```bash curl -s -X POST https://prod.api.fakturnia.pl/external/v1/ksef/connection \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "certificateContent": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----", "privateKeyContent": "-----BEGIN ENCRYPTED PRIVATE KEY-----\n...\n-----END ENCRYPTED PRIVATE KEY-----", "privateKeyPassword": "haslo-do-klucza", "environment": "prod" }' ``` | Pole | Znaczenie | |---|---| | `certificateContent` | certyfikat KSeF w formacie PEM | | `privateKeyContent` | klucz prywatny w formacie PEM | | `privateKeyPassword` | hasło do klucza prywatnego | | `environment` | `prod`, `test` lub `demo` (domyślnie zgodnie ze środowiskiem API) | Ponowne wywołanie nadpisuje poprzednią konfigurację. Certyfikat wystawiasz w aplikacji KSeF dla NIP swojej firmy; Fakturnia używa go wyłącznie do komunikacji z bramką KSeF. Na [środowisku sandbox](/docs/quickstart) połączenie wskazuje bramkę testową KSeF, więc możesz bezpiecznie przećwiczyć cały przepływ bez skutków prawnych. ## Test połączenia ```bash curl -s -X POST https://prod.api.fakturnia.pl/external/v1/ksef/connection/test \ -H "Authorization: Bearer $TOKEN" ``` Test wykonuje pełne uwierzytelnienie w KSeF i aktualizuje `isAuthorized`, `permissions` oraz `lastError`. Wywołaj go po każdej zmianie konfiguracji, zanim zaczniesz wysyłać faktury. ## Usunięcie połączenia ```bash curl -s -X DELETE https://prod.api.fakturnia.pl/external/v1/ksef/connection \ -H "Authorization: Bearer $TOKEN" ``` Po usunięciu wysyłka do KSeF przestaje działać do czasu ponownej konfiguracji. Wystawione wcześniej dokumenty i ich numery KSeF pozostają bez zmian. --- # Powiązania z zamówieniami Jeśli integrujesz Fakturnię ze sklepem lub innym systemem sprzedaży, możesz powiązać dokument z zamówieniem w Twoim systemie. Powiązanie jest widoczne w aplikacji i pozwala odróżnić dokumenty wystawione przez integrację od tych wystawionych ręcznie. Wymagany zakres uprawnień: `documents:update-external-reference`. ## Ustawienie powiązania ```bash curl -s -X PATCH https://prod.api.fakturnia.pl/external/v1/documents/{id}/external-reference \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "externalSource": "woocommerce", "externalOrderId": "10241", "externalOrderNumber": "ZAM/2026/10241" }' ``` | Pole | Znaczenie | |---|---| | `externalSource` | identyfikator Twojego systemu (np. `woocommerce`, `allegro`, nazwa własna), wymagany | | `externalOrderId` | identyfikator zamówienia w Twoim systemie, wymagany | | `externalOrderNumber` | numer zamówienia widoczny dla ludzi, opcjonalny | Odpowiedź zwraca aktualny stan powiązania: ```json { "success": true, "message": "External reference updated.", "data": { "id": "665f0c...", "externalSource": "woocommerce", "externalOrderId": "10241", "externalOrderNumber": "ZAM/2026/10241" } } ``` ## Usunięcie powiązania ```bash curl -s -X DELETE https://prod.api.fakturnia.pl/external/v1/documents/{id}/external-reference \ -H "Authorization: Bearer $TOKEN" ``` Dokument pozostaje bez zmian, znika tylko powiązanie z zamówieniem. ## Filtrowanie po powiązaniu Na liście dokumentów parametr `onlyUnlinked=true` zwraca wyłącznie dokumenty bez powiązania. To przydatne, gdy dopinasz istniejące dokumenty do zamówień i chcesz znaleźć te jeszcze nieprzypisane: ```bash curl -s "https://prod.api.fakturnia.pl/external/v1/documents?onlyUnlinked=true&buyerNip=5252248481" \ -H "Authorization: Bearer $TOKEN" ``` Pozostałe filtry listy dokumentów (typ, zakres dat, dane nabywcy) opisuje [Paginacja i filtrowanie](/docs/pagination). Typowy przepływ integracji: wystaw dokument przez `POST /documents`, a następnie od razu ustaw powiązanie tym endpointem, używając identyfikatora zamówienia ze swojego systemu. --- # Kontrahenci Kontrahent to nabywca lub odbiorca na Twoich dokumentach. API udostępnia pełny CRUD pod `/clients`, a przy [tworzeniu dokumentów](/docs/creating-documents) możesz wskazać kontrahenta po `id` zamiast przesyłać jego dane za każdym razem. Wymagane zakresy uprawnień: `clients:browse`, `clients:read`, `clients:create`, `clients:update`, `clients:delete` (osobno dla każdej operacji). ## Lista kontrahentów ```bash curl -s "https://prod.api.fakturnia.pl/external/v1/clients?searchQuery=Kowalski¤tPage=1&itemsPerPage=20" \ -H "Authorization: Bearer $TOKEN" ``` `searchQuery` filtruje po nazwie. Lista jest [paginowana](/docs/pagination): odpowiedź zawiera `totalCount` i `totalPages`. ## Tworzenie kontrahenta Kontrahent jest firmą (`type: "company"`) albo osobą (`type: "person"`). Firma wymaga `name`, osoba wymaga `firstName` i `lastName`. ```bash curl -s -X POST https://prod.api.fakturnia.pl/external/v1/clients \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "company", "name": "Przykład Sp. z o.o.", "identifierType": "nip", "nip": "5252248481", "street": "ul. Prosta 1", "postalCode": "00-001", "city": "Warszawa", "countryCode": "PL", "email": "biuro@przyklad.pl" }' ``` Pole `identifierType` mówi, którym identyfikatorem kontrahent jest wskazany w KSeF; zasady opisuje sekcja [Identyfikatory kontrahenta](#identyfikatory-kontrahenta). Pozostałe pola są opcjonalne: `regon`, `krs`, `bdo`, `pesel`, `idNumber`, dane adresowe (`street`, `postalCode`, `city`, `country`, `countryCode`), kontaktowe (`phoneNumber`, `email`, `emailAddresses`, `www`, `fax`) oraz `description`. Dane polskiej firmy możesz pobrać automatycznie z GUS przez [wyszukiwanie po NIP](/docs/company-lookup), zamiast przepisywać je ręcznie. ## Identyfikatory kontrahenta Faktura w KSeF wskazuje nabywcę (i odbiorcę) dokładnie jednym identyfikatorem. `identifierType` wybiera, którym, a API sprawdza i zapisuje go w postaci, którą przyjmie KSeF. Te same zasady obowiązują kontrahentów (`POST` i `PUT /clients`) oraz nabywcę i odbiorcę podanych danymi przy [tworzeniu dokumentów](/docs/creating-documents) (`POST /documents`, `POST /documents/validate`, `PUT` i `PATCH /documents/{id}`). | `identifierType` | Kontrahent | Wymagane pola | Przykład | W KSeF | |---|---|---|---|---| | `nip` | polska firma | `nip` | `"nip": "5261040828"` | `NIP` | | `vatEu` | firma z innego kraju UE | `vatUE` z prefiksem kraju | kontrahent z Niemiec: `"identifierType": "vatEu", "vatUE": "DE123456789"` | `KodUE` + `NrVatUE` | | `other` | firma spoza UE albo z UE bez numeru VAT UE | `countryCode`, `otherTaxIdentifier` | `"countryCode": "US", "otherTaxIdentifier": "12-3456789"` | `KodKraju` + `NrID` | | `idWew` | jednostka wewnętrzna, tylko jako odbiorca dokumentu | `idWew` | `"idWew": "5252248481-16006"` | `IDWew` | | `none` | osoba prywatna albo brak identyfikatora | brak | `"type": "person"` | `BrakID` | Jak API zapisuje identyfikatory: - **NIP** to 10 cyfr: spacje, myślniki i prefiks `PL` są usuwane (`PL 526-104-08-28` zapisuje się jako `5261040828`), a cyfra kontrolna musi się zgadzać. - **Numer VAT UE** jest zapisywany w `vatUE` z prefiksem kraju, wielkimi literami i bez spacji (`de 123 456 789` to `DE123456789`). Numer bez prefiksu dostaje prefiks z `countryCode`. Grecja ma w numerach VAT prefiks `EL` (podany `GR` zamieniamy na `EL`), a `countryCode` pozostaje `GR`. Po prefiksie dozwolone są cyfry, wielkie litery, `+` i `*`, od 1 do 12 znaków. Pole `nip` nigdy nie przechowuje zagranicznego numeru. - **Polska firma z numerem VAT UE** (np. do faktur WDT) ma `identifierType: "nip"`, NIP w `nip` i `PL` z tym samym NIP-em w `vatUE`. - **Irlandia Północna** (`XI`) nie jest krajem UE w KSeF: podaj ją jako `other` z `countryCode: "XI"`. - **`other`** wymaga kraju (`countryCode`, kod ISO, np. `US`, `CH`, `GB`, `UA`) i identyfikatora do 50 znaków; identyfikator zostaje zapisany tak, jak go podasz. - **`idWew`** ma postać `NIP-NNNNN` i przysługuje tylko odbiorcy: nabywca z `idWew` dostaje błąd. ### Gdy nie podasz identifierType API ustala typ z danych, a każde ustalenie zwraca w `data.warnings`, żeby integracja mogła zacząć wysyłać typ jawnie: | Dane | Ustalony typ | Co zapisuje API | |---|---|---| | `nip` z polskim NIP-em (10 cyfr, także z `PL`, spacjami albo myślnikami) | `nip` | NIP jako 10 cyfr | | `nip` z numerem VAT UE kraju UE, np. `DE123456789` | `vatEu` | numer w `vatUE`, pusty `nip`, `countryCode` z prefiksu, gdy brak | | `nip` z numerem bez prefiksu i `countryCode` kraju UE innego niż Polska, np. `123456789` i `DE` | `vatEu` | `vatUE` = `DE123456789` | | `vatUE` przy `countryCode` kraju UE innego niż Polska, albo sam `vatUE` z prefiksem kraju UE | `vatEu` | numer w `vatUE` | | sam `vatUE` z prefiksem `PL` | `nip` | NIP z numeru VAT UE | | `nip` albo `vatUE` z numerem kraju spoza UE (np. `GB…`, `XI…`) albo `nip` przy `countryCode` spoza UE | `other` | numer w `otherTaxIdentifier` | | sam `otherTaxIdentifier` | `other` | bez zmian | | sam `idWew` | `idWew` | bez zmian | | brak identyfikatora | `none` | bez zmian | Polski NIP z błędną cyfrą kontrolną jest odrzucany także wtedy, gdy typ został ustalony. ### Ostrzeżenia w odpowiedzi `POST` i `PUT /clients` zwracają `data.warnings` obok `data.client`, a dokumenty obok `data.document` (przy `validate` obok `data.preview`). Lista jest pusta, gdy identyfikatory podano jawnie i poprawnie. ```json { "success": true, "data": { "client": { "identifierType": "vatEu", "nip": null, "vatUE": "DE123456789", "countryCode": "DE" }, "warnings": [ { "code": "IDENTIFIER_TYPE_INFERRED", "field": "identifierType", "message": "Nie podano identifierType: przyjęto vatEu (numer VAT UE). Podawaj identifierType jawnie." }, { "code": "IDENTIFIER_MOVED", "field": "vatUE", "message": "Numer DE123456789 przeniesiono z pola nip do vatUE: pole nip jest tylko na polski NIP." }, { "code": "COUNTRY_CODE_INFERRED", "field": "countryCode", "message": "Nie podano countryCode: przyjęto DE z prefiksu numeru VAT UE DE123456789." } ] } } ``` | `code` | Znaczenie | |---|---| | `IDENTIFIER_TYPE_INFERRED` | nie podano `identifierType`, API je ustaliło | | `IDENTIFIER_MOVED` | numer z niewłaściwego pola przeniesiono do `vatUE` albo `otherTaxIdentifier` | | `IDENTIFIER_IGNORED` | pole niepasujące do typu pominięto (np. `nip` przy `vatEu`) | | `COUNTRY_CODE_INFERRED` | `countryCode` przyjęto z prefiksu numeru | | `VAT_UE_PREFIX_NORMALIZED` | grecki prefiks `GR` zamieniono na `EL` | Przy dokumentach `field` ma prefiks strony, np. `buyer.vatUE`. ### Sprzeczne dane Sprzeczności API odrzuca z nazwą pola w `errors`: kontrahenci z kodem `422 VALIDATION_ERROR`, dokumenty z kodem `422 PARTY_IDENTIFIER_INVALID` (pole z prefiksem `buyer.` albo `receiver.`). Przykłady: `identifierType: "nip"` z numerem `DE123456789`, `vatEu` z numerem `PL…` albo `XI…`, numer VAT UE bez prefiksu i bez kraju UE, NIP o złej długości albo z błędną cyfrą kontrolną, różne numery VAT UE w `nip` i `vatUE`, `other` bez kraju, `idWew` u nabywcy, nieznana wartość `identifierType`. Kontrahent zapisany wcześniej z danymi, których dziś API by nie przyjęło, pozostaje edytowalny: `PUT` nie sprawdza ponownie identyfikatorów równych zapisanym. Nabywca podany danymi z takim samym błędnym NIP-em jak zapisany kontrahent jest brany z kartoteki. ## Pobieranie, aktualizacja i usuwanie ```bash # Szczegóły curl -s https://prod.api.fakturnia.pl/external/v1/clients/{id} \ -H "Authorization: Bearer $TOKEN" # Aktualizacja (PUT, pełne zastąpienie pól) curl -s -X PUT https://prod.api.fakturnia.pl/external/v1/clients/{id} \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "company", "name": "Przykład Sp. z o.o.", "email": "faktury@przyklad.pl" }' # Usunięcie curl -s -X DELETE https://prod.api.fakturnia.pl/external/v1/clients/{id} \ -H "Authorization: Bearer $TOKEN" ``` `PUT` działa jak pełna aktualizacja: prześlij komplet pól, które kontrahent ma mieć po zmianie. Usunięcie kontrahenta nie zmienia wystawionych wcześniej dokumentów, dane nabywcy są na nich utrwalone. ## Webhooki Operacje zapisu emitują [zdarzenia webhook](/docs/webhooks): `client.created`, `client.updated`, `client.deleted`. --- # Produkty Produkty i usługi to katalog pozycji, z których korzystasz przy wystawianiu dokumentów. API udostępnia pełny CRUD pod `/products`. Wymagane zakresy uprawnień: `products:browse`, `products:read`, `products:create`, `products:update`, `products:delete` (osobno dla każdej operacji). ## Lista produktów ```bash curl -s "https://prod.api.fakturnia.pl/external/v1/products?searchQuery=hosting¤tPage=1&itemsPerPage=20" \ -H "Authorization: Bearer $TOKEN" ``` `searchQuery` filtruje po nazwie. Lista jest [paginowana](/docs/pagination). ## Tworzenie produktu Jedyne pole wymagane to `name`. Kwoty podajesz tak jak przy pozycjach dokumentu: wystarczy `netPrice` albo `grossPrice` razem z `vatRate`, a API dolicza brakujące wartości według tych samych reguł co przy [tworzeniu dokumentów](/docs/documents). ```bash curl -s -X POST https://prod.api.fakturnia.pl/external/v1/products \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Hosting WWW (rok)", "unitOfMeasure": "szt.", "netPrice": 400, "vatRate": 23, "currency": "PLN", "gtu": "GTU_12" }' ``` | Pole | Znaczenie | |---|---| | `name` | nazwa produktu lub usługi (wymagana) | | `netPrice` / `grossPrice` | cena jednostkowa netto lub brutto | | `vatRate` | stawka VAT: liczba (np. `23`) lub kod specjalny ze [słowników](/docs/dictionaries) (np. `zw`, `oo`) | | `quantity`, `discount` | domyślna ilość i rabat procentowy | | `unitOfMeasure` | jednostka miary (np. `szt.`, `godz.`) | | `currency` | waluta, domyślnie `PLN` | | `gtu` | kod GTU, jeśli produkt go wymaga | | `pkwiu` | symbol PKWiU | ## Pobieranie, aktualizacja i usuwanie ```bash # Szczegóły curl -s https://prod.api.fakturnia.pl/external/v1/products/{id} \ -H "Authorization: Bearer $TOKEN" # Aktualizacja (PUT, pełne zastąpienie pól) curl -s -X PUT https://prod.api.fakturnia.pl/external/v1/products/{id} \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Hosting WWW (rok)", "netPrice": 450, "vatRate": 23 }' # Usunięcie curl -s -X DELETE https://prod.api.fakturnia.pl/external/v1/products/{id} \ -H "Authorization: Bearer $TOKEN" ``` Usunięcie produktu nie zmienia dokumentów, na których został wcześniej użyty: pozycje dokumentu przechowują kopię danych produktu z chwili wystawienia. ## Pola magazynowe Produkt ma pola modułu Magazyn: `productType` (`goods` = towar prowadzący stany, `service` = usługa; brak = usługa), `sku`, `ean` i `purchasePrice` (cena zakupu netto, domyślna cena przyjęć PZ/PW). Przy tworzeniu podajesz je razem z resztą danych; przy aktualizacji zapisywane są tylko wtedy, gdy są w żądaniu, więc klient, który ich nie zna, ich nie wyczyści. `quantity` jest opcjonalne (domyślnie 1). Dokumenty magazynowe: [Magazyn](/docs/warehouse). ## Webhooki Operacje zapisu emitują [zdarzenia webhook](/docs/webhooks): `product.created`, `product.updated`, `product.deleted`. --- # Ustawienia Endpoint `/settings` pozwala odczytać i zmienić ustawienia konta, które wpływają na dokumenty wystawiane przez API: format daty oraz numerację dokumentów. Wymagane zakresy uprawnień: `settings:read` (odczyt) i `settings:update` (zapis). ## Odczyt ustawień ```bash curl -s https://prod.api.fakturnia.pl/external/v1/settings \ -H "Authorization: Bearer $TOKEN" ``` ```json { "success": true, "data": { "general": { "dateFormat": "dd.MM.yyyy", "newsletter": true, "dateFormatOptions": ["dd.MM.yyyy", "yyyy-MM-dd", "..."] }, "numbering": { "numberingType": "MONTH", "numberingFormat": "/MM/YYYY", "numberingTypeOptions": ["MONTH", "YEAR"], "numberingFormatOptions": ["/MM/YYYY", "/YYYY", "..."], "documentSymbols": { "FakturaVAT": "FV", "FakturaKorygujaca": "KOR" } } } } ``` Pola `*Options` to dopuszczalne wartości dla zapisu. Traktuj je jak słownik: pobierz przed wyświetleniem formularza użytkownikowi zamiast wpisywać wartości na sztywno. ## Ustawienia ogólne ```bash curl -s -X PATCH https://prod.api.fakturnia.pl/external/v1/settings/general \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "dateFormat": "yyyy-MM-dd" }' ``` | Pole | Znaczenie | |---|---| | `dateFormat` | format dat na PDF dokumentów, jedna z wartości `dateFormatOptions` | | `newsletter` | zgoda na newsletter produktowy (`true`/`false`) | ## Numeracja dokumentów ```bash curl -s -X PATCH https://prod.api.fakturnia.pl/external/v1/settings/numbering \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "numberingType": "MONTH", "numberingFormat": "/MM/YYYY", "documentSymbols": { "FakturaVAT": "FV", "FakturaKorygujaca": "KOR" } }' ``` | Pole | Znaczenie | |---|---| | `numberingType` | `MONTH` (licznik resetowany co miesiąc) lub `YEAR` (co rok) | | `numberingFormat` | format sufiksu numeru, jedna z wartości `numberingFormatOptions` | | `documentSymbols` | symbol (prefiks) numeru dla każdego typu dokumentu | Wszystkie trzy pola są wymagane w tym żądaniu. Przy `numberingType: "MONTH"` format musi zawierać `MM`, inaczej dostaniesz `400`. Numeracja działa w połączeniu z automatycznym nadawaniem numerów przy [tworzeniu dokumentów](/docs/creating-documents): jeśli nie podasz numeru, API nada kolejny zgodnie z tymi ustawieniami. --- # Magazyn Moduł magazynowy prowadzi stany towarów metodą księgi ruchów: stan produktu to suma przyjęć i wydań. Przez API zarządzasz magazynami, wystawiasz dokumenty magazynowe (WZ, PZ, PW, RW, MM), rejestrujesz korekty stanów, pobierasz raport stanów na dowolny dzień i wystawiasz dokumenty WZ do faktur sprzedaży. Moduł wymaga planu **MAX** na koncie Fakturnia. Bez niego każdy endpoint magazynowy zwraca `403` z kodem `SUBSCRIPTION_REQUIRED`. W planie MAX nie ma limitu liczby magazynów. Zakresy uprawnień: `warehouse:browse`, `warehouse:create`, `warehouse:update`, `warehouse:delete`, `warehouse:stock:read`, `warehouse:adjust`, `warehouse:wz:create`, `warehouse:documents:create`. ## Magazyny ```bash # Lista curl -s https://prod.api.fakturnia.pl/external/v1/warehouse \ -H "Authorization: Bearer $TOKEN" # Utworzenie curl -s -X POST https://prod.api.fakturnia.pl/external/v1/warehouse \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Magazyn główny", "symbol": "MAG1", "address": "ul. Prosta 1, Warszawa" }' ``` Pierwszy utworzony magazyn automatycznie staje się domyślny. `symbol` trafia do numeracji dokumentów magazynowych. Aktualizacja to `PUT /warehouse/{id}`, usunięcie to `DELETE /warehouse/{id}`. Usunięcie jest zablokowane (`409 CONFLICT`) w dwóch sytuacjach: magazyn ma ruchy magazynowe albo jest jedynym magazynem na koncie. ## Dokumenty magazynowe (WZ, PZ, PW, RW, MM) `POST /warehouse/documents` wystawia dokument magazynowy i od razu księguje jego ruchy. `POST /warehouse/documents/validate` przyjmuje to samo ciało i zwraca podgląd bez zapisu: numer, magazyny, pozycje z kartoteki ze stanem przed i po oraz braki towaru. Scope: `warehouse:documents:create`. ```bash curl -s -X POST https://prod.api.fakturnia.pl/external/v1/warehouse/documents \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "documentType": "PZ", "issueDate": "2026-09-27", "buyerId": "665d0a1234567890abcdef56", "products": [ { "productId": "665f0c1234567890abcdef12", "quantity": 20, "netPrice": 45.5 } ] }' ``` | Typ | Znaczenie | Kontrahent (`buyerId` / `buyer`) | Magazyn | |---|---|---|---| | `WZ` | wydanie zewnętrzne | odbiorca | `warehouseId` (domyślnie magazyn domyślny) | | `PZ` | przyjęcie zewnętrzne | dostawca | `warehouseId` (domyślnie magazyn domyślny) | | `PW` | przychód wewnętrzny | brak | `warehouseId` (domyślnie magazyn domyślny) | | `RW` | rozchód wewnętrzny | brak | `warehouseId` (domyślnie magazyn domyślny) | | `MM` | przesunięcie między magazynami | brak | `sourceWarehouseId` (domyślnie domyślny) i `targetWarehouseId` (wymagany) | Zasady: - **Pozycje wskazują produkty z kartoteki** (`productId`); na ten produkt księgowany jest ruch. Nazwa, jednostka i cena pochodzą z kartoteki, jeśli ich nie podasz: przy PZ/PW cena zakupu (`purchasePrice`), przy WZ/RW/MM cena sprzedaży netto. Usługa użyta na dokumencie magazynowym zostaje oznaczona w kartotece jako towar. - **Braki towaru blokują wydanie.** WZ, RW i MM, które zeszłyby ze stanem poniżej zera (stan minus rezerwacje innych dokumentów oczekujących), są odrzucane z `422 STOCK_SHORTAGE`, jak w aplikacji. `allowNegativeStock: true` wystawia dokument mimo braku. - **Ustawienia konta działają jak w edytorze**: symbol i numeracja typu, miejsce wystawienia, uwagi, podpisy z ustawienia podpisów dokumentów magazynowych, data wydania / otrzymania towaru (WZ, PZ) i tryb potwierdzania wydań (`stockStatus: "pending"` rezerwuje towar do potwierdzenia w aplikacji). `useAccountDefaults: false` wyłącza wartości domyślne; każde pole podane jawnie wygrywa. - **WZ do faktury**: `sourceInvoiceId` wiąże dokumenty w obie strony, a numer WZ trafia na fakturę. Bez `buyerId` odbiorca jest brany z faktury. - Nieznany albo cudzy magazyn, MM bez magazynu docelowego albo z tym samym magazynem: `422 WAREHOUSE_INVALID`. Nieznany produkt: `404 PRODUCT_NOT_FOUND`. - Pozycji wystawionego dokumentu magazynowego nie zmienisz przez `PUT`/`PATCH /documents/{id}` (`409 WAREHOUSE_LINES_LOCKED`): usuń dokument i wystaw nowy. Zmiana daty przesuwa ruchy, a `DELETE /documents/{id}` je cofa. ## Korekty stanów (inwentaryzacja) Ręczna korekta tworzy ruch typu `INW`. To także sposób na bilans otwarcia przy starcie modułu: ```bash curl -s -X POST https://prod.api.fakturnia.pl/external/v1/warehouse/adjustments \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "productId": "665f0c1234567890abcdef12", "warehouseId": "665e0b1234567890abcdef34", "quantity": 10, "unitCost": 80, "note": "Bilans otwarcia" }' ``` | Pole | Znaczenie | |---|---| | `productId` | ID produktu z kartoteki ([Produkty](/docs/products)) | | `warehouseId` | ID magazynu | | `quantity` | zmiana stanu: dodatnia = przyjęcie, ujemna = wydanie | | `unitCost` | koszt jednostkowy netto, podstawa wyceny dla przyjęć (opcjonalny) | | `date` | data ruchu `YYYY-MM-DD`, domyślnie dzisiaj | | `note` | opis widoczny w historii ruchów | ## Raport stanów ```bash curl -s "https://prod.api.fakturnia.pl/external/v1/warehouse/stock?asOf=2026-06-30" \ -H "Authorization: Bearer $TOKEN" ``` ```json { "success": true, "data": { "levels": [ { "productId": "665f0c...", "warehouseId": "665e0b...", "quantity": 9 } ], "asOf": "2026-06-30" } } ``` Bez parametrów zwraca stan bieżący. `asOf` daje stan na koniec wskazanego dnia (np. do remanentu), `warehouseId` ogranicza raport do jednego magazynu. ## Automatyczne WZ do faktury Po wystawieniu faktury sprzedaży możesz jednym wywołaniem utworzyć dokument WZ i zdjąć towar ze stanu: ```bash curl -s -X POST https://prod.api.fakturnia.pl/external/v1/warehouse/wz-from-invoice/{invoiceId} \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "warehouseId": "665e0b1234567890abcdef34" }' ``` Zasady: - Na WZ trafiają wyłącznie pozycje **towarowe**: takie, które istnieją w kartotece produktów i mają tam ustawione `productType: "goods"`. Usługi i pozycje spoza kartoteki są pomijane, a ich nazwy wracają w polu `skipped`. - Jeśli żadna pozycja nie jest towarem, dostaniesz `422` z podpowiedzią, żeby oznaczyć produkty jako towar w kartotece. - Do jednej faktury może istnieć jeden WZ; ponowna próba zwraca `409 CONFLICT`. - `warehouseId` jest opcjonalne, domyślnie używany jest magazyn domyślny; nieznany magazyn to `422 WAREHOUSE_INVALID`. `issueDate` domyślnie jest datą faktury. - Dokumenty są powiązane w obie strony (numer WZ trafia na fakturę i do KSeF), a ruchy magazynowe (wydanie) powstają automatycznie. - WZ dostaje ustawienia konta jak dokument z edytora (symbol WZ, miejsce, uwagi, podpisy, tryb potwierdzania wydań). - Brak towaru odrzuca WZ (`422 STOCK_SHORTAGE`), chyba że wyślesz `allowNegativeStock: true`. Dokumenty WZ znajdziesz na liście dokumentów przez filtr typu: `GET /documents?documentType=WZ` (zobacz [Paginacja i filtrowanie](/docs/pagination)). ## Pojedynczy magazyn i księga ruchów `GET /warehouse/{id}` zwraca jeden magazyn (`404` dla cudzego lub usuniętego). `GET /warehouse/movements` to księga ruchów, z której liczą się stany: każde przyjęcie i wydanie z dokumentem, ilością, kosztem jednostkowym i osobą, która je zapisała. Filtry `productId`, `warehouseId`, `documentId`, `type` (lista po przecinku), `dateFrom` / `dateTo`; stronicowanie jak w listach. Raport stanów przyjmuje dodatkowo `productId`, gdy interesuje Cię jeden produkt. Przykłady i kształt wpisu: [Historia i aktywność](/docs/activity). ## Dobre praktyki - **Stan pobieraj raportem, nie licz go po swojej stronie.** `GET /warehouse/stock` sumuje ruchy dokładnie tak samo jak aplikacja, a `asOf` daje stan historyczny (np. do remanentu na koniec miesiąca). Własna kopia stanów prędzej czy później się rozjedzie. - **Wydawaj dokumentem, nie korektą.** WZ/RW/MM pilnują stanu (`422 STOCK_SHORTAGE`), a ujemna korekta (`quantity < 0`) jest przyjmowana bez weryfikacji. Korekta z `quantity: 0` i `unitCost` to przecena (ruch `WYCENA`) bez zmiany ilości. - **Przy przyjęciach podawaj `unitCost`.** To podstawa wyceny stanów; przyjęcia bez kosztu zubażają raporty wartościowe. - **WZ do faktur twórz przez `wz-from-invoice`, nie ręcznie.** Endpoint sam wiąże dokumenty w obie strony, tworzy ruchy i pilnuje duplikatu (`409` przy drugiej próbie), a idempotencję masz za darmo. - **Utrzymuj kartotekę produktów.** Na WZ trafiają tylko pozycje z `productType: "goods"`; jeśli w polu `skipped` wracają nazwy, których się nie spodziewasz, najpierw uzupełnij typy w kartotece zamiast obchodzić to po swojej stronie. - **Dokumenty magazynowe filtruj po typie.** `GET /documents?documentType=WZ` (albo lista po przecinku, np. `WZ,PZ`) zwraca je razem z resztą metadanych dokumentów. --- # Pracownicy i uprawnienia Konto pracownicze to osobna tożsamość logowania (własny email i hasło), która pracuje na danych konta głównego. Właściciel nadaje pracownikowi uprawnienia z zamkniętego rejestru, np. tylko podgląd dokumentów albo pełne wystawianie faktur. Przez API możesz zautomatyzować zakładanie takich kont, np. przy wdrażaniu nowego pracownika w systemie HR. Zakresy uprawnień API: `employees:browse`, `employees:create`, `employees:update`, `employees:delete`. Jedno rozróżnienie jest ważne: **uprawnienia pracowników to nie to samo co scope'y API**. Scope'y ograniczają, co może zrobić Twój token API. Uprawnienia pracownika ograniczają, co dana osoba może zrobić po zalogowaniu w aplikacji Fakturnia. Token API zawsze działa w imieniu konta głównego. ## Rejestr uprawnień Lista dostępnych uprawnień z etykietami i opisami (do wyświetlenia w Twoim UI). Wymaga jedynie ważnego tokenu, bez dodatkowego scope'a: ```bash curl -s https://prod.api.fakturnia.pl/external/v1/employees/permissions \ -H "Authorization: Bearer $TOKEN" ``` ```json { "success": true, "data": { "permissions": [ { "key": "documents:read", "label": "Podgląd dokumentów", "description": "Przeglądanie faktur…" }, { "key": "documents:write", "label": "Wystawianie dokumentów", "description": "Tworzenie, edycja…" } ] } } ``` Traktuj rejestr jak słownik: pobieraj go zamiast wpisywać klucze na sztywno. Nieznane klucze przesłane przy tworzeniu lub edycji konta są po prostu ignorowane. ## Tworzenie konta pracowniczego ```bash curl -s -X POST https://prod.api.fakturnia.pl/external/v1/employees \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "firstName": "Jan", "lastName": "Kowalski", "emailAddress": "jan@firma.pl", "password": "bezpieczne-haslo", "permissions": ["documents:read", "documents:write"] }' ``` Zasady: - email jest **globalnie unikalny** w całej Fakturni; kolizja zwraca `409` z kodem `EMAIL_TAKEN`, - hasło ma minimum 8 znaków (`422` przy krótszym), - konto główne może mieć maksymalnie 20 kont pracowniczych (`409 EMPLOYEE_LIMIT_REACHED`), - odpowiedź nigdy nie zawiera hasła. ## Lista, edycja i usuwanie ```bash # Lista curl -s https://prod.api.fakturnia.pl/external/v1/employees \ -H "Authorization: Bearer $TOKEN" # Edycja: zmiana uprawnień i dezaktywacja curl -s -X PUT https://prod.api.fakturnia.pl/external/v1/employees/{id} \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "permissions": ["documents:read"], "isActive": false }' # Usunięcie curl -s -X DELETE https://prod.api.fakturnia.pl/external/v1/employees/{id} \ -H "Authorization: Bearer $TOKEN" ``` W edycji wszystkie pola są opcjonalne: przesyłasz tylko to, co się zmienia. `permissions` zastępuje całą listę. Nowe hasło ustawisz polem `password`. **Dezaktywacja a usunięcie.** `isActive: false` blokuje konto natychmiast (aktywne sesje pracownika są kończone), ale zachowuje email na koncie. Usunięcie kasuje konto trwale, a email wraca do puli i może zostać użyty ponownie. Zmiany uprawnień też działają natychmiast, bez ponownego logowania pracownika. ## Jedno konto i jego aktywność `GET /employees/{id}` zwraca jedno konto (bez hasła), `404` dla cudzego. `GET /employees/{id}/activity` to dziennik pracownika: logowania z urządzeniem i adresem IP, zmiany konta, akcje na dokumentach i operacje magazynowe, od najnowszego, z kursorem `before` / `nextBefore` i filtrem `scope`. Kształt wpisów opisuje [Historia i aktywność](/docs/activity). ## Dobre praktyki - **Nadawaj minimalne uprawnienia i rozszerzaj w miarę potrzeb.** Zacznij od `documents:read`; uprawnienia implikowane są dopinane automatycznie (np. `documents:write` dokłada `documents:read`), więc nie musisz budować pełnych zestawów samodzielnie. - **Pobieraj rejestr uprawnień przy każdej synchronizacji.** `GET /employees/permissions` to słownik: nowe uprawnienia pojawiają się w nim bez zmiany wersji API, a nieznane klucze w zapisie są po cichu ignorowane. Hardkodowana lista w Twoim UI będzie kłamać. - **Offboarding rób dwuetapowo.** Najpierw `isActive: false` (natychmiast kończy sesje, zachowuje email i historię), a dopiero po okresie przejściowym `DELETE`, jeśli konto ma zniknąć na stałe. - **Obsłuż `409 EMAIL_TAKEN` jawnie.** Email jest unikalny globalnie w całej Fakturni, nie tylko na Twoim koncie, więc kolizja może dotyczyć zupełnie obcego konta. Pokaż użytkownikowi komunikat zamiast ponawiać. - **Rotuj hasła przez `PUT` z polem `password`.** Nie ma osobnego endpointu resetu; zmiana hasła nie kończy aktywnych sesji, w razie potrzeby połącz ją z chwilową dezaktywacją. --- # Dokumenty cykliczne Reguła cykliczna to szablon dokumentu połączony z harmonogramem. Raz skonfigurowana, sama wystawia faktury (tryb `AUTO`), przygotowuje szkice do akceptacji (`DRAFT`) albo tylko przypomina o wystawieniu (`REMINDER`). Przez API odczytasz stan reguł i historię generacji oraz wstrzymasz lub wznowisz regułę, np. gdy klient zrezygnuje z abonamentu w Twoim systemie. Moduł wymaga planu **MAX** na koncie Fakturnia. Bez niego każdy endpoint zwraca `403` z kodem `SUBSCRIPTION_REQUIRED`. Zakresy uprawnień: `recurring:browse`, `recurring:read`, `recurring:toggle`. Reguły tworzy się i edytuje w aplikacji Fakturnia. API celowo ich nie zakłada: szablon reguły to kompletny dokument z pozycjami i automatyzacjami, a jego konfiguracja jest po prostu wygodniejsza w edytorze niż w JSON-ie. ## Lista reguł ```bash curl -s https://prod.api.fakturnia.pl/external/v1/recurring-rules \ -H "Authorization: Bearer $TOKEN" ``` ```json { "success": true, "data": { "rules": [ { "id": "665f0c1234567890abcdef12", "name": "Abonament, Firma Sp. z o.o.", "status": "ACTIVE", "mode": "AUTO", "documentSummary": { "documentType": "FakturaVAT", "buyerName": "Firma Sp. z o.o.", "grossSum": 1230, "currency": "PLN" }, "schedule": { "frequency": "MONTHLY", "dayOfMonth": 1, "startDate": "2026-01-01", "endType": "NEVER" }, "nextRunAt": "2026-08-01", "occurrencesCount": 7, "documentsIssuedCount": 7 } ], "summary": { "total": 3, "active": 2, "paused": 1, "finished": 0, "nextRunAt": "2026-08-01", "monthlyRecurringGross": 2460 } } } ``` Parametr `?status=ACTIVE|PAUSED|FINISHED` zawęża listę. Blok `summary` zawsze opisuje wszystkie reguły konta, niezależnie od filtra. `monthlyRecurringGross` to miesięczny przychód brutto z aktywnych reguł, po przeliczeniu częstotliwości na miesiąc (np. reguła kwartalna liczy się jako 1/3 kwoty). Sumowane są tylko reguły w PLN, bo API nie zgaduje kursów walut. Zamiast pełnego szablonu dokumentu reguła zwraca `documentSummary`: typ dokumentu, nazwę nabywcy, kwotę brutto i walutę. To wystarcza do listowania i raportowania, a wygenerowane dokumenty pobierzesz normalnie przez [endpointy dokumentów](/docs/documents). ## Szczegóły reguły ```bash curl -s https://prod.api.fakturnia.pl/external/v1/recurring-rules/{id} \ -H "Authorization: Bearer $TOKEN" ``` Poza polami z listy szczegóły zawierają pełny harmonogram i automatyzacje: | Pole | Znaczenie | |---|---| | `schedule.frequency` | `WEEKLY`, `MONTHLY`, `BIMONTHLY`, `QUARTERLY`, `SEMIANNUALLY`, `YEARLY` lub `CUSTOM` (z `customInterval` + `customUnit`) | | `schedule.endType` | `NEVER`, `DATE` (do `endDate`) lub `COUNT` (po `endCount` wystąpieniach) | | `automation` | wysyłka e-mail do nabywcy, wysyłka do KSeF, powiadomienia | | `payment` | status płatności nadawany generowanym dokumentom (`PAID` / `UNPAID` + termin) | | `nextRunAt` | data najbliższej generacji (`null` gdy reguła wstrzymana lub zakończona) | | `skipDates` | daty wystąpień pominiętych ręcznie w aplikacji | ## Historia generacji Dziennik wystąpień reguły, od najnowszych: ```bash curl -s "https://prod.api.fakturnia.pl/external/v1/recurring-rules/{id}/history?itemsPerPage=50" \ -H "Authorization: Bearer $TOKEN" ``` ```json { "success": true, "data": { "runs": [ { "id": "666a0d1234567890abcdef56", "plannedDate": "2026-07-01", "issueDate": "2026-07-01", "documentId": "666a0e1234567890abcdef78", "documentNumber": "FV/7/2026", "status": "GENERATED", "emailSent": true, "ksefQueued": false } ] } } ``` | Status | Znaczenie | |---|---| | `GENERATED` | dokument wystawiony automatycznie (tryb `AUTO`) | | `DRAFT` | powstał szkic czekający na akceptację w aplikacji | | `REMINDED` | wysłano przypomnienie o wystawieniu (tryb `REMINDER`) | | `SKIPPED` | wystąpienie pominięte | | `FAILED` | generacja nieudana, szczegóły w polu `error` | | `PENDING` | wystąpienie w trakcie przetwarzania | `documentId` i `documentNumber` wskazują wygenerowany dokument. Historia jest stronicowana jak listy: `currentPage`, `itemsPerPage` (domyślnie 50, do 200), w odpowiedzi `totalCount` i `totalPages`; starszy parametr `limit` nadal działa jako alias `itemsPerPage`. Lista reguł `GET /recurring-rules` także jest stronicowana (`itemsPerPage` do 100), a jej `summary` zawsze obejmuje wszystkie reguły. ## Pauza i wznowienie ```bash # Wstrzymanie curl -s -X PATCH https://prod.api.fakturnia.pl/external/v1/recurring-rules/{id}/toggle \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "active": false }' # Wznowienie curl -s -X PATCH https://prod.api.fakturnia.pl/external/v1/recurring-rules/{id}/toggle \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "active": true }' ``` Wstrzymana reguła nie generuje dokumentów. Wznowienie przelicza `nextRunAt` od dnia dzisiejszego: wystąpienia zaległe z okresu pauzy **nie są nadrabiane**. To bezpieczne zachowanie dla typowego scenariusza, w którym klient zawiesza abonament i wraca po kilku miesiącach. Reguły zakończonej (`FINISHED`, czyli po dacie końcowej lub limicie wystąpień) nie można wznowić, próba zwraca `409 CONFLICT`. ## Dobre praktyki - **Nasłuchuj webhooka `document.created` zamiast odpytywać listę.** Dokumenty wystawiane przez reguły emitują `document.created` (jak każdy nowy dokument, z API albo z aplikacji), więc Twoja integracja dostaje je natychmiast. Historia reguły (`/history`) pozwala potem powiązać wystąpienie z `documentId`. - **Monitoruj wystąpienia `FAILED`.** Okresowo (np. raz dziennie) przejrzyj historię aktywnych reguł; pole `error` mówi, dlaczego generacja się nie powiodła. Nieudane wystąpienie nie zatrzymuje cyklu, kolejne terminy idą dalej. - **Pauzuj zamiast usuwać.** Gdy klient zawiesza abonament, wywołaj `toggle` z `active: false`. Historia i konfiguracja reguły zostają, a wznowienie samo przeliczy `nextRunAt` bez ryzyka wystawienia zaległych faktur. - **Trzymaj `id` reguły, nie jej nazwę.** Nazwa jest edytowalna w aplikacji i może się zmienić bez ostrzeżenia. - **Rozróżniaj liczniki.** `occurrencesCount` liczy wystąpienia harmonogramu (także przypomnienia i pominięcia), `documentsIssuedCount` tylko faktycznie wystawione dokumenty. Do raportowania sprzedaży używaj tego drugiego. - **Traktuj `monthlyRecurringGross` jako przybliżenie.** Suma obejmuje wyłącznie aktywne reguły w PLN; reguły walutowe dolicz po własnym kursie na podstawie `documentSummary`. --- # Konta pocztowe Konto Fakturnia może wysyłać dokumenty e-mailem z własnej skrzynki właściciela zamiast z adresu systemowego. Skrzynki podłącza się w aplikacji (Inne → Konta pocztowe): SMTP z hasłem albo Gmail i Outlook przez OAuth. Przez API sprawdzisz, które skrzynki są podłączone i w jakim są stanie. To wszystko: **podłączanie, weryfikacja i edycja odbywają się wyłącznie w aplikacji**, bo wymagają hasła albo zgody OAuth, których integracja nie powinna przechowywać. Po co integracji ten odczyt: żeby wiedzieć, z jakiego adresu wychodzą faktury (np. do wpisania w stopce sklepu), żeby ostrzec użytkownika, gdy domyślna skrzynka ma błąd weryfikacji, albo żeby pokazać stan konfiguracji w panelu integracji. ## Endpointy | Metoda | Ścieżka | Scope | Opis | |---|---|---|---| | `GET` | `/email-accounts` | `email-accounts:browse` | lista skrzynek, domyślna pierwsza | | `GET` | `/email-accounts/{id}` | `email-accounts:read` | jedna skrzynka | ```bash curl -s https://sandbox.api.fakturnia.pl/external/v1/email-accounts \ -H "Authorization: Bearer $TOKEN" ``` ```json { "success": true, "data": { "emailAccounts": [ { "id": "681a3b2c4d5e6f7a8b9c0d1e", "provider": "gmail", "isOAuth": true, "email": "biuro@firma.pl", "host": null, "port": null, "secure": true, "verificationStatus": "verified", "isVerified": 1, "isDefault": true, "lastError": null, "lastVerifiedAt": "2026-09-20T10:00:00.000Z", "createdAt": "2026-09-01T10:00:00.000Z", "updatedAt": "2026-09-20T10:00:00.000Z" }, { "id": "681a3b2c4d5e6f7a8b9c0d1f", "provider": "smtp", "isOAuth": false, "email": "faktury@firma.pl", "host": "smtp.firma.pl", "port": 465, "secure": true, "verificationStatus": "error", "isVerified": 2, "isDefault": false, "lastError": "535 Authentication failed", "lastVerifiedAt": "2026-08-30T08:12:00.000Z", "createdAt": "2026-08-30T08:00:00.000Z", "updatedAt": "2026-09-25T06:40:00.000Z" } ] }, "executionTime": 6 } ``` ## Pola | Pole | Znaczenie | |---|---| | `provider` | `smtp`, `gmail` albo `outlook` | | `isOAuth` | `true` dla Gmail i Outlook, `false` dla SMTP | | `email` | adres nadawcy | | `host`, `port`, `secure` | konfiguracja SMTP (`null` dla OAuth); `secure: true` to SSL na porcie 465, `false` to STARTTLS | | `verificationStatus` | `unverified` (jeszcze niesprawdzona albo weryfikacja w aplikacji nie powiodła się; wtedy powód jest w `lastError`), `verified` (wysyłka testowa przeszła), `error` (ostatnia wysyłka nie powiodła się, patrz `lastError`) | | `isVerified` | ten sam stan jako liczba, jak w aplikacji: `0`, `1`, `2` | | `isDefault` | skrzynka używana do wysyłki dokumentów, gdy użytkownik nie wybierze innej | | `lastError` | komunikat serwera pocztowego z ostatniej nieudanej próby albo `null` | | `lastVerifiedAt` | kiedy skrzynka ostatnio przeszła weryfikację | Sekrety (hasło SMTP, tokeny OAuth) nigdy nie są zwracane, także w szczegółach pojedynczej skrzynki. ## Co się dzieje, gdy skrzynka ma błąd Gdy wysyłka z własnej skrzynki nie powiedzie się, Fakturnia oznacza ją stanem `error`, a dokument wychodzi z adresu systemowego, więc odbiorca i tak dostaje fakturę. Użytkownik musi jednak poprawić konfigurację w aplikacji. Jeśli Twoja integracja pokazuje stan skrzynek, zwróć uwagę na `verificationStatus === 'error'` przy skrzynce z `isDefault: true`. ## Błędy | Status | Kod | Kiedy | |---|---|---| | `403` | `NO_PERMISSIONS` | token bez `email-accounts:browse` albo `email-accounts:read` | | `403` | `SUBSCRIPTION_REQUIRED` | konto bez planu MAX (patrz [Uwierzytelnianie](/docs/authentication)) | | `404` | `NOT_FOUND` | skrzynka nie istnieje albo należy do innego konta | --- # Historia i aktywność Wszystko, co dzieje się na koncie, zostaje w dziennikach: kto wystawił dokument, kiedy poszedł mailem, czy KSeF go przyjął, kto oznaczył zapłatę, jakie ruchy przeszły przez magazyn, kiedy logował się pracownik. Przez API czytasz te dzienniki na cztery sposoby, w zależności od tego, o co pytasz. | Pytanie | Endpoint | Scope | Paginacja | |---|---|---|---| | Co się działo z tym dokumentem? | `GET /documents/{id}/history` | `documents:read` | strony | | Co się dzisiaj działo na koncie? | `GET /activity` | `activity:read` | kursor | | Co robił ten pracownik? | `GET /employees/{id}/activity` | `employees:browse` | kursor | | Jakie ruchy przeszły przez magazyn? | `GET /warehouse/movements` | `warehouse:stock:read` | strony | Dzienniki są tylko do odczytu i obejmują akcje z każdego źródła: API, aplikacji web i mobilnej, automatów (dokumenty cykliczne, kolejka KSeF). Historię reguł cyklicznych opisuje [Dokumenty cykliczne](/docs/recurring-rules), historię dostaw webhooków [Webhooki](/docs/webhooks). ## Historia dokumentu ```bash curl -s "https://sandbox.api.fakturnia.pl/external/v1/documents/681a3b2c4d5e6f7a8b9c0d1e/history?itemsPerPage=20" \ -H "Authorization: Bearer $TOKEN" ``` ```json { "success": true, "data": { "documentId": "681a3b2c4d5e6f7a8b9c0d1e", "documentNumber": "FV/12/09/2026", "entries": [ { "id": "681a…2a", "type": "PAYMENT_RECEIVED", "createdAt": "2026-09-26T12:00:00.000Z", "actor": "employee", "employeeId": "681a…3b", "employeeName": "Ewa Kowalska", "origin": "app", "channel": null, "channelLabel": null, "changes": null, "data": { "origin": "app", "receivedPayment": "RECEIVED" } }, { "id": "681a…29", "type": "KSEF_SENT", "createdAt": "2026-09-26T11:05:00.000Z", "actor": "owner", "employeeId": null, "employeeName": null, "origin": null, "channel": null, "channelLabel": null, "changes": null, "data": { "ksefNumber": "5271012345-20260926-AABBCC-DD", "sendMode": "instant" } }, { "id": "681a…28", "type": "UPDATE", "createdAt": "2026-09-26T10:40:00.000Z", "actor": "owner", "employeeId": null, "employeeName": null, "origin": null, "channel": null, "channelLabel": null, "changes": [{ "field": "comments", "oldValue": "", "newValue": "Zamówienie #10234" }], "data": null }, { "id": "681a…27", "type": "CREATE", "createdAt": "2026-09-26T10:30:00.000Z", "actor": "owner", "employeeId": null, "employeeName": null, "origin": "external_api", "channel": null, "channelLabel": null, "changes": null, "data": { "origin": "external_api", "source": "local" } } ], "currentPage": 1, "itemsPerPage": 20, "totalCount": 4, "totalPages": 1 } } ``` Typy wpisów: `CREATE`, `UPDATE` (z różnicami pól w `changes`), `DELETE`, `SENT_BY_EMAIL` (adresaci w `data`), `DOWNLOAD`, `DOWNLOAD_XML`, `DOWNLOAD_UPO`, `PRINT`, `KSEF_SENT`, `KSEF_ERROR`, `PAYMENT_RECEIVED`, `PAYMENT_REVERTED`, `ADVANCE_INVOICE_LINKED` / `UNLINKED`, `SETTLEMENT_INVOICE_LINKED` / `UNLINKED`, `DOWNLOAD_CLIENT` (pobranie przez odbiorcę z linku). `actor` mówi, czy działał właściciel, czy pracownik, a `origin` (gdy wpis to zapisuje) skąd przyszła akcja: `external_api`, `app` albo `import`. `channel` ma wartość `mcp`, gdy akcję wykonał asystent AI połączony przez [serwer MCP](/docs/mcp), a `channelLabel` podaje wtedy nazwę jego poświadczenia (np. `Claude`, `ChatGPT` albo etykietę klucza); w pozostałych wpisach oba pola mają wartość `null`. Endpoint jest stronicowany jak listy ([Paginacja](/docs/pagination)): `currentPage`, `itemsPerPage` (do 100), w odpowiedzi `totalCount` i `totalPages`. ## Aktywność konta `GET /activity` łączy trzy dzienniki całego konta (właściciel i wszyscy pracownicy): akcje na dokumentach, logowania i zmiany kont pracowniczych, operacje magazynowe. Wpisy przychodzą od najnowszego. ```bash curl -s "https://sandbox.api.fakturnia.pl/external/v1/activity?scope=documents&from=2026-09-26T00:00:00Z&limit=50" \ -H "Authorization: Bearer $TOKEN" ``` ```json { "success": true, "data": { "items": [ { "id": "681a…1e", "scope": "document", "type": "CREATE", "createdAt": "2026-09-26T10:30:00.000Z", "employeeId": null, "employeeName": null, "entityId": "681a…1f", "entityName": "FV/12/09/2026", "entityType": "DOCUMENT", "entityDeleted": false, "changes": null, "data": { "origin": "external_api", "source": "local" } }, { "id": "681a…1d", "scope": "auth", "type": "LOGIN", "createdAt": "2026-09-26T07:58:00.000Z", "employeeId": "681a…3b", "employeeName": "Ewa Kowalska", "entityId": null, "entityName": null, "entityType": null, "entityDeleted": false, "changes": null, "data": { "ip": "203.0.113.5", "device": "web" } } ], "hasMore": true, "nextBefore": "2026-09-26T07:58:00.000Z" } } ``` Filtry: | Parametr | Znaczenie | |---|---| | `scope` | `all` (domyślnie), `documents`, `auth` (logowania), `management` (zmiany kont pracowniczych), `warehouse` | | `employeeId` | tylko jeden pracownik (id) albo `main` (tylko konto właściciela) | | `from`, `to` | okno czasowe (ISO 8601) | | `q` | tekst: imię i nazwisko pracownika albo numer dokumentu | | `limit` | wpisów na stronę, domyślnie 30, maksymalnie 100 | | `before` | kursor: `nextBefore` z poprzedniej strony | `scope` `document` w odpowiedzi to akcje na dokumentach (w tym `entityDeleted: true`, gdy dokument został później usunięty), `warehouse` to operacje magazynowe (utworzenie magazynu, korekta stanu, dokumenty magazynowe), `auth` i `management` dotyczą kont pracowniczych. ## Aktywność pracownika `GET /employees/{id}/activity` zwraca ten sam kształt co `GET /activity`, ograniczony do jednego konta pracowniczego, oraz obiekt `employee` (jak w `GET /employees/{id}`). Przyjmuje `scope`, `before` i `limit`. Pracownik spoza konta odpowiada `404`. ## Ruchy magazynowe `GET /warehouse/movements` to księga ruchów, z której liczą się stany: każde przyjęcie i wydanie z dokumentem, ilością, kosztem jednostkowym i osobą, która je zapisała. Filtry: `productId`, `warehouseId`, `documentId`, `type` (lista po przecinku: `PZ`, `WZ`, `PW`, `RW`, `MM_OUT`, `MM_IN`, `INW`, `WYCENA`), `dateFrom` / `dateTo` (`YYYY-MM-DD`). Stronicowanie jak w listach (`itemsPerPage` do 100, domyślnie 50). ```bash curl -s "https://sandbox.api.fakturnia.pl/external/v1/warehouse/movements?productId=681a3b2c4d5e6f7a8b9c0d2a&dateFrom=2026-09-01" \ -H "Authorization: Bearer $TOKEN" ``` ```json { "success": true, "data": { "movements": [ { "id": "681a…4c", "productId": "681a…2a", "warehouseId": "681a…3b", "quantity": -2, "unitCost": null, "type": "WZ", "documentId": "681a…4d", "documentNumber": "WZ/3/09/2026", "date": "2026-09-26", "note": null, "employeeId": null, "employeeName": null, "createdAt": "2026-09-26T10:30:00.000Z" }, { "id": "681a…4b", "productId": "681a…2a", "warehouseId": "681a…3b", "quantity": 25, "unitCost": 12.5, "type": "INW", "documentId": null, "documentNumber": null, "date": "2026-09-01", "note": "Bilans otwarcia", "employeeId": null, "employeeName": null, "createdAt": "2026-09-01T08:00:00.000Z" } ], "currentPage": 1, "itemsPerPage": 50, "totalCount": 2, "totalPages": 1 } } ``` Stan produktu na dzień to suma ruchów do tego dnia; ten sam wynik da `GET /warehouse/stock?productId=…&asOf=…` bez przeglądania księgi. Szczegóły modułu: [Magazyn](/docs/warehouse). ## Dwa style paginacji Listy zasobów i historia dokumentu są **stronicowane numerami stron** (`currentPage`, `itemsPerPage`, `totalCount`, `totalPages`): wiesz, ile jest wszystkiego, i możesz skoczyć na dowolną stronę. Strumienie aktywności (`GET /activity`, `GET /employees/{id}/activity`) łączą kilka dzienników, więc używają **kursora czasowego**: bierzesz `nextBefore` z odpowiedzi i podajesz jako `before`, dopóki `hasMore` nie będzie `false`. Nowe wpisy, które dojdą w międzyczasie, nie przesuwają kolejnych stron. `before` działa jednak ostro (zwraca wpisy starsze niż podany czas), więc wpisy z dokładnie tym samym czasem co ostatni wpis strony, co do milisekundy, mogą zostać pominięte na następnej stronie. --- # Dane firmy po NIP Endpoint `/company/nip-lookup` pobiera dane polskiej firmy z rejestru GUS. Użyj go, żeby automatycznie uzupełnić dane [kontrahenta](/docs/clients) albo nabywcy przy [tworzeniu dokumentu](/docs/creating-documents), zamiast prosić użytkownika o ręczne przepisywanie. Wymagany zakres uprawnień: `gus:company:read`. ## Wyszukiwanie ```bash curl -s "https://prod.api.fakturnia.pl/external/v1/company/nip-lookup?nip=5252248481" \ -H "Authorization: Bearer $TOKEN" ``` NIP podajesz jako 10 cyfr. Myślniki i spacje są ignorowane, więc `525-224-84-81` też zadziała. ```json { "success": true, "data": { "company": { "nip": "5252248481", "name": "PRZYKŁAD SPÓŁKA Z OGRANICZONĄ ODPOWIEDZIALNOŚCIĄ", "regon": "146061343", "street": "ul. Prosta 1 lok. 2", "postcode": "00-001", "city": "Warszawa", "country": "Polska", "countryCode": "PL" } } } ``` ## Błędy | Status | Kod | Znaczenie | |---|---|---| | `400` | `INVALID_NIP` | NIP nie składa się z 10 cyfr | | `404` | `COMPANY_NOT_FOUND` | brak firmy o tym NIP w rejestrze GUS | ## Typowy przepływ 1. Użytkownik podaje NIP w Twoim formularzu. 2. Wywołujesz `nip-lookup` i wypełniasz formularz zwróconymi danymi. 3. Po zatwierdzeniu tworzysz [kontrahenta](/docs/clients) albo przekazujesz dane nabywcy wprost w `POST /documents`. Dane pochodzą z rejestru publicznego i mają charakter pomocniczy: przed wystawieniem faktury użytkownik powinien móc je poprawić. Pamiętaj też, że wysyłka do KSeF wymaga pełnego adresu nabywcy. --- # Błędy API używa standardowych kodów statusu HTTP oraz **spójnej koperty błędu**. Każdy błąd zawiera stabilny, czytelny maszynowo `message` (kod błędu) oraz, gdy ma to znaczenie, szczegóły w `errors`. ## Struktura błędu ```json { "success": false, "data": null, "message": "AMBIGUOUS_PARTY", "executionTime": 5, "errors": ["Podaj albo buyerId, albo buyer, ale nie oba."] } ``` Zawsze podejmuj decyzje na podstawie `message` (kodu), a nie czytelnego dla człowieka tekstu. Tekst może się zmienić, kod nie. Pole `errors` ma dwie postacie: przy walidacji pól to lista obiektów `{ field, code?, message }`, w pozostałych przypadkach lista zdań wyjaśniających, np. `["REST API wymaga planu MAX."]`. ## Kody statusu | Status | Znaczenie | |---|---| | `200` / `201` | sukces | | `400` | nieprawidłowe parametry zapytania lub ścieżki, operacja niedozwolona dla stanu dokumentu, a przy tworzeniu dokumentów także błędny kształt treści żądania (np. brak `documentType`, `issueDate`, `products` albo nabywcy) | | `401` | brakujący, nieprawidłowy lub wygasły token, albo unieważnione konto API | | `403` | uwierzytelniono, ale brak uprawnień: scope, plan MAX, lista dozwolonych adresów IP | | `404` | nie znaleziono zasobu (także gdy należy do innego konta) | | `409` | konflikt (np. numer dokumentu już istnieje, webhook dla adresu już zarejestrowany) | | `422` | niepowodzenie walidacji treści żądania: brak pola, zły format, reguły zależne od typu, sprzeczne adnotacje (przy tworzeniu dokumentów część tych sprawdzeń zwraca `400`, patrz `VALIDATION_ERROR`) | | `429` | przekroczony limit żądań albo pełna kolejka renderowania PDF | | `500` | nieoczekiwany błąd serwera | | `503` | zewnętrzny rejestr chwilowo nie odpowiada (wyszukiwanie firmy w GUS); ponów za chwilę | ## Kody błędów | Kod | Status | Kiedy | |---|---|---| | `UNAUTHORIZED` | 401 | brakujący lub nieprawidłowy token; także token unieważnionego konta API | | `TOKEN_EXPIRED` | 401 | token wygasł, pobierz nowy z `/v1/oauth/token` | | `NO_PERMISSIONS` | 403 | token nie ma wymaganego scope (`errors[0]` podaje, którego) | | `SUBSCRIPTION_REQUIRED` | 403 | konto bez aktywnego planu MAX (całe REST API jest modułem MAX) | | `FORBIDDEN` | 403 | adres IP spoza listy dozwolonych dla konta API | | `DOCUMENT_LIMIT_EXCEEDED` | 403 | plan konta nie pozwala wystawiać ani edytować dokumentów (`errors[0]` zaczyna się od „Subskrypcja nieaktywna” albo „Subskrypcja wygasła”) | | `VALIDATION_ERROR` | 422 / 400 | treść żądania nie przeszła walidacji; szczegóły w `errors`. Zwykle `422`, ale tworzenie dokumentów (`POST /documents`, `POST /documents/validate`) i dokumentów magazynowych (`POST /warehouse/documents`, `POST /warehouse/documents/validate`) zwraca `400` przy błędach kształtu żądania (brak `documentType`, `issueDate` albo `products`, zły format daty, błędna pozycja). Traktuj oba statusy z tym kodem jako błąd walidacji | | `PARTIAL_PAYMENTS_INVALID` | 422 | zapłaty częściowe nie zgadzają się z kwotą albo statusem płatności | | `DOCUMENT_DATES_INVALID` | 422 | daty dokumentu przeczą sobie albo ustawie o VAT (okres sprzedaży, data sprzedaży ponad 60 dni po wystawieniu, zapłata po wystawieniu, korekta przed fakturą korygowaną) | | `PAYMENT_INSTALLMENTS_INVALID` | 422 | raty nie zgadzają się z kwotą do zapłaty, terminami albo statusem płatności, albo `paymentInstallmentsSplit` nie da się wykonać (np. kwota za mała na tyle rat) | | `PARTY_IDENTIFIER_INVALID` | 422 | identyfikatory nabywcy albo odbiorcy podanych danymi przy dokumencie są sprzeczne (np. `nip` z numerem `DE…`, `vatEu` z numerem `PL…` albo `XI…`, błędna cyfra kontrolna NIP-u, `idWew` u nabywcy); `errors` podaje pole, np. `buyer.vatUE`. Kontrahenci (`/clients`) zwracają takie błędy jako `VALIDATION_ERROR`. Zob. [Identyfikatory kontrahenta](/docs/clients#identyfikatory-kontrahenta) | | `ADVANCE_RECEIPTS_INVALID` | 422 | wpłaty na fakturze zaliczkowej nie zgadzają się z kwotą zaliczki, datami albo kursami | | `INVALID_PARAMS` | 400 | nieprawidłowe parametry zapytania (np. nieznany `documentType`, zła data) | | `BAD_REQUEST` | 400 | brak wymaganego elementu żądania poza walidacją pól | | `AMBIGUOUS_PARTY` | 400 | podano jednocześnie `buyerId` i `buyer` (albo `receiverId` i `receiver`) | | `BUYER_REQUIRED` | 400 | nie podano ani `buyerId`, ani `buyer` | | `NOT_FOUND` / `DOCUMENT_NOT_FOUND` | 404 | brak zasobu | | `DOCUMENT_NUMBER_EXISTS` | 409 | numer zajęty w swojej serii albo ten sam pełny numer ma już inny dokument na koncie | | `CONFLICT` | 409 | stan zasobu nie pozwala na operację (np. jedyny magazyn, webhook dla adresu już istnieje, zakończona reguła cykliczna) | | `KSEF_CONFIRMED` | 400 | usunięcie/edycja dokumentu już zatwierdzonego w KSeF (posiada numer KSeF) | | `KSEF_SENT` | 400 | usunięcie/edycja dokumentu wysłanego do KSeF i oczekującego na potwierdzenie | | `DOCUMENT_CORRECTED` | 409 | edycja albo usunięcie faktury, do której istnieje korekta (zmianę wprowadza kolejna korekta) | | `CORRECTED_INVOICE_HAS_CORRECTION` | 409 | nowa korekta dokumentu, który ma już korektę (kolejną zmianę wystaw jako korektę jego ostatniej korekty, numer i id w `errors[0]`) | | `ADVANCE_INVOICE_SETTLED` | 409 | edycja albo usunięcie faktury zaliczkowej rozliczonej fakturą rozliczeniową | | `ORDER_HAS_ADVANCE_INVOICES` | 400 | edycja albo usunięcie zamówienia z wystawionymi fakturami zaliczkowymi | | `WAREHOUSE_LINES_LOCKED` | 409 | zmiana pozycji dokumentu magazynowego przez `PUT`/`PATCH` (usuń i wystaw nowy) | | `STOCK_SHORTAGE` | 422 | WZ/RW/MM zeszłoby ze stanem poniżej zera (`allowNegativeStock: true` wystawia mimo braku) | | `WAREHOUSE_INVALID` | 422 | nieznany albo cudzy magazyn, MM bez magazynu docelowego albo z tym samym magazynem | | `PRODUCT_NOT_FOUND` | 404 | pozycja dokumentu magazynowego wskazuje nieznany produkt | | `KSEF_CONNECTION_REQUIRED` | 409 | wysyłka bez skonfigurowanego połączenia z KSeF | | `KSEF_CONNECTION_NOT_AUTHORIZED` | 422 | połączenie istnieje, ale nie przeszło autoryzacji | | `KSEF_ALREADY_PROCESSING` / `KSEF_SEND_NOT_ALLOWED` | 409 | wysyłka w toku albo dokument w stanie niepozwalającym na wysyłkę | | `INVALID_PAYMENT_STATUS` / `PAYMENT_STATUS_UPDATE_NOT_ALLOWED` | 400 | zła wartość `receivedPayment` albo dokument nie jest „Do zapłaty” | | `EMAIL_TAKEN` / `EMPLOYEE_LIMIT_REACHED` | 409 | e-mail pracownika zajęty albo limit 20 kont | | `INVALID_NIP` / `COMPANY_NOT_FOUND` | 400 / 404 | wyszukiwanie firmy po NIP | | `GUS_UNAVAILABLE` | 503 | rejestr GUS nie odpowiada przy wyszukiwaniu firmy po NIP; ponów za chwilę | | `RATE_LIMIT_EXCEEDED` | 429 | przekroczony limit zapytań, ponów po `Retry-After` (zobacz [Limity zapytań](/docs/rate-limits)) | | `WAIT_PDF_QUEUE` | 429 | pełna kolejka renderowania PDF, ponów po `Retry-After` (zobacz [Kolejka renderowania PDF](/docs/pdf-download-queue)) | | `INTERNAL_SERVER_ERROR` | 500 | nieoczekiwany, bezpieczny do idempotentnego ponowienia | Pełna tabela kodów ze statusami jest też na górze [specyfikacji OpenAPI](/openapi.json) (`info.description`). ## Nagłówki odpowiedzi Każda odpowiedź niesie nagłówek `X-Request-ID` z unikalnym identyfikatorem żądania. Zapisuj go w logach integracji i podawaj w zgłoszeniach do pomocy: pozwala odnaleźć konkretne wywołanie po stronie Fakturnii. Nagłówki `X-RateLimit-*` i `Retry-After` opisuje strona [Limity zapytań](/docs/rate-limits), a `Idempotent-Replayed` strona [Idempotentność](/docs/idempotency). ## Obsługa błędów ```ts const res = await fetch(url, opts); const body = await res.json(); if (!body.success) { switch (body.message) { case 'TOKEN_EXPIRED': // refresh the token and retry once case 'UNAUTHORIZED': // re-authenticate; a revoked API account needs a new one case 'SUBSCRIPTION_REQUIRED': // the account needs the MAX plan; tell the user, do not retry case 'RATE_LIMIT_EXCEEDED': // wait Retry-After seconds, then retry case 'DOCUMENT_NUMBER_EXISTS': // pick a new number or let the API auto-number case 'VALIDATION_ERROR': // 400 or 422: inspect body.errors default: throw new Error(`${body.message} [${res.headers.get('X-Request-ID')}]: ${JSON.stringify(body.errors ?? [])}`); } } ``` Błędy `5xx` i `429` można bezpiecznie ponawiać z odczekiwaniem (backoff) **wtedy, gdy żądanie zawierało `Idempotency-Key`**. Zobacz [Idempotentność](/docs/idempotency). --- # Słowniki Wszystkie wartości enumów potrzebne przy tworzeniu dokumentów zwraca jeden endpoint: ```bash curl -s https://prod.api.fakturnia.pl/external/v1/dictionaries \ -H "Authorization: Bearer $TOKEN" ``` To **źródło prawdy** dla integracji. Endpoint wymaga jedynie ważnego tokenu OAuth (bez dodatkowego scope), a dane są statyczne i niewrażliwe. Zamiast wpisywać poniższe wartości na sztywno, pobierz je raz przy starcie integracji — dzięki temu nowa pozycja w słowniku nie zepsuje Twojego kodu. Poniżej pełna zawartość każdego słownika. Listy używasz przy [tworzeniu dokumentów](/docs/creating-documents). ## Typy dokumentów (`documentTypes`) To wszystkie typy obsługiwane przez External API. Nie ma tu osobnych typów na mechanizm podzielonej płatności, procedurę marży, odwrotne obciążenie czy fakturę zwolnioną z VAT, jakie spotyka się w innych systemach. Fakturnia jest w pełni zgodna z KSeF i te cechy dokumentu ustawia się przez [adnotacje](/docs/creating-documents). To obecny standard: mniej typów dokumentów i prostsza integracja. Kolumna „Wysyłka do KSeF" mówi, czy dany typ można wysłać do KSeF. | Wartość | Wysyłka do KSeF | |---|---| | `Faktura` | nie (dokument poza KSeF, dla zwolnionych z VAT) | | `FakturaVAT` | tak | | `FakturaKorygujaca` | tak | | `FakturaKorygujacaZaliczkowa` | tak | | `FakturaKorygujacaRozliczeniowa` | tak | | `FakturaZaliczka` | tak | | `FakturaRozliczeniowa` | tak | | `FakturaProforma` | nie (dokument informacyjny) | | `FakturaProformaBezVAT` | nie (dokument informacyjny) | | `Zamowienie` | nie (zamówienie, nie faktura) | ## Stawki VAT (`vatRates`) `value` to wartość przesyłana w `vatRate`. Kody specjalne (string) oznaczają netto = brutto, VAT = 0. | `value` | Etykieta | Opis | |---|---|---| | `np I` | np. I | Nie podlega - z wyłączeniem art. 100 ust. 1 pkt 4 ustawy | | `np II` | np. II | Nie podlega - na podstawie art. 100 ust. 1 pkt 4 ustawy | | `zw` | zw. | Zwolnione od podatku | | `oo` | o.o. | Odwrotne obciążenie | | `0 KR` | 0% - KR | 0% w przypadku sprzedaży towarów i usług na terytorium kraju | | `0 WDT` | 0% - WDT | 0% w przypadku wewnątrzwspólnotowej dostawy towarów (WDT) | | `0 EX` | 0% - EX | 0% w przypadku eksportu towarów | | `marza` | marża | Procedura marży (art. 119 i 120): na fakturze tylko kwoty brutto, w KSeF suma w `P_13_11`. Wymaga `annotations.marginProcedure`. Nie ma jej na liście `vatRates`. | | `3` | 3% | | | `4` | 4% | | | `5` | 5% | | | `7` | 7% | | | `8` | 8% | | | `22` | 22% | | | `23` | 23% | | ## Metody płatności (`paymentMethods`) Wartość `paymentMethodId` przesyłasz w dokumencie. | `paymentMethodId` | Nazwa | |---|---| | `1` | Gotówka | | `2` | Karta | | `3` | Bon | | `4` | Czek | | `5` | Kredyt | | `6` | Przelew | | `7` | Płatność mobilna | ## Statusy płatności (`paymentStatuses`) | `id` | Nazwa | |---|---| | `0` | Zapłacono | | `1` | Do zapłaty | ## Waluty (`currencies`) `availableExchangeRate` mówi, czy dla danej waluty można podać `exchangeRate`. | `id` | Nazwa | `availableExchangeRate` | |---|---|---| | `PLN` | Złoty polski | nie | | `EUR` | Euro | tak | | `USD` | Dolar amerykański | tak | | `GBP` | Funt brytyjski | tak | | `CHF` | Frank szwajcarski | tak | | `DKK` | Korona duńska | tak | | `NOK` | Korona norweska | tak | | `SEK` | Korona szwedzka | tak | | `CZK` | Korona czeska | tak | | `HUF` | Forint | tak |
Pokaż wszystkie (18) | `id` | Nazwa | `availableExchangeRate` | |---|---|---| | `RUR` | Rubel | nie | | `LTL` | Lit litewski | nie | | `UAH` | Hrywna | tak | | `BGN` | Lewa | tak | | `ROL` | Lej rumuński | nie | | `JPY` | Jen | tak | | `TRL` | Lira turecka | nie | | `HRK` | Kuna | nie |
## Kody krajów (`countries`) Kod ISO 3166-1 alpha-2 przesyłasz w `countryCode` (kontrahent). | `code` | Nazwa | |---|---| | `PL` | Polska | | `AT` | Austria | | `BE` | Belgia | | `BG` | Bułgaria | | `HR` | Chorwacja | | `CY` | Cypr | | `CZ` | Czechy | | `DK` | Dania | | `EE` | Estonia | | `FI` | Finlandia |
Pokaż wszystkie (68) | `code` | Nazwa | |---|---| | `FR` | Francja | | `GR` | Grecja | | `ES` | Hiszpania | | `NL` | Holandia | | `IE` | Irlandia | | `LT` | Litwa | | `LU` | Luksemburg | | `LV` | Łotwa | | `MT` | Malta | | `DE` | Niemcy | | `PT` | Portugalia | | `RO` | Rumunia | | `SK` | Słowacja | | `SI` | Słowenia | | `SE` | Szwecja | | `HU` | Węgry | | `IT` | Włochy | | `GB` | Wielka Brytania | | `US` | Stany Zjednoczone | | `CA` | Kanada | | `CH` | Szwajcaria | | `NO` | Norwegia | | `UA` | Ukraina | | `RU` | Rosja | | `CN` | Chiny | | `JP` | Japonia | | `KR` | Korea Południowa | | `IN` | Indie | | `BR` | Brazylia | | `MX` | Meksyk | | `AU` | Australia | | `NZ` | Nowa Zelandia | | `ZA` | Republika Południowej Afryki | | `TR` | Turcja | | `IL` | Izrael | | `AE` | Zjednoczone Emiraty Arabskie | | `SA` | Arabia Saudyjska | | `SG` | Singapur | | `TH` | Tajlandia | | `VN` | Wietnam | | `ID` | Indonezja | | `MY` | Malezja | | `PH` | Filipiny | | `AR` | Argentyna | | `CL` | Chile | | `CO` | Kolumbia | | `EG` | Egipt | | `IS` | Islandia | | `RS` | Serbia | | `BA` | Bośnia i Hercegowina | | `ME` | Czarnogóra | | `MK` | Macedonia Północna | | `AL` | Albania | | `BY` | Białoruś | | `MD` | Mołdawia | | `GE` | Gruzja | | `AM` | Armenia | | `AZ` | Azerbejdżan |
## Kody GTU (`gtu`) Opcjonalny `gtu` w pozycji dokumentu. | `id` | Opis | |---|---| | `GTU_01` | Dostawa napojów alkoholowych | | `GTU_02` | Dostawa paliw silnikowych | | `GTU_03` | Dostawa oleju opałowego, olejów smarownych oraz smarów plastycznych itp. | | `GTU_04` | Dostawa wyrobów tytoniowych oraz wyborów nowatorskich | | `GTU_05` | Dostawa odpadów | | `GTU_06` | Dostawa urządzeń elektronicznych oraz części i materiałów do nich | | `GTU_07` | Dostawa pojazdów oraz części samochodowych | | `GTU_08` | Dostawa metali szlachetnych oraz nieszlachetnych | | `GTU_09` | Dostawa leków oraz wyrobów medycznych | | `GTU_10` | Dostawa budynków, budowli i gruntów | | `GTU_11` | Świadczenie usług w zakresie przenoszenia uprawnień do emisji gazów cieplarnianych | | `GTU_12` | Świadczenie usług doradczych, księgowych, prawnych, zarządczych, szkoleniowych, marketingowych, reklamowych, badania rynku i prac rozwojowych | | `GTU_13` | Świadczenie usług transportowych i gospodarki magazynowej | ## Procedury marży (`marginProcedures`) `id` przesyłasz w `annotations.marginProcedure.procedureId`. | `id` | Opis | |---|---| | `1` | Procedura marży dla biur podróży | | `2` | Procedura marży - towary używane | | `3` | Procedura marży - dzieła sztuki | | `4` | Procedura marży - przedmioty kolekcjonerskie i antyki | ## Typy zwolnienia z VAT (`vatExemptionTypes`) `type` przesyłasz w `annotations.vatExemption.exemptionType`, a podstawę prawną w `legalBasis` (wzór w kolumnie „Przykład"). | `type` | Opis | Przykład (`legalBasis`) | |---|---|---| | `A` | Przepis ustawy albo aktu wydanego na podstawie ustawy, na podstawie którego podatnik stosuje zwolnienie | np. art. 43 ust. 1 pkt 19 ustawy o VAT | | `B` | Przepis dyrektywy 2006/112/WE, który zwalnia od podatku taką dostawę lub usługę | np. art. 132 ust. 1 lit. c dyrektywy 2006/112/WE | | `C` | Inna podstawa prawna wskazująca na zwolnienie dostawy lub usługi | np. zwolnienie wynikające z umowy międzynarodowej | ## Nagłówki dat płatności (`paymentDateHeaders`) `id` przesyłasz w `additionalPaymentDateHeaderId`. | `id` | Nazwa | |---|---| | `0` | Data płatności | | `1` | Data dokonania płatności | | `2` | Data otrzymania zapłaty | | `3` | Końcowa data płatności | ## Nagłówki dat dodatkowych (`additionalDateHeaders`) `id` przesyłasz w `subDateHeaderId` (nagłówek nad datą sprzedaży). | `id` | Nazwa | |---|---| | `0` | Data sprzedaży | | `1` | Data wykonania usługi | | `2` | Data dokonania dostawy towarów | | `3` | Data zakończenia dostawy towarów | | `4` | Data zakończenia świadczenia usług | | `5` | Data otrzymania zaliczki | | `6` | Data otrzymania zapłaty | | `7` | Termin realizacji zamówienia | | `8` | Data otrzymania towaru | | `9` | Data wydania towaru | | `10` | Data zaliczki | | `11` | Termin ważności oferty | --- # Limity zapytań API stosuje limity zapytań, aby zapewnić szybkość i równe traktowanie. Limity są liczone dla aplikacji (konta API), którą identyfikuje token, a nie dla adresu IP. Są najbardziej restrykcyjne dla kosztownych endpointów: | Endpoint | Limit | |---|---| | `POST /documents` | 30 zapytań / 60 s | | `POST /documents/validate` | 30 zapytań / 60 s | | `POST /warehouse/documents` | 30 zapytań / 60 s | | `GET /documents` | 10 zapytań / 30 s | | `GET /clients` | 10 zapytań / 30 s | | `GET /products` | 10 zapytań / 30 s | | `POST /documents/{id}/send-to-ksef` | 10 zapytań / 60 s | | `GET /company/nip-lookup` | 10 zapytań / 60 s | | `GET /documents/{id}/download` | 5 zapytań / 60 s | | `GET /documents/{id}/ksef-status` | 5 zapytań / 60 s | | `POST /ksef/connection/test` | 5 zapytań / 60 s | | `POST /webhooks/{id}/test` | 5 zapytań / 60 s | | Pozostałe endpointy | 45 zapytań / 60 s (wspólny licznik) | Każda aplikacja ma własne liczniki, niezależnie od adresu IP, z którego wysyła zapytania. Kilka integracji działających na jednym serwerze nie dzieli więc limitu, jeśli każda używa własnej aplikacji, a jedna aplikacja wywoływana z kilku serwerów ma jeden wspólny limit. Każdy endpoint z tabeli ma osobny licznik, a wszystkie pozostałe endpointy dzielą jeden wspólny. Odpowiedzi niosą nagłówki `X-RateLimit-Limit`, `X-RateLimit-Remaining` i `X-RateLimit-Reset` (czas uniksowy końca okna); przy zapytaniu z prawidłowym tokenem opisują limit aplikacji dla wywołanego endpointu. Limit każdego endpointu jest też wypisany w jego opisie w [specyfikacji OpenAPI](/openapi.json). Dodatkowo obowiązuje ogólny limit **240 zapytań na minutę z jednego adresu IP**, łącznie dla wszystkich endpointów i aplikacji. Jest sprawdzany przed uwierzytelnieniem, więc liczy też zapytania z brakującym, błędnym albo wygasłym tokenem. Takie zapytania nie zużywają limitu aplikacji. Przy zwykłym ruchu tego limitu nie odczujesz. Ma znaczenie, gdy wiele aplikacji wysyła zapytania z jednego adresu (np. zza wspólnego NAT-u) albo gdy klient ponawia zapytania z nieprawidłowym tokenem. Po przekroczeniu któregokolwiek limitu otrzymasz odpowiedź `429` z kodem `RATE_LIMIT_EXCEEDED` i nagłówkiem `Retry-After` (liczba sekund). Wycofaj się i ponów zapytanie, najlepiej z wykładniczym wycofywaniem i losowym odchyleniem (jitter). Jeśli zapytanie zawierało nagłówek [`Idempotency-Key`](/docs/idempotency), ponawianie jest bezpieczne. ```ts async function withRetry(fn: () => Promise): Promise { for (let i = 0; i < 5; i++) { const res = await fn(); if (res.status !== 429) return res.json() as Promise; const retryAfter = Number(res.headers.get('Retry-After') ?? 0) * 1000; const wait = Math.max(retryAfter, Math.min(2 ** i * 500 + Math.random() * 250, 8000)); await new Promise(r => setTimeout(r, wait)); } throw new Error('rate limited'); } ``` Projektuj z myślą o limitach: buforuj token OAuth, grupuj zapytania tam, gdzie to możliwe, i zamiast odpytywania preferuj [webhooki](/docs/webhooks). ## Uczciwe korzystanie Limity zapytań istnieją po to, by API było szybkie dla wszystkich. Po przekroczeniu limitu uszanuj nagłówek `Retry-After` i wycofaj się (wykładniczy backoff z jitterem). Klient, który ignoruje `429` i dalej zarzuca endpoint zapytaniami, zostaje objęty **krótkim automatycznym cooldownem**, który wydłuża się przy kontynuacji, więc zawsze opłaca się zwolnić, a nie ponawiać agresywniej. Uporczywe lub rażące nadużycia mogą skończyć się czasowym zawieszeniem klienta API. Jeśli Twój przypadek naprawdę wymaga wyższych limitów, skontaktuj się z pomocą zamiast obchodzić ograniczenia. --- # Kolejka renderowania PDF `GET /documents/{id}/download` renderuje PDF faktury po stronie serwera. Renderowanie obciąża procesor, więc działa w niewielkiej puli wątków roboczych z ograniczoną kolejką. Pod dużym obciążeniem kolejka może się zapełnić. Gdy tak się stanie, API **nie** każe Ci czekać ani nie kończy żądania timeoutem. Odpowiada natychmiast kodem `429` z kodem błędu `WAIT_PDF_QUEUE`, informując, że należy ponowić zapytanie za chwilę. To co innego niż [limity kwotowe](/docs/rate-limits). Oba zwracają `429`, ale znaczą co innego: | | `WAIT_PDF_QUEUE` | Limit kwotowy | |---|---|---| | Przyczyna | pula renderująca chwilowo wysycona | aplikacja przekroczyła swój limit na endpoint | | `message` | `WAIT_PDF_QUEUE` | ogólny komunikat o limicie | | Czas trwania | sekundy (kolejka szybko schodzi) | do zresetowania okna limitu | | Co robić | ponów po `Retry-After`, bez spowalniania ogólnego tempa | wycofaj się, zwolnij, uszanuj cooldown | Rozróżniaj po polu `message` (kodzie błędu), nigdy po samym statusie HTTP, aby odróżnić te dwa przypadki. ## Co otrzymujesz w odpowiedzi ```http HTTP/1.1 429 Too Many Requests Retry-After: 5 Content-Type: application/json ``` ```json { "success": false, "data": null, "message": "WAIT_PDF_QUEUE", "executionTime": 2 } ``` Nagłówek `Retry-After` zawiera sugerowany czas oczekiwania w sekundach (zwykle `5`). Uszanuj go: slot renderujący zwalnia się szybko, a ponowienie niemal zawsze kończy się sukcesem w kolejnym podejściu. ## Jak to obsłużyć Pobieranie to operacja odczytu, więc ponawianie jest zawsze bezpieczne. Nie potrzeba nagłówka `Idempotency-Key`. Ponawiaj, dopóki kod to `WAIT_PDF_QUEUE`, czekając między próbami przez czas z `Retry-After`: ```ts async function downloadInvoicePdf(documentId: string, token: string): Promise { const url = `https://prod.api.fakturnia.pl/external/v1/documents/${documentId}/download`; for (let attempt = 0; attempt < 10; attempt++) { const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } }); if (res.ok) return res.blob(); if (res.status === 429) { const body = await res.clone().json().catch(() => null); // Przejściowe przeciążenie kolejki renderowania: poczekaj sugerowany czas i ponów. if (body?.message === 'WAIT_PDF_QUEUE') { const retryAfter = Number(res.headers.get('Retry-After')) || 5; await new Promise(r => setTimeout(r, retryAfter * 1000)); continue; } } // Każdy inny status (w tym kwotowe 429) to realny błąd tego wywołania. throw new Error(`Pobieranie nie powiodło się: ${res.status}`); } throw new Error('PDF wciąż nie jest gotowy po kilku ponowieniach'); } ``` > **Pokaż oczekiwanie użytkownikowi.** W aplikacji webowej i mobilnej Fakturnia przy `WAIT_PDF_QUEUE` pojawia się niewielki dialog „przygotowujemy plik”, który ponawia żądanie w tle, aż PDF dotrze. Krótki, uczciwy wskaźnik postępu jest lepszy niż spinner, który wygląda na zawieszony. ## Dlaczego kolejka, a nie zwykłe czekanie Trzymanie otwartego połączenia, dopóki długi render się nie skończy, blokowałoby zasoby serwera i groziło timeoutami po stronie klienta pod obciążeniem. Ograniczona kolejka z szybką informacją zwrotną `429` utrzymuje responsywność API dla wszystkich: reszta API nie zwalnia z powodu serii pobrań w toku, a Twój klient dostaje czytelny, ponawialny sygnał zamiast nieprzejrzystego timeoutu. Zobacz [Błędy](/docs/errors), aby poznać pełną kopertę błędu, oraz [Limity zapytań](/docs/rate-limits) w sprawie zachowania kwotowego. --- # Paginacja Endpointy zwracające listy (`GET /documents`, `GET /clients`, `GET /products`, `GET /recurring-rules`, `GET /recurring-rules/{id}/history`, `GET /webhooks/{id}/deliveries`, `GET /documents/{id}/history`, `GET /warehouse/movements`) są paginowane za pomocą parametrów zapytania: | Parametr | Znaczenie | |---|---| | `currentPage` | numer strony liczony od 1 | | `itemsPerPage` | rozmiar strony; wartość domyślna i maksymalna zależą od listy, większe wartości są przycinane do maksimum | | `searchQuery` | filtr tekstowy (tam, gdzie jest obsługiwany) | Domyślny i maksymalny rozmiar strony: | Lista | Domyślnie | Maksymalnie | |---|---|---| | `GET /documents`, `GET /clients`, `GET /products` | 10 | 100 | | `GET /recurring-rules` | 50 | 100 | | `GET /recurring-rules/{id}/history` | 50 | 200 | | `GET /warehouse/movements` | 50 | 100 | | `GET /documents/{id}/history` | 20 | 100 | | `GET /webhooks/{id}/deliveries` | 20 | 100 | Wartości dla każdego endpointu podaje też [specyfikacja OpenAPI](/openapi.json) przy parametrze `itemsPerPage`. Odpowiedź zawiera stronę wyników oraz pola `totalCount` i `totalPages`: ```bash curl -s "https://prod.api.fakturnia.pl/external/v1/documents?currentPage=1&itemsPerPage=50" \ -H "Authorization: Bearer $TOKEN" ``` ```json { "success": true, "data": { "documents": [ /* … */ ], "currentPage": 1, "itemsPerPage": 50, "totalCount": 184, "totalPages": 4, "searchQuery": "" }, "executionTime": 38 } ``` Iteruj, zwiększając wartość `currentPage`, aż przekroczysz `totalPages`. Krótkie, ograniczone listy (`GET /warehouse`, `GET /employees`, `GET /webhooks`, `GET /email-accounts`) zwracają wszystko naraz. Strumienie aktywności (`GET /activity`, `GET /employees/{id}/activity`) używają kursora czasowego `before` / `nextBefore`, opisanego w [Historii i aktywności](/docs/activity). ## Filtry listy dokumentów `GET /documents` obsługuje dodatkowe filtry, które można łączyć: | Parametr | Znaczenie | |---|---| | `documentType` | typ dokumentu lub lista typów rozdzielona przecinkami (np. `FakturaVAT,FakturaKorygujaca`) | | `dateFrom` | data wystawienia od, włącznie (`YYYY-MM-DD`) | | `dateTo` | data wystawienia do, włącznie (`YYYY-MM-DD`) | | `buyerNip` / `buyerVatUE` / `buyerIdentifier` | dokumenty dla nabywcy o danym identyfikatorze | | `excludeIds` | lista ID dokumentów do pominięcia, rozdzielona przecinkami | | `onlyUnlinked` | tylko dokumenty bez [powiązania z zamówieniem](/docs/external-references) | ```bash curl -s "https://prod.api.fakturnia.pl/external/v1/documents?documentType=FakturaVAT&dateFrom=2026-01-01&dateTo=2026-01-31" \ -H "Authorization: Bearer $TOKEN" ``` Nieznany `documentType` albo data w złym formacie zwraca `400` z kodem `INVALID_PARAMS`. Dopuszczalne typy znajdziesz w [słownikach](/docs/dictionaries). Listy podlegają [limitom zapytań](/docs/rate-limits) — przechodź po stronach sekwencyjnie i buforuj wyniki zamiast odpytywać agresywnie. --- # Idempotentność Wywołania sieciowe kończą się niepowodzeniem w niejednoznaczny sposób: żądanie przekracza limit czasu, ale czy dokument został utworzony? Klucze idempotentności pozwalają **bezpiecznie ponawiać** modyfikujące żądania bez tworzenia duplikatów. ## Jak to działa W żądaniach `POST` tworzących zasoby należy przesłać nagłówek `Idempotency-Key` (unikalną wartość dla każdej logicznej operacji, idealny jest UUID v4): ``` Idempotency-Key: 7c1f1d6e-0b2a-4a1e-9b1a-2a4e9b1a2a4e ``` - **Pierwsze** żądanie z danym kluczem jest wykonywane normalnie, a jego wynik zostaje zapamiętany. - Każde **ponowienie** z **tym samym kluczem** zwraca pierwotny wynik, zamiast tworzyć drugi dokument. - Klucze są przechowywane przez **24 godziny**. Należy wygenerować jeden klucz na intencję (np. „utworzenie faktury dla zamówienia #1234") i używać go we wszystkich ponowieniach tej intencji, nie zaś w różnych operacjach. ```ts import { randomUUID } from 'node:crypto'; const key = randomUUID(); async function createInvoiceWithRetry(payload: unknown) { for (let attempt = 0; attempt < 3; attempt++) { const res = await fetch('https://prod.api.fakturnia.pl/external/v1/documents', { method: 'POST', headers: { 'Authorization': `Bearer ${await getToken()}`, 'Content-Type': 'application/json', 'Idempotency-Key': key, // same key on every retry }, body: JSON.stringify(payload), }); if (res.status < 500 && res.status !== 429) return res.json(); await new Promise(r => setTimeout(r, 2 ** attempt * 500)); // backoff } throw new Error('exhausted retries'); } ``` > **Klucze idempotentności należy łączyć z mechanizmem backoff.** Ponawianie odpowiedzi `5xx`/`429` jest bezpieczne tylko wtedy, gdy ten sam klucz gwarantuje, że operacja zostanie wykonana co najwyżej raz. Tworzenie dokumentów to główny idempotentny punkt końcowy. Operacje odczytu są z natury idempotentne i nie wymagają klucza. --- # Webhooki Webhooki wysyłają zdarzenia na Twój serwer w momencie, gdy coś się wydarzy, dzięki czemu nie musisz odpytywać API. Rejestrujesz endpoint HTTPS, wybierasz zdarzenia, weryfikujesz podpis przy każdej dostawie i szybko odpowiadasz kodem `2xx`. Webhooki, jak całe REST API, wymagają planu MAX: zdarzenia są wysyłane tylko wtedy, gdy konto ma aktywny plan MAX i co najmniej jedną aktywną aplikację API. ## Zdarzenia | Zdarzenie | Wyzwalane, gdy | `data` | |---|---|---| | `document.created` | dokument zostaje utworzony w aplikacji webowej albo mobilnej (także szkic), przez REST API (także przez asystenta AI połączonego przez MCP) albo przez regułę cykliczną; dotyczy też dokumentów magazynowych; faktury pobrane z KSeF mają osobne zdarzenie `document.ksefImported` | `documentId` | | `document.updated` | dokument zostaje zmieniony w aplikacji albo przez API, w tym po powiązaniu z zamówieniem, po wystawieniu do niego korekty w aplikacji albo przez API, po wystawieniu szkicu albo zamianie dokumentu z powrotem w szkic oraz po zmianie w aplikacji oznaczenia wysłania, wydruku albo pobrania | `documentId` | | `document.deleted` | dokument zostaje usunięty | `documentId` | | `document.sentToKSeF` | KSeF przyjął dokument i nadał mu numer, także gdy przetwarzanie dokończyło się w tle | `documentId` | | `document.ksefRejected` | KSeF odrzucił dokument albo wysyłka zakończyła się błędem (walidacja, timeout, utracona sesja) | `documentId` | | `document.paymentStatusChanged` | zmienia się oznaczenie zapłaty dokumentu wystawionego jako „Do zapłaty” (otrzymano albo cofnięto) | `documentId` | | `document.ksefImported` | synchronizacja z KSeF pobrała nowe faktury: otrzymane od dostawców albo wystawione poza Fakturnią; do 100 identyfikatorów w jednej dostawie, większy import przychodzi w kilku dostawach | `documentIds` (`documentId` = pierwszy z nich) | | `client.created` / `client.updated` / `client.deleted` | kontrahent zostaje dodany, zmieniony, usunięty | `clientId` (`clientIds` przy usuwaniu wielu) | | `product.created` / `product.updated` / `product.deleted` | produkt zostaje dodany, zmieniony, usunięty | `productId` | | `webhook.test` | wywołasz `POST /webhooks/{id}/test`; tego zdarzenia nie da się subskrybować, dostaje je każdy webhook na żądanie | `webhookId` | Zdarzenia dokumentów przychodzą niezależnie od tego, kto wykonał akcję: integracja przez API (także asystent AI przez MCP), użytkownik w aplikacji webowej albo mobilnej, reguła cykliczna czy automat (kolejka KSeF). Zdarzenia kontrahentów i produktów także przychodzą zarówno z API, jak i z aplikacji. Korekta wystawiona w aplikacji albo przez API wyzwala `document.created` dla siebie i `document.updated` dla faktury korygowanej. Szkic zapisany w aplikacji też wyzwala `document.created` (w odczycie ma `isDraft: true`). Wystawienie szkicu w aplikacji wyzwala `document.updated` (w odczycie `isDraft` zmienia się na `false`). To samo zdarzenie przychodzi po zamianie dokumentu z powrotem w szkic i po zmianie w aplikacji oznaczeń `isSent`, `isPrinted` i `isDownloaded`, także przy pierwszym wydruku albo pobraniu dokumentu. Liczy się tylko rzeczywista zmiana: ponowne pobranie już pobranego dokumentu nie wysyła kolejnego zdarzenia. `document.updated` przychodzi też po potwierdzeniu albo cofnięciu wydania magazynowego, po usunięciu ostatniej korekty faktury (dla faktury, którą znowu można edytować), po wystawieniu WZ do faktury (dla faktury, która ma wtedy tę WZ w `linkedDocuments`), gdy reguła cykliczna wyśle wygenerowany dokument e-mailem (oznaczenie `isSent`) i wtedy, gdy synchronizacja z KSeF dopisze numer KSeF dokumentowi wystawionemu w Fakturni, ale wysłanemu do KSeF gdzie indziej. `client.created` przychodzi też dla kontrahenta zapisanego z danych nabywcy dokumentu utworzonego przez API albo MCP, a `client.updated` wtedy, gdy przy kontrahencie zostaną zapisane adresy e-mail (także przy wysyłce dokumentu na nowe adresy). Faktury pobrane z KSeF (zakupowe od dostawców i sprzedażowe wystawione poza Fakturnią) nie przychodzą jako `document.created`. Synchronizacja zgłasza je zdarzeniem `document.ksefImported`: identyfikatory nowych faktur są w `data.documentIds`, do 100 w jednej dostawie, więc większy import (np. pierwsza synchronizacja po połączeniu z KSeF) przychodzi w kilku dostawach, każda z własnym `id`. Każdy dokument odczytaj przez `GET /documents/{id}`: `source` ma wartość `ksef`, a `direction` odróżnia zakup (`expense`) od sprzedaży (`income`). ## Zarządzanie webhookami | Metoda | Ścieżka | Scope | Opis | |---|---|---|---| | `GET` | `/webhooks` | `webhooks:read` | lista webhooków (bez sekretów); z `?endpoint=` sprawdza, czy dany adres jest już zarejestrowany | | `POST` | `/webhooks` | `webhooks:create` | rejestruje endpoint na wybrane zdarzenia; odpowiedź `201` zawiera `secret` (tylko raz) | | `GET` | `/webhooks/{id}` | `webhooks:read` | jeden webhook z licznikami dostaw | | `PATCH` | `/webhooks/{id}` | `webhooks:update` | zmiana `endpoint`, `events` (cała lista), `description`, `status` (`ACTIVE` / `INACTIVE`) | | `DELETE` | `/webhooks/{id}` | `webhooks:delete` | usunięcie; oczekujące dostawy są anulowane | | `POST` | `/webhooks/{id}/rotate-secret` | `webhooks:update` | nowy sekret podpisujący, zwracany raz | | `GET` | `/webhooks/{id}/deliveries` | `webhooks:read` | historia dostaw, stronicowana (`currentPage`, `itemsPerPage` do 100, `?status=`), do diagnozowania odbiorcy | | `POST` | `/webhooks/{id}/test` | `webhooks:update` | wysyła `webhook.test` od razu i zwraca wynik HTTP; limit 5 na minutę | | `POST` | `/webhooks/initial` | `webhooks:create` | dla wtyczek: rejestracja na wszystkie zdarzenia, idempotentna per adres, z opcją `rotateSecret` | ```bash curl -s -X POST https://sandbox.api.fakturnia.pl/external/v1/webhooks \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{ "endpoint": "https://erp.example.com/hooks/fakturnia", "events": ["document.created", "document.sentToKSeF", "document.ksefRejected", "document.paymentStatusChanged"], "description": "ERP" }' ``` ```json { "success": true, "message": "Webhook created.", "data": { "webhook": { "id": "681a3b2c4d5e6f7a8b9c0d1e", "endpoint": "https://erp.example.com/hooks/fakturnia", "description": "ERP", "events": ["document.created", "document.sentToKSeF", "document.ksefRejected", "document.paymentStatusChanged"], "status": "ACTIVE", "successCount": 0, "failureCount": 0, "consecutiveFailures": 0, "lastDeliveryAt": null, "createdAt": "2026-09-26T10:00:00.000Z", "updatedAt": "2026-09-26T10:00:00.000Z" }, "secret": "9f2a7c1d4e6b8a0c3d5f7e9b1a2c4d6e8f0a1b3c5d7e9f2a4b6c8d0e1f3a5b7c" } } ``` Zapisz `secret` od razu: nie da się go później odczytać. Jeśli go zgubisz, wywołaj `POST /webhooks/{id}/rotate-secret` (najpierw zaktualizuj odbiorcę, potem rotuj, bo od tej chwili dostawy są podpisywane nowym sekretem). Endpoint musi być publicznym adresem HTTP(S); adresy prywatne i lokalne są odrzucane z `422 VALIDATION_ERROR` w polu `endpoint`. Dla jednego adresu istnieje jeden webhook (`409 CONFLICT` przy powtórce), więc listę zdarzeń zmieniaj przez `PATCH`. Zaraz po rejestracji wyślij ping testowy: ```bash curl -s -X POST https://sandbox.api.fakturnia.pl/external/v1/webhooks/681a3b2c4d5e6f7a8b9c0d1e/test \ -H "Authorization: Bearer $TOKEN" ``` ```json { "success": true, "data": { "delivery": { "id": "681a3b2c4d5e6f7a8b9c0d1f", "ok": true, "statusCode": 200, "status": "SUCCESS", "lastError": null } } } ``` Ping jest podpisany jak prawdziwa dostawa, nie jest ponawiany i nie liczy się do automatycznego wyłączenia. ## Format dostawy Każda dostawa to żądanie `POST` z treścią w formacie JSON oraz nagłówkami: | Nagłówek | Wartość | |---|---| | `X-Webhook-Event` | nazwa zdarzenia, np. `document.created` | | `X-Webhook-Delivery` | identyfikator dostawy, ten sam przy każdym ponowieniu (użyj do deduplikacji) | | `X-Webhook-Signature` | `HMAC-SHA256(rawBody, secret)` w postaci szesnastkowej małymi literami | | `Content-Type` | `application/json` | ```json { "id": "681a3b2c4d5e6f7a8b9c0d1f", "event": "document.created", "timestamp": "2026-09-26T10:30:00.000Z", "data": { "documentId": "681a3b2c4d5e6f7a8b9c0d2a" }, "resourceId": "681a3b2c4d5e6f7a8b9c0d2a" } ``` - `id` powtarza wartość nagłówka `X-Webhook-Delivery`. - `data` zawiera identyfikator zasobu nazwany po jego typie (`documentId`, `clientId`, `productId`, `webhookId`). Listę niosą dwa zdarzenia: `client.deleted` po usunięciu kilku kontrahentów naraz w aplikacji (`clientIds`) i `document.ksefImported` (`documentIds`, zawsze lista, do 100 identyfikatorów); pole pojedyncze wskazuje wtedy pierwszy z nich. - `resourceId` (i `resourceIds` przy zdarzeniach z listą) to to samo, zachowane dla starszych odbiorców. - Dostawa nie niesie pełnego obiektu. Po odbiorze pobierz zasób (`GET /documents/{id}`), żeby przeczytać aktualny stan; przy `document.sentToKSeF` i `document.ksefRejected` sprawdź `GET /documents/{id}/ksef-status`. Pełny schemat treści to `WebhookEventPayload` w [specyfikacji OpenAPI](/openapi.json). ## Weryfikacja podpisu Oblicz `HMAC-SHA256` na **surowej treści żądania** przy użyciu sekretu webhooka i porównaj wynik z nagłówkiem `X-Webhook-Signature` w czasie stałym. Odrzuć wszystko, co się nie zgadza. ```ts import express from 'express'; import crypto from 'node:crypto'; const app = express(); // Capture the RAW body; the signature is computed over exact bytes. app.post('/webhooks/fakturnia', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.header('X-Webhook-Signature') ?? ''; const expected = crypto .createHmac('sha256', process.env.FAKTURNIA_WEBHOOK_SECRET!) .update(req.body) // Buffer of the raw body .digest('hex'); const ok = signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); if (!ok) return res.status(401).send('invalid signature'); const event = JSON.parse(req.body.toString('utf8')); // dedupe on event.id, then handle event.event / event.data and ACK fast: res.sendStatus(200); }); ``` ```python import hmac, hashlib def verify(raw_body: bytes, signature: str, secret: str) -> bool: expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature) ``` > **Zawsze weryfikuj podpis, zanim zaufasz payloadowi.** Żądanie bez podpisu lub z niezgodnym podpisem nie pochodzi z Fakturnia. ## Mechanizm ponawiania Pierwsza próba dostawy następuje od razu po zdarzeniu. Jeśli Twój endpoint nie odpowie statusem `2xx` w ciągu 10 sekund, Fakturnia kolejkuje zdarzenie i ponawia dostarczenie z rosnącymi odstępami: po 5 minutach, po 30 minutach, po 2 godzinach i po 8 godzinach od poprzedniej próby. Łącznie to **maksymalnie 5 prób** (pierwsza i 4 ponowienia). Przekierowania (`3xx`) nie są śledzone i liczą się jako niepowodzenie. Gdy któraś z prób zakończy się odpowiedzią `2xx`, dostawa jest udana. ![Schemat mechanizmu ponawiania wysyłki webhooków](/retry-flow.pl.svg) Po 5 nieudanych próbach dostawa dostaje status `FAILED`. Po **10 niepowodzeniach z rzędu** dla tego samego webhooka (niezależnie od zdarzenia) webhook przechodzi w status `ERROR` i przestaje dostawać zdarzenia. Popraw endpoint i włącz go ponownie przez `PATCH /webhooks/{id}` z `{ "status": "ACTIVE" }`; licznik niepowodzeń zaczyna wtedy od zera. Historię prób obejrzysz przez `GET /webhooks/{id}/deliveries`: ```json { "success": true, "data": { "deliveries": [ { "id": "681a…", "event": "document.created", "resourceId": "681a…", "status": "PENDING", "attempts": 2, "nextAttemptAt": "2026-09-26T11:05:00.000Z", "lastAttemptAt": "2026-09-26T10:35:00.000Z", "lastError": "HTTP 503", "createdAt": "2026-09-26T10:30:00.000Z" } ], "currentPage": 1, "itemsPerPage": 20, "totalCount": 1, "totalPages": 1 }, "executionTime": 12 } ``` Rekordy dostaw są przechowywane 30 dni. ## Dobre praktyki - **Odpowiadaj szybko (`2xx`) i przetwarzaj asynchronicznie.** Ciężką pracę wykonuj po potwierdzeniu odbioru; w przeciwnym razie dostawy mogą wygasnąć i zostać ponowione. - **Zapewnij idempotentność.** To samo zdarzenie może dotrzeć więcej niż raz. Usuwaj duplikaty na podstawie `id` dostawy (nagłówek `X-Webhook-Delivery`). - **Traktuj webhooki jako wskazówki, a nie źródło prawdy.** W przypadku czegokolwiek krytycznego pobierz zasób, aby uzyskać miarodajny stan. - **Zwracaj kod inny niż 2xx, aby wywołać ponawianie**, jeśli faktycznie nie możesz jeszcze przetworzyć żądania. - **Przechowuj sekret po stronie serwera** i rotuj go w razie wycieku. - **Subskrybuj tylko to, co obsługujesz.** Mniej zdarzeń to mniej dostaw do zignorowania i mniejsze ryzyko wpadnięcia w serię niepowodzeń. --- # Narzędzia API to zwykły REST + JSON z pełną specyfikacją OpenAPI 3, więc możesz pracować w dowolnym języku i narzędziu. Poniżej to, co przygotowaliśmy: kolekcja Postman, CLI do szybkich sprawdzeń i przepis na wygenerowanie typowanego klienta. ## Specyfikacja OpenAPI Kanoniczny kontrakt API to **[/openapi.json](/openapi.json)** (wersje językowe: [/openapi.pl.json](/openapi.pl.json), [/openapi.en.json](/openapi.en.json)). Zawiera każdy endpoint, schematy żądań i odpowiedzi z przykładami, kody błędów, nagłówki i wymagane scope'y. Ta sama specyfikacja zasila [dokumentację API](/api) i interaktywny [Swagger](/swagger.html?lang=pl). ## Wygeneruj klienta w swoim języku Wygeneruj klienta z OpenAPI, na przykład narzędziem `openapi-generator`: ```bash # Python openapi-generator generate \ -i https://developers.fakturnia.pl/openapi.json \ -g python -o ./fakturnia-python # PHP, C#, Java, Go, TypeScript: zmień -g na php, csharp, java, go, typescript-fetch ``` Możesz też po prostu skorzystać bezpośrednio z `fetch` / `requests`. Zobacz [Szybki start](/docs/quickstart) oraz [dokumentację API](/api), aby poznać dokładne kształty danych. Przy własnym kliencie pamiętaj o trzech rzeczach z przewodników: buforowaniu tokenu ([Uwierzytelnianie](/docs/authentication)), nagłówku `Idempotency-Key` przy zapisach ([Idempotentność](/docs/idempotency)) i ponowieniach `429` po `Retry-After` ([Limity zapytań](/docs/rate-limits)). ## Kolekcja Postman Zaimportuj gotową **[kolekcję Postman](/fakturnia.postman_collection.json)**: wszystkie endpointy pogrupowane według zasobów, z przykładowymi treściami żądań wziętymi wprost ze specyfikacji. Kolekcja zawiera żądanie **Authentication → get access token**, którego skrypt testowy zapisuje token do zmiennej kolekcji, dzięki czemu każde kolejne żądanie jest uwierzytelniane automatycznie. Jak użyć: 1. Zaimportuj `https://developers.fakturnia.pl/fakturnia.postman_collection.json` do Postmana. 2. Ustaw zmienne kolekcji `client_id` oraz `client_secret`. 3. Uruchom raz **Authentication → get access token**, a następnie wywołuj dowolny endpoint. 4. Aby testować na sandboxie, zmień zmienne `baseUrl` i `tokenUrl` na ich odpowiedniki z `sandbox.api.fakturnia.pl`. ## Wiersz poleceń: CLI `fakturnia` Niewielka, bezzależnościowa nakładka powłoki na `curl`, która sama pobiera, buforuje i dołącza token bearer. Przydatna do szybkich sprawdzeń i skryptów. ```bash curl -fsSL https://developers.fakturnia.pl/fakturnia.sh -o fakturnia && chmod +x fakturnia export FAKTURNIA_CLIENT_ID=client_... export FAKTURNIA_CLIENT_SECRET=secret_... export FAKTURNIA_ENV=sandbox # lub: production ./fakturnia GET /documents/ ./fakturnia POST /documents/ '{"documentType":"FakturaVAT","issueDate":"2026-06-10","buyer":{"identifierType":"nip","nip":"5261040828","name":"Acme Corp Sp. z o.o.","street":"Marszałkowska 1","postalCode":"00-001","city":"Warszawa","countryCode":"PL"},"products":[{"name":"Usługa","unitOfMeasure":"szt.","quantity":1,"netPrice":100,"vatRate":23}],"paymentMethodId":6,"paymentStatusId":1,"paymentDeadlineDate":"2026-06-24"}' ``` Domyślnie działa na **sandboxie**, na produkcję przełącz się świadomie. Wywołania `POST`/`PUT`/`PATCH` z treścią otrzymują automatyczny nagłówek `Idempotency-Key`. ## Dla agentów AI Plik [/skill.md](/skill.md) to przenośna instrukcja w formacie Agent Skills, a [/llms-full.txt](/llms-full.txt) cały korpus dokumentacji w jednym pliku. Szczegóły na stronie [Dla agentów i botów](/docs/agents). Asystent działający na koncie użytkownika łączy się przez [serwer MCP](/docs/mcp): ChatGPT, Claude i Claude Code, Gemini, Mistral Vibe (dawniej Le Chat), Perplexity, GitHub Copilot, Cursor albo Windsurf. Zamiast konta API jest logowanie OAuth 2.1, narzędzia są ograniczone do uprawnień użytkownika, a faktury wystawia się w prowadzonej ścieżce z podglądem. --- # Dla agentów, botów i robotów indeksujących Ta dokumentacja została zaprojektowana z myślą o **maszynowej czytelności w pierwszej kolejności**. Jeśli jesteś modelem LLM, autonomicznym agentem, robotem indeksującym lub generatorem kodu, wszystko, czego potrzebujesz, jest dostępne w ustrukturyzowanych, czytelnych formatach. Nigdy nie powinieneś analizować wyrenderowanego kodu HTML ani wykonywać JavaScriptu. ## Punkty końcowe dla maszyn | Zasób | URL | Format | |---|---|---| | Specyfikacja OpenAPI 3 | [`/openapi.json`](/openapi.json) | JSON: kanoniczny, kompletny kontrakt | | Umiejętność dla agenta | [`/skill.md`](/skill.md) | tekst: przenośna instrukcja w formacie [Agent Skills](https://code.claude.com/docs/en/skills) do wystawiania faktur i wysyłki do KSeF | | Indeks LLM | [`/llms.txt`](/llms.txt) | tekst: zwięzła mapa dokumentacji ([llmstxt.org](https://llmstxt.org)) | | Pełny korpus dokumentacji | [`/llms-full.txt`](/llms-full.txt) | tekst: wszystkie przewodniki połączone w jeden plik Markdown | | Dowolny przewodnik jako Markdown | `/.md` | tekst, np. [`/documents.md`](/documents.md) | | Kolekcja Postman | [`/fakturnia.postman_collection.json`](/fakturnia.postman_collection.json) | JSON: wszystkie endpointy z żądaniem tokenu OAuth | | CLI (nakładka na curl) | [`/fakturnia.sh`](/fakturnia.sh) | powłoka: bezzależnościowe CLI z wbudowanym OAuth | | Serwer MCP | `https://prod.api.fakturnia.pl/mcp` | Streamable HTTP, OAuth 2.1: narzędzia na koncie użytkownika dla ChatGPT, Claude, Claude Code, Gemini, Mistral Vibe (dawniej Le Chat), Perplexity, GitHub Copilot, Cursor, Windsurf i własnych agentów ([Serwer MCP](/docs/mcp)) | | Mapa witryny | [`/sitemap.xml`](/sitemap.xml) | XML | ## Gwarancje dla maszyn - **Treść bez JavaScriptu.** Każdy przewodnik jest dostępny jako czysty Markdown pod `/.md`, a cały korpus w `/llms-full.txt`; nie musisz renderować aplikacji. Indeksuj zwykłym żądaniem HTTP GET. - **Specyfikacja OpenAPI jest źródłem prawdy.** Ścieżki endpointów, parametry, schematy żądań/odpowiedzi, zabezpieczenia oraz zakresy uprawnień pochodzą z `/openapi.json`. Korzystaj z niej zamiast analizować tekst. - **Stabilne kody błędów.** Rozgałęziaj logikę na podstawie pola `message` (kod maszynowy), a nie tekstu czytelnego dla człowieka. Każda odpowiedź ma nagłówek `X-Request-ID`. Zobacz [Błędy](/docs/errors). - **Plan MAX.** Całe REST API (token, endpointy, webhooki) wymaga aktywnego planu MAX; `403 SUBSCRIPTION_REQUIRED` to problem planu, nie kodu. Nie ponawiaj, poinformuj użytkownika. - **Deterministyczne obliczenia kwot.** Reguły dotyczące kwot są w pełni opisane w sekcji [Typy dokumentów i kwoty](/docs/documents): `zaokrąglanie w górę połówek` do 2 miejsc po przecinku, `netValue + amountOfVat === grossValue` dla każdej pozycji. Każdą wartość można odtworzyć. ## Programowe budowanie integracji 1. Pobierz [`/openapi.json`](/openapi.json) i wygeneruj typowanego klienta lub odczytaj schematy bezpośrednio. 2. Uwierzytelnij się za pomocą OAuth 2.0 client credentials, aby uzyskać token bearer ([Uwierzytelnianie](/docs/authentication)). 3. Twórz dokumenty, przesyłając `quantity`, `netPrice`/`grossPrice`, `vatRate`; nigdy nie wyliczaj sum wcześniej. 4. Używaj nagłówka `Idempotency-Key` przy operacjach zapisu; ponawiaj `5xx`/`429` z wykładniczym opóźnieniem. 5. Subskrybuj [webhooki](/docs/webhooks) zamiast odpytywać; weryfikuj sygnaturę HMAC `X-Webhook-Signature`. ## Działanie w imieniu użytkownika: serwer MCP Gdy agent ma pracować na koncie konkretnego użytkownika (wystawić fakturę, sprawdzić płatności), nie potrzebuje konta API. Użytkownik podłącza go przez [serwer MCP](/docs/mcp): logowanie OAuth 2.1 z ekranem zgody w aplikacji, narzędzia ograniczone do uprawnień osoby, która się połączyła, prowadzone wystawianie dokumentu z podglądem i potwierdzeniem oraz `confirm=true` przy operacjach nieodwracalnych. Adres: `https://prod.api.fakturnia.pl/mcp`. ## Zalecana polityka indeksowania Plik `/robots.txt` zezwala na indeksowanie dokumentacji i jawnie wskazuje artefakty maszynowe. Dokumentacja jest niewielka i statyczna, więc pojedyncze przejście przez `/llms-full.txt` wraz z `/openapi.json` daje pełny obraz. > Jeśli jesteś agentem działającym w imieniu użytkownika, możesz traktować `/llms-full.txt` jako pełny tekst tej witryny, a `/openapi.json` jako wykonywalny kontrakt. --- # Serwer MCP: faktury z ChatGPT, Claude, Gemini i innych asystentów AI Fakturnia udostępnia serwer **MCP (Model Context Protocol)**, dzięki któremu asystent AI pracuje bezpośrednio na koncie użytkownika: wystawia faktury, sprawdza płatności, wyszukuje kontrahentów, odczytuje stany magazynowe i wysyła dokumenty do KSeF. Serwer działa z ChatGPT, Claude i Claude Code, Gemini, Mistral Vibe (dawniej Le Chat), Perplexity, GitHub Copilot, Cursor i Windsurf, a także z innymi klientami MCP i z własnym agentem. Użytkownik łączy asystenta w aplikacji jednym logowaniem (OAuth 2.1), a każda zmiana danych przechodzi przez podgląd i potwierdzenie w rozmowie. Ta strona opisuje serwer od strony technicznej. Instrukcja dla użytkownika końcowego (krok po kroku ze zrzutami) jest w [pomocy Fakturnii](https://pomoc.fakturnia.pl/inne/asystent-ai-chatgpt-claude). > **W skrócie.** Adres serwera: `https://prod.api.fakturnia.pl/mcp` (sandbox: `https://sandbox.api.fakturnia.pl/mcp`). Transport: Streamable HTTP, tylko `POST`, bez sesji. Autoryzacja: OAuth 2.1 z odkrywaniem (RFC 9728 i RFC 8414), PKCE S256, rejestracja dynamiczna i dokumenty client_id; alternatywnie klucz `fmcp_…` w nagłówku `Authorization`. Wymagany aktywny plan MAX. ## Co daje MCP w porównaniu z REST API | | REST API | Serwer MCP | |---|---|---| | Dla kogo | integracje systemowe (sklep, ERP, CRM) | asystenci AI i agenci działający w imieniu użytkownika | | Uwierzytelnianie | OAuth 2.0 client credentials (konto API) | OAuth 2.1 authorization code z PKCE (zgoda użytkownika) albo klucz osobisty | | Kto łączy | właściciel konta | właściciel albo pracownik, każdy ze swoimi uprawnieniami | | Plan | MAX | MAX | | Wystawianie dokumentu | `POST /documents` | prowadzona ścieżka: `prepareDocument` (podgląd, ostrzeżenia) i `issueDocument` (po potwierdzeniu) | | Operacje nieodwracalne | wykonywane od razu | wymagają argumentu `confirm=true` po zgodzie użytkownika | Narzędzia MCP wykonują się przez to samo External API (z pełną walidacją, limitami i logami), więc reguły z pozostałych przewodników (kwoty, typy dokumentów, KSeF) obowiązują bez zmian. ## Adresy i odkrywanie | Zasób | Produkcja | Sandbox | |---|---|---| | Serwer MCP | `https://prod.api.fakturnia.pl/mcp` | `https://sandbox.api.fakturnia.pl/mcp` | | Metadane zasobu (RFC 9728) | `https://prod.api.fakturnia.pl/.well-known/oauth-protected-resource/mcp` | `https://sandbox.api.fakturnia.pl/.well-known/oauth-protected-resource/mcp` | | Metadane serwera autoryzacji (RFC 8414) | `https://prod.api.fakturnia.pl/.well-known/oauth-authorization-server` | `https://sandbox.api.fakturnia.pl/.well-known/oauth-authorization-server` | | Rejestracja klienta (RFC 7591) | `POST https://prod.api.fakturnia.pl/mcp/oauth/register` | `POST https://sandbox.api.fakturnia.pl/mcp/oauth/register` | | Autoryzacja | `GET https://prod.api.fakturnia.pl/mcp/oauth/authorize` | `GET https://sandbox.api.fakturnia.pl/mcp/oauth/authorize` | | Token | `POST https://prod.api.fakturnia.pl/mcp/oauth/token` | `POST https://sandbox.api.fakturnia.pl/mcp/oauth/token` | Żądanie bez tokenu dostaje `401` z nagłówkiem `WWW-Authenticate: Bearer realm="mcp", resource_metadata="…/.well-known/oauth-protected-resource/mcp"`, od którego klienci MCP zaczynają odkrywanie. `GET` i `DELETE` na `/mcp` zwracają `405`: transport jest bezstanowy, nagłówek `Mcp-Session-Id` jest ignorowany. Serwer obsługuje obie ery protokołu: żądania z metadanymi w `params._meta` (rewizja 2026-07-28) i starsze klienty zaczynające od `initialize`. ## Podłączanie klientów Każdemu klientowi wystarczy adres serwera `https://prod.api.fakturnia.pl/mcp`. Klient z obsługą OAuth rejestruje się sam (rejestracja dynamiczna) i otwiera ekran zgody Fakturnii, a klient bez OAuth wysyła klucz dostępu `fmcp_…` (opis w sekcji Uwierzytelnianie). ### ChatGPT Ustawienia, Konektory, Utwórz: nazwa `Fakturnia`, adres serwera MCP, uwierzytelnianie OAuth. ChatGPT rejestruje się dynamicznie, otwiera ekran zgody Fakturnii i po kliknięciu „Zezwól" dostaje tokeny. Konektory MCP działają w płatnych planach ChatGPT; w części kont wymagają trybu deweloperskiego. ### Claude (claude.ai, Claude Desktop) Ustawienia, Konektory, „Dodaj niestandardowy konektor": nazwa `Fakturnia`, adres serwera. Pola Client ID i Client secret zostają puste (rejestracja dynamiczna albo dokument client_id). Po „Połącz" otwiera się ekran zgody. ### Claude Code ```bash claude mcp add --transport http fakturnia https://prod.api.fakturnia.pl/mcp ``` Następnie `/mcp` w Claude Code, `fakturnia`, `Authenticate`: przeglądarka otwiera ekran zgody. Claude Code rejestruje `redirect_uri` na `http://localhost:/callback` (obie formy, `localhost` i `127.0.0.1`, są dopuszczone). ### Gemini **Aplikacja Gemini (gemini.google.com).** W ustawieniach otwórz Connected Apps, potem Custom apps i wybierz „Add a custom app". Wklej adres serwera MCP, kliknij „Next" i zatwierdź dostęp na ekranie zgody Fakturnii. Gemini rejestruje się dynamicznie, więc nie podajesz Client ID ani sekretu. Po podłączeniu w przeglądarce Fakturnia działa też w aplikacji mobilnej Gemini, a w poleceniu wybierzesz ją przez `@`. Google udostępnia własne aplikacje w Gemini na razie tylko w USA: dla osób pełnoletnich, po angielsku, na prywatnym koncie Google (nie Workspace) i z włączonym zapisywaniem aktywności (Keep Activity). Gdy funkcja trafi do Polski, podłączysz Fakturnię tym samym adresem serwera. **Gemini Enterprise.** W edycji Business administrator zespołu może dodać połączenie z własnym serwerem MCP. Google wymaga transportu Streamable HTTP i publicznego adresu HTTPS z zaufanym certyfikatem, a serwer Fakturnii spełnia oba warunki. Adresy autoryzacji i tokenu znajdziesz w tabeli „Adresy i odkrywanie" powyżej, zakres (scope) to `mcp`. Jeśli formularz wymaga stałego Client ID i sekretu, użyj klienta prekonfigurowanego wydawanego przez Fakturnię (sekcja Uwierzytelnianie). **Gemini CLI.** Dodaj serwer w pliku `~/.gemini/settings.json`: ```json { "mcpServers": { "fakturnia": { "httpUrl": "https://prod.api.fakturnia.pl/mcp" } } } ``` Gemini CLI wykrywa OAuth po odpowiedzi `401` z serwera i otwiera ekran zgody w przeglądarce. ### Mistral Vibe (dawniej Le Chat) Le Chat nazywa się teraz Mistral Vibe, a konta, plany i ustawienia przeszły bez zmian. Na chat.mistral.ai otwórz Connectors, „Add connector" i zakładkę „Custom MCP Connector". Wpisz nazwę `Fakturnia` i adres serwera, potem kliknij „Connect". Vibe sam wykrywa logowanie OAuth 2.1 z rejestracją dynamiczną i otwiera ekran zgody Fakturnii. Jeśli wolisz klucz, wybierz uwierzytelnianie „HTTP Bearer Token" i wklej klucz `fmcp_…`. Własne konektory dodaje administrator organizacji; w planach Free, Pro i Student administratorem jest właściciel konta. ### Perplexity Własne konektory zdalne (custom remote connectors) są dostępne w planach Pro, Max i Enterprise, w planie Free ich nie ma. W ustawieniach konektorów dodaj własny konektor zdalny, wklej adres serwera i wybierz OAuth: Perplexity zarejestruje się dynamicznie i otworzy ekran zgody Fakturnii. Jeśli zamiast OAuth wybierzesz klucz API, podaj klucz `fmcp_…`, który serwer przyjmuje w nagłówku `Authorization: Bearer`. ### GitHub Copilot (VS Code, JetBrains, Eclipse, Xcode) Tryb agenta GitHub Copilot łączy się ze zdalnymi serwerami MCP i loguje przez OAuth z rejestracją dynamiczną. W VS Code (wersja 1.101 lub nowsza) dodaj do projektu plik `.vscode/mcp.json`: ```json { "servers": { "fakturnia": { "type": "http", "url": "https://prod.api.fakturnia.pl/mcp" } } } ``` Następnie uruchom serwer („Start" nad jego wpisem w pliku). VS Code otworzy ekran zgody Fakturnii w przeglądarce, a narzędzia pojawią się w czacie Copilota w trybie agenta. W JetBrains, Eclipse i Xcode dodaj ten sam adres w konfiguracji MCP wtyczki GitHub Copilot; logowanie przebiega tak samo, przez rejestrację dynamiczną. ### Cursor, Windsurf i inne klienty MCP Serwer typu HTTP z adresem powyżej. Klient z obsługą OAuth loguje się w przeglądarce; klient bez OAuth używa klucza dostępu: ```json { "mcpServers": { "fakturnia": { "url": "https://prod.api.fakturnia.pl/mcp", "headers": { "Authorization": "Bearer fmcp_..." } } } } ``` ### Własny agent (Claude API, OpenAI Responses API) Serwer działa jako zdalny serwer MCP w Messages API (`mcp_servers` z `url` i `authorization_token` równym kluczowi `fmcp_…` albo access tokenowi OAuth). Ten sam wzorzec działa w OpenAI Responses API (`tools: [{ type: "mcp", server_url, authorization }]`). ## Uwierzytelnianie ### OAuth 2.1 (zalecane) Przepływ authorization code z PKCE S256, zgodny z sekcją autoryzacji specyfikacji MCP: 1. Klient odczytuje metadane zasobu, potem metadane serwera autoryzacji (`code_challenge_methods_supported: ["S256"]`, `client_id_metadata_document_supported: true`, `authorization_response_iss_parameter_supported: true`). 2. Klient identyfikuje się jednym z trzech sposobów: **rejestracja dynamiczna** (`POST /mcp/oauth/register`, klienci publiczni, `redirect_uris` tylko `https://` albo `http://localhost`), **dokument client_id** (`client_id` jest adresem HTTPS dokumentu JSON z `redirect_uris`; serwer pobiera go z ochroną przed SSRF i buforuje) albo **klient prekonfigurowany** (wydawany przez Fakturnię dla konektorów, które wymagają stałego `client_id` i sekretu). 3. `GET /mcp/oauth/authorize` z `response_type=code`, `code_challenge`, `code_challenge_method=S256`, `state`, `resource=https://prod.api.fakturnia.pl/mcp` (RFC 8707; brak `resource` oznacza zasób `/mcp`). Serwer przekierowuje do aplikacji Fakturnia na ekran zgody. Nieznany `client_id` albo `redirect_uri` spoza rejestracji kończy się stroną błędu, nigdy przekierowaniem. 4. Zalogowany użytkownik (właściciel albo pracownik) widzi nazwę klienta, host powrotu, zakres dostępu i plan, po czym zezwala albo odmawia. Odpowiedź autoryzacji niesie `code`, `state` i `iss`. 5. `POST /mcp/oauth/token` (`grant_type=authorization_code`, `code_verifier`, `redirect_uri`, `resource`) zwraca `access_token` (JWT, 1 godzina, audience równe adresowi serwera) i `refresh_token` (30 dni, **rotowany** przy każdym odświeżeniu; ponowne użycie zużytego tokenu unieważnia całe połączenie). Stan konta jest sprawdzany przy każdym żądaniu, nie z tokenu: rozłączenie w aplikacji, dezaktywacja pracownika, zmiana uprawnień albo wygaśnięcie planu działają natychmiast. ### Klucz dostępu `fmcp_…` Dla klientów bez OAuth. Użytkownik tworzy klucz w aplikacji (Integracje, Asystent AI, Klucze dostępu): etykieta, ważność do 90 dni, sekret pokazany raz. Klucz ma dokładnie uprawnienia osoby, która go utworzyła, i jest przekazywany jako `Authorization: Bearer fmcp_…`. Odwołanie działa natychmiast; właściciel konta widzi i odwołuje klucze pracowników. ## Uprawnienia i plan - **Właściciel konta** widzi wszystkie narzędzia poza konfiguracją integracji (webhooki, konta pracownicze) i poza tym, co nigdy nie trafia do MCP: konfiguracja KSeF (token KSeF w argumentach), pobieranie PDF (binaria), dane skrzynek pocztowych poza odczytem. - **Pracownik** widzi tylko narzędzia odpowiadające jego uprawnieniom z panelu (np. „Podgląd dokumentów" daje `browseDocuments` i `getDocument`, „Wystawianie i edycja dokumentów" dodaje `prepareDocument`, `issueDocument`, `patchDocument`). Narzędzie niewidoczne na liście jest niedostępne także przy wywołaniu; odpowiedź nie rozróżnia „nie istnieje" od „brak uprawnień". - **Plan.** Asystent AI jest funkcją planu MAX: podłączenie konta (ekran zgody) i utworzenie klucza `fmcp_…` wymagają aktywnego planu MAX, inaczej odpowiedź to `403 SUBSCRIPTION_REQUIRED`. Gdy plan wygaśnie albo spadnie poniżej MAX, działają tylko `getAccountOverview`, `getDictionaries` i `lookupCompanyByNip`. Pozostałe narzędzia zostają na liście z prefiksem `[WYMAGA PLANU MAX …]` i zwracają `isError` z wyjaśnieniem, żeby model mógł powiedzieć, czego brakuje, zamiast udawać, że funkcji nie ma. ## Narzędzia Lista zależy od uprawnień i planu, pełny zestaw poniżej. Narzędzia oznaczone jako generowane odpowiadają 1:1 operacjom REST API (ten sam `operationId`, te same pola), więc ich schematy znajdziesz w [specyfikacji OpenAPI](/openapi.json). | Grupa | Narzędzia | Uwagi | |---|---|---| | Konto | `getAccountOverview` | sprzedawca, plan, numeracja, domyślny rachunek bankowy, stan KSeF bez sekretów; działa zawsze | | Rachunki bankowe | `listBankAccounts`, `addBankAccount`, `setDefaultBankAccount` | numer sprawdzany cyframi kontrolnymi (NRB/IBAN); domyślny rachunek trafia na faktury przelewowe; lista dla scope `settings:read`, dodawanie i zmiana domyślnego wyłącznie dla właściciela konta | | Wystawianie (prowadzone) | `prepareDocument`, `issueDocument` | zastępują `createDocument` i `validateDocument`; opis niżej | | Dokumenty | `browseDocuments`, `getDocument`, `getDocumentHistory`, `patchDocument`, `updateDocumentPaymentStatus`, `updateDocumentExternalReference`, `deleteDocumentExternalReference` (confirm), `deleteDocument` (confirm), `sendDocumentToKsef` (confirm), `getDocumentKsefStatus` | generowane | | Kontrahenci | `browseClients`, `getClient`, `createClient`, `updateClient`, `deleteClient` (confirm) | generowane | | Produkty | `browseProducts`, `getProduct`, `createProduct`, `updateProduct`, `deleteProduct` (confirm) | generowane | | Słowniki i GUS | `getDictionaries`, `lookupCompanyByNip` | widoczne dla każdego tokenu | | KSeF | `getKsefConnection`, `testKsefConnection` | konfiguracja połączenia tylko w aplikacji | | Ustawienia | `getSettings`, `updateGeneralSettings`, `updateNumberingSettings` | generowane | | Magazyn | `prepareWarehouseDocument` (WZ, PZ, PW, RW, MM; wystawia `issueDocument`), `browseWarehouses`, `getWarehouse`, `createWarehouse`, `updateWarehouse`, `deleteWarehouse` (confirm), `getStockReport`, `getWarehouseMovements`, `createStockAdjustment`, `createWzFromInvoice` | prowadzone i generowane | | Dokumenty cykliczne | `browseRecurringRules`, `getRecurringRule`, `getRecurringRuleHistory`, `toggleRecurringRule` | generowane | | Konta pocztowe | `browseEmailAccounts`, `getEmailAccount` | tylko odczyt, bez sekretów | | Aktywność | `getAccountActivity` | dziennik konta; tylko właściciel konta | ### Prowadzone wystawianie dokumentu Surowe `createDocument` nie jest wystawione jako narzędzie. Zamiast niego: 1. `prepareDocument` przyjmuje pełne ciało dokumentu (schemat identyczny z `POST /documents`: `documentType`, `issueDate`, `buyerId` albo `buyer`, `products`, płatność, adnotacje), uruchamia pełną walidację **bez zapisu** i zwraca: ```json { "previewId": "b7Jk…", "expiresAt": "2026-09-26T12:15:00.000Z", "preview": { "documentType": "FakturaVAT", "documentNumber": "FV/12/09/2026", "seller": { "name": "Firma Testowa", "nip": "5261040828" }, "buyer": { "name": "Beta Sp. z o.o.", "nip": "5252248481", "street": "Prosta 1", "postalCode": "00-001", "city": "Warszawa" }, "products": [{ "name": "Usługa doradcza", "quantity": 2, "unitOfMeasure": "szt.", "netPrice": 500, "vatRate": 23 }], "totals": { "netSum": 1000, "vatSum": 230, "grossSum": 1230, "currency": "PLN" }, "payment": { "method": "przelew", "statusId": 1, "paymentDeadlineDate": "2026-10-10" } }, "warnings": [ { "code": "NEW_CONTRACTOR", "message": "Nabywca „Beta Sp. z o.o." nie występował wcześniej w kontrahentach…" } ], "summaryText": "Faktura VAT nr FV/12/09/2026 (data wystawienia 2026-09-26)\nSprzedawca: …\nRazem: netto 1 000,00 zł, VAT 230,00 zł, brutto 1 230,00 zł\n…", "nextStep": "Pokaż użytkownikowi summaryText razem z ostrzeżeniami i zapytaj, czy wystawić dokument…" } ``` Ostrzeżenia: `POSSIBLE_DUPLICATE` (ten sam nabywca, kwota i typ w ostatnich 30 dniach), `NEW_CONTRACTOR`, `ISSUE_DATE_IN_FUTURE`, `ISSUE_DATE_OLD`, `PAYMENT_DEADLINE_BEFORE_ISSUE`, `SALE_DATE_AFTER_ISSUE`, `NO_ITEMS`, `ZERO_QUANTITY`, `ZERO_PRICE`, `FOREIGN_CURRENCY_NO_RATE`, `HIGH_AMOUNT`, `KSEF_BUYER_ADDRESS_MISSING`, `BANK_ACCOUNT_INVALID` (numer rachunku na dokumencie nie przechodzi cyfr kontrolnych). Podgląd zawiera też `payment.bankAccount`: rachunek sprzedawcy, który trafi na dokument (domyślny rachunek konta przy płatności przelewem). Błąd walidacji wraca jako `isError` z kodem i listą pól z External API (`422 VALIDATION_ERROR`, `PAYMENT_DEADLINE_REQUIRED` itd.). Brakujące pola uzupełniają ustawienia konta tak jak w aplikacji (uwagi, podpisy, miejsce wystawienia, automatyczne adnotacje, podstawa zwolnienia z VAT, płatność, stawka VAT): podgląd wymienia je w `preview.appliedDefaults` i w `summaryText`, żeby model powiedział o nich użytkownikowi. Szczegóły w [Tworzeniu dokumentów](/docs/creating-documents). Ostrzeżenia dotyczące adnotacji: `VAT_EXEMPTION_BASIS_MISSING` (stawka zw bez podstawy zwolnienia), `REVERSE_CHARGE_ANNOTATION_MISSING` i `REVERSE_CHARGE_WITHOUT_OO_RATE` (odwrotne obciążenie niespójne ze stawką oo), `WDT_BUYER_VAT_UE_MISSING` (0% WDT bez VAT UE nabywcy), `INVOICE_ALREADY_CORRECTED` (faktura ma już korektę). 2. Model pokazuje użytkownikowi `summaryText` i ostrzeżenia, użytkownik potwierdza. 3. `issueDocument({ previewId, confirm: true, sendToKsef?: false })` wystawia **dokładnie** zapisany podgląd (model nie ma jak zmienić danych między podglądem a wystawieniem), z kluczem idempotencji, i zwraca `document` (id, numer, kwota), `appUrl` (podgląd w aplikacji) oraz `ksef` (wynik wysyłki, gdy `sendToKsef=true`). Podgląd jest jednorazowy, ważny 15 minut i przypisany do osoby, która go przygotowała. ### Dokumenty magazynowe `prepareWarehouseDocument` działa tak samo dla WZ, PZ, PW, RW i MM (schemat `POST /warehouse/documents`): pozycje wskazują produkty z kartoteki (`productId` z `browseProducts`), podgląd pokazuje magazyn, stan przed i po każdej pozycji, braki towaru i wartości z ustawień, a `canIssue: false` oznacza, że brak towaru zablokuje wystawienie (model pyta użytkownika, co zrobić; `allowNegativeStock` tylko na jego wyraźne życzenie). Wystawienie to ten sam `issueDocument`. Pracownik z uprawnieniem do dokumentów magazynowych widzi `prepareWarehouseDocument` i `issueDocument`, ale nie `prepareDocument`. Zasady modułu: [Magazyn](/docs/warehouse). ### Zasady wspólne dla narzędzi - **`confirm=true`** jest wymagany przez narzędzia nieodwracalne (`delete*`, `sendDocumentToKsef`, `issueDocument`). Bez niego narzędzie nic nie zmienia i odpowiada instrukcją dla modelu. - **Argumenty spoza schematu** przy zapisie kończą się odmową z listą obsługiwanych pól (pole nie ginie po cichu); przy odczycie są logowane. - **Listy** mają domyślnie 25 wierszy i maksymalnie 100 (`itemsPerPage`, `currentPage`); większe wartości są przycinane z adnotacją w `meta.hint`. - **Rozmiar wyniku** jest ograniczony do 120 000 znaków; większy wynik wraca jako błąd z instrukcją zawężenia zapytania zamiast obciętego JSON-a. - **Koperta wyniku.** Treść narzędzia jest otoczona znacznikami `[DANE_NARZEDZIA_START]` i `[DANE_NARZEDZIA_END]`, a błędy mają prefiks `[HTTP ]`. Instrukcje serwera mówią modelowi, że wyniki narzędzi to dane, nie polecenia. - **Instrukcje serwera** (`instructions` w odpowiedzi `initialize` i w `server/discover`) zawierają nazwę firmy, kto jest połączony (właściciel czy pracownik), stan planu i kolejność kroków przy wystawianiu dokumentu. ## Limity i bezpieczeństwo - Limity: 120 wywołań na minutę na konto Fakturnii (właściciel i pracownicy razem), 240 na minutę na adres IP przed uwierzytelnieniem, 10 rejestracji klientów na minutę na IP, 30 wymian tokenu na minutę na IP. Odpowiedź `429` niesie `Retry-After`. Fakturnia może dodatkowo ustawić limit wywołań na 30 dni; wyczerpany limit wraca jako `isError` z wyjaśnieniem. - Wywołania asystenta są widoczne w logach REST API w aplikacji, a limity REST API Twoich własnych integracji pozostają od nich niezależne. - Sekrety nigdy nie trafiają do modelu: token KSeF, hasła skrzynek, sekrety kluczy API i webhooków są poza zasięgiem narzędzi. - Każde wykonanie narzędzia jest zapisywane (kto, którym poświadczeniem, jakie narzędzie, wynik) i widoczne w aplikacji w sekcji „Ostatnia aktywność". - Rozłączenie aplikacji, odwołanie klucza, dezaktywacja pracownika i wygaśnięcie planu działają od następnego żądania. ## Przykład: jedno wywołanie JSON-RPC ```http POST /mcp HTTP/1.1 Host: sandbox.api.fakturnia.pl Authorization: Bearer fmcp_... Content-Type: application/json Accept: application/json, text/event-stream MCP-Protocol-Version: 2026-07-28 Mcp-Method: tools/call Mcp-Name: browseDocuments { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "browseDocuments", "arguments": { "dateFrom": "2026-09-01", "dateTo": "2026-09-30", "documentType": "FakturaVAT" }, "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientInfo": { "name": "my-agent", "version": "1.0.0" }, "io.modelcontextprotocol/clientCapabilities": {} } } } ``` Odpowiedź to `result.content[0].text` z kopertą danych i `result.isError`. ## Przykładowe polecenia dla asystenta - „Wystaw fakturę VAT dla Nowak Sp. z o.o. za usługę doradczą: 2 godziny po 250 zł netto, termin płatności 14 dni." - „Pokaż faktury z tego miesiąca, które nie są jeszcze opłacone, i podsumuj kwotę." - „Znajdź w GUS firmę o NIP 5252248481 i dodaj ją do kontrahentów." - „Oznacz fakturę FV/12/09/2026 jako zapłaconą dzisiaj przelewem." - „Ile sprzedałem w trzecim kwartale w podziale na kontrahentów?" - „Przyjmij na magazyn 20 krzeseł biurowych od Fabryki Mebli po 120 zł netto." - „Wystaw korektę do FV/12/09/2026: klient zwrócił jeden laptop." Asystent zaczyna od `getAccountOverview`, dobiera nabywcę z kontrahentów albo z GUS, przygotowuje podgląd, pokazuje go i dopiero po potwierdzeniu wystawia dokument. Do KSeF wysyła wyłącznie na wyraźne życzenie. KSeF już obowiązuje, więc faktura wystawiona przez asystenta trafia do systemu tak samo jak faktura wystawiona ręcznie: po Twojej decyzji. ## Zobacz też - [Dla agentów i botów](/docs/agents): maszynowo czytelne zasoby dokumentacji. - [Uwierzytelnianie](/docs/authentication) i [Tworzenie dokumentów](/docs/creating-documents): reguły, które narzędzia MCP dziedziczą z REST API. - [Wysyłka do KSeF](/docs/ksef): asynchroniczny przebieg, statusy i UPO. --- # Dziennik zmian API jest wersjonowane w ścieżce (`/external/v1`). Zmiany łamiące zgodność są wprowadzane w nowej wersji głównej; zmiany addytywne (nowe pola, nowe punkty końcowe, nowe parametry opcjonalne) są udostępniane w ramach `v1`. Nieznane pola odpowiedzi należy traktować jako zgodne w przód: pomijaj te, których nie rozpoznajesz. ## v1 (bieżąca) **2026-10-09: faktury w procedurze marży** - **Pozycje w marży.** Stawka `vatRate: "marza"` oznacza pozycję w procedurze marży (art. 119 i 120 ustawy o VAT): faktura pokazuje tylko kwoty brutto, w KSeF pozycja nie ma `P_12`, a suma trafia do `P_13_11`. Dokument musi mieć `annotations.marginProcedure` (inaczej `422 MARGIN_LINE_WITHOUT_PROCEDURE`). Pozycje w marży i ze zwykłymi stawkami mogą być na jednej fakturze. - **Kwota nabycia i VAT od marży.** Nowe pola pozycji `marginPurchaseValue` (kwota nabycia brutto w PLN) i `marginTaxRate` (`23`, `8`, `5`, `0`) w `POST`, `PUT` i `PATCH /documents` oraz w odpowiedziach `GET /documents/{id}`. Nie trafiają na fakturę ani do KSeF; z nich powstają marża i VAT od marży w rejestrze VAT i JPK_V7. Nowe kody: `422 MARGIN_TAX_RATE_INVALID`, `422 MARGIN_PURCHASE_INVALID`. Przewodnik: [Procedura marży](/docs/creating-documents#procedura-marzy). - **Kwota nabycia po wystawieniu.** `PATCH /documents/{id}/margin-purchases` (scope `documents:update`, narzędzie MCP `updateDocumentMarginPurchases`) ustawia kwotę nabycia i stawkę VAT od marży pozycji w marży także po wysyłce do KSeF. Kody `422 MARGIN_LINE_NOT_FOUND` i `422 MARGIN_PURCHASES_INVALID`. `PUT` i `PATCH /documents/{id}` bez pól marży przy pozycji zostawiają zapisane wartości. - **Sprzedaż w marży w podglądzie.** `POST /documents/validate` zwraca `preview.totals.marginSaleSum` (sprzedaż w marży zawarta w `netSum`, w KSeF `P_13_11`), a narzędzie MCP `prepareDocument` podaje w podsumowaniu netto zwykłych stawek osobno od sprzedaży w marży. Zapisany `netSum` nie zmienia znaczenia. - **Korekta faktury marżowej pobranej z KSeF.** Faktura sprzedaży w procedurze marży wystawiona w innym programie (pozycje bez `P_12`, suma w `P_13_11`) ma po odczycie pozycje ze stawką `marza` i można do niej wystawić korektę. Kod `422 CORRECTED_INVOICE_MARGIN_LINES` zostaje tylko dla pozycji bez stawki na fakturze bez adnotacji procedury marży. **2026-10-09: korekta faktury sprzedaży pobranej z KSeF** - **Korekta faktury wystawionej poza Fakturnią.** `POST /documents` przyjmuje w `correctedInvoiceId` fakturę sprzedaży pobraną z KSeF (`source: "ksef"`, `direction: "income"`, typ `FakturaVAT`). API czyta jej pozycje, sumy i adres nabywcy z XML faktury (pobierając go z KSeF, gdy trzeba), liczy korektę jako różnicę i w KSeF wskazuje numer KSeF faktury pierwotnej. Nowe kody: `422 CORRECTED_INVOICE_NOT_INCOME`, `422 CORRECTED_INVOICE_WITHOUT_POSITIONS`; `CORRECTED_INVOICE_TYPE_MISMATCH` obejmuje też importy innych typów. Przewodnik: [Korekty](/docs/correcting-documents). - **Pozycje faktur pobranych z KSeF.** `GET /documents/{id}` faktury pobranej z KSeF zwraca `products`, `netSum` i `vatSum`, gdy jej XML został już odczytany (po otwarciu w aplikacji albo po wystawieniu korekty); wcześniej takie faktury miały tylko `grossSum`. **2026-10-09: identyfikatory kontrahenta zgodne z KSeF** - **Jedne zasady identyfikatorów dla kontrahentów i stron dokumentu.** `POST` i `PUT /clients` oraz nabywca i odbiorca podani danymi w `POST /documents`, `POST /documents/validate`, `PUT` i `PATCH /documents/{id}` zapisują identyfikatory w postaci, którą przyjmie KSeF: NIP jako 10 cyfr, numer VAT UE z prefiksem kraju w `vatUE` (Grecja `EL`), `other` z krajem. Przewodnik: [Identyfikatory kontrahenta](/docs/clients#identyfikatory-kontrahenta). - **Numer VAT UE wpisany w `nip` trafia do `vatUE`.** Bez `identifierType` numer `DE123456789` w `nip` jest zapisywany jako `vatEu` w `vatUE` z `countryCode` z prefiksu, a nie jako NIP. - **Ostrzeżenia `data.warnings`.** Odpowiedzi kontrahentów i dokumentów (także `validate`) mówią, jak odczytano identyfikatory: ustalony typ, przeniesiony numer, ustalony kraj (`code`, `field`, `message`). Narzędzie MCP `prepareDocument` zwraca je w `identifierNotes`. - **Sprzeczne dane są odrzucane.** Kontrahenci: `422 VALIDATION_ERROR`, dokumenty: nowy kod `422 PARTY_IDENTIFIER_INVALID`, zawsze z nazwą pola. Dotyczy to też NIP-u z błędną cyfrą kontrolną podanego bez `identifierType` (wcześniej przyjmowanego) oraz identyfikatora `idWew` w innej postaci niż `NIP-NNNNN`. - **Dane zapisane wcześniej pozostają edytowalne.** Identyfikatory równe zapisanym nie są ponownie sprawdzane przy `PUT /clients` ani przy `PUT` i `PATCH` dokumentu. **2026-10-09: daty FA(3), raty, kilka wpłat zaliczki, dzień wpłaty** - **Okres sprzedaży: `subDateTo`.** Z `subDate` jako początkiem daje `OkresFa` w KSeF (np. usługa za cały miesiąc). `PATCH` zachowuje zapisany okres i nagłówek daty. - **Kurs waluty z dniem i tabelą NBP: `exchangeRateDate`, `exchangeRateTable`.** Kurs faktury to kurs z dnia roboczego przed obowiązkiem podatkowym, a gdy faktura jest wcześniejsza, przed datą wystawienia (art. 31a ustawy o VAT). Korekta bez `exchangeRate` przejmuje kurs, dzień i tabelę faktury korygowanej (art. 31b ust. 1). - **Obowiązek podatkowy z datą wystawienia: `taxPointRule`.** Dla usług budowlanych, książek i prasy, mediów, telekomunikacji, najmu, leasingu, ochrony i stałej obsługi (art. 19a ust. 5 pkt 3 i 4). Decyduje o okresie w raportach VAT i o dniu kursu. Gdy konto ma włączone ustawienie z domyślną regułą, dokument bez `taxPointRule` dostaje ją z ustawień (`preview.appliedDefaults`: `TAX_POINT_RULE`); `useAccountDefaults: false` albo jawne `null` ją pomija. - **Raty i termin opisowy: `paymentInstallments`, `paymentDeadlineTerm`.** Kilka terminów z kwotami albo termin w rodzaju „14 dni od doręczenia faktury”; w KSeF `TerminPlatnosci` i `TerminOpis`. Błędne raty: `422 PAYMENT_INSTALLMENTS_INVALID`. Przewodnik: [Tworzenie dokumentów](/docs/creating-documents#raty-paymentinstallments). - **Równe raty liczone przez API: `paymentInstallmentsSplit`.** `POST`, `PUT` i `PATCH /documents` oraz `POST /documents/validate` przyjmują `{ count, firstDate?, every? }`; API dzieli kwotę do zapłaty co do grosza (reszta w ostatniej racie) i zapisuje zwykłe `paymentInstallments`. Podgląd `validate` pokazuje raty w `preview.paymentInstallments`, a narzędzia MCP `prepareDocument` i `patchDocument` przyjmują to samo pole. Przewodnik: [Tworzenie dokumentów](/docs/creating-documents#rowne-raty-paymentinstallmentssplit). - **Kilka wpłat na fakturze zaliczkowej: `advanceReceipts`.** Każda wpłata z datą, kwotą i kursem; w KSeF `ZaliczkaCzesciowa`. Błędne wpłaty: `422 ADVANCE_RECEIPTS_INVALID`. - **Daty korekty: `correctionEffectDate`, `correctionConfirmationDate`.** Dzień, od którego korekta działa (zwiększenie po sprzedaży z przyczyną w innym miesiącu daje `TypKorekty` 3), i dzień potwierdzenia przez nabywcę korekty wystawionej poza KSeF. Korekta bez `subDate` przejmuje datę albo okres sprzedaży faktury korygowanej. Przewodnik: [Korygowanie dokumentów](/docs/correcting-documents#powod-korekty-i-jej-skutek-w-ksef). - **Dzień wpłaty przy oznaczaniu zapłaty.** `PATCH /documents/{id}/payment-status` przyjmuje `paymentDate` (domyślnie dzisiejszy dzień w Polsce, nie z przyszłości); `GET` zwraca go w `payment.receivedPaymentDate`. Przy metodzie kasowej to dzień obowiązku podatkowego. - **Daty sprawdzane jak w aplikacji: `422 DOCUMENT_DATES_INVALID`.** Okres sprzedaży z początkiem po końcu, data sprzedaży ponad 60 dni po wystawieniu, data zapłaty późniejsza niż wystawienie, korekta wystawiona przed fakturą korygowaną. `PUT` i `PATCH` sprawdzają też format dat. - **KSeF: więcej dat w XML.** Miejsce wystawienia (`P_1M`), raty i termin opisowy, wpłaty zaliczki. Data z godziną to dzień w Polsce, a nieczytelna data jest błędem walidacji zamiast dzisiejszej. Wszystkie nowe pola są opcjonalne: integracje, które ich nie wysyłają, działają jak dotąd. **2026-10-09: korekta korekty** - **Jedna korekta na dokument.** Nowa korekta dokumentu, który ma już korektę, dostaje `409 CORRECTED_INVOICE_HAS_CORRECTION` z numerem i id ostatniej korekty łańcucha w `errors[0]`. Kolejną zmianę wystawiasz jako korektę tej korekty, więc stan „przed” uwzględnia wcześniejsze zmiany i kwoty nie liczą ich dwa razy. Tak samo odpowiadają `PUT` i `PATCH`, gdy przepinasz korektę na dokument z inną korektą. Przewodnik: [Korygowanie dokumentów](/docs/correcting-documents#kolejna-korekta-tej-samej-faktury). - **Korekta korekty wskazuje w KSeF fakturę pierwotną.** `DaneFaKorygowanej` podaje numer, datę i numer KSeF faktury pierwotnej, a nie poprzedniej korekty (Podręcznik KSeF 2.0, część II, punkt 2.13.3). Wydruk pokazuje fakturę pierwotną i poprzednią korektę. - **Faktura pierwotna bez numeru KSeF.** `POST /documents/validate` zwraca w `preview.warnings` uwagi, które nie blokują dokumentu. Na koncie połączonym z KSeF korekta, której faktura pierwotna nie ma numeru KSeF, dostaje kod `CORRECTION_ORIGINAL_KSEF_PENDING`, `CORRECTION_ORIGINAL_NOT_SENT_TO_KSEF` albo `CORRECTION_ORIGINAL_OUTSIDE_KSEF`. Wysyłka do KSeF działa jak dotąd. Przewodnik: [Korygowanie dokumentów](/docs/correcting-documents#faktura-pierwotna-bez-numeru-ksef). - **Korekta korekty zaliczkowej.** `FakturaKorygujacaZaliczkowa` może korygować poprzednią korektę zaliczkową: zamówienie i zaliczka przed korektą to stan po niej. **2026-10-09: powód i typ korekty, limit przyczyny korekty** - **Powód korekty (`correctionCause`) i typ korekty w KSeF (`correctionType`).** Korekta może podać, dlaczego ją wystawiasz: `after_sale` (zdarzenie po sprzedaży: rabat, zwrot, reklamacja) albo `mistake` (błąd na fakturze korygowanej). Z powodu i kwot Fakturnia wylicza `TypKorekty` wysyłany do KSeF, dotąd zawsze `2`; `correctionType` (`1`, `2`, `3`) ustawia go ręcznie. Oba pola przyjmują `POST`, `PUT` i `PATCH /documents`, a zwracają `GET /documents/{id}` i webhooki dokumentów. Powód decyduje też o okresie korekty w raportach aplikacji (rejestr VAT, JPK_V7, KPiR). Integracje, które nie wysyłają nowych pól, działają jak dotąd. Przewodnik: [Korygowanie dokumentów](/docs/correcting-documents#powod-korekty-i-jej-skutek-w-ksef). - **Przyczyna korekty najwyżej 256 znaków.** Tyle przyjmuje KSeF w `PrzyczynaKorekty`. Dłuższy `correctionReason` dostaje teraz błąd walidacji od razu (`400` przy `POST`, `422` przy `PUT` i `PATCH`), a nie odrzucenie przez KSeF przy wysyłce. **2026-10-02: połączenie z KSeF tokenem** - **Token KSeF jako metoda połączenia.** `POST /ksef/connection` przyjmuje `connectionType: TOKEN` z `token` wygenerowanym w Aplikacji Podatnika KSeF albo, jak dotąd, certyfikat (`connectionType: CERTIFICATE`). Zapis jednej metody zastępuje drugą. `GET /ksef/connection` podaje `connectionType`, `hasToken` i `hasCertificate`. **2026-10-01: webhooki dla dokumentów z aplikacji i z KSeF, limity zapytań, unikalne numery** - **`document.created` także dla dokumentów z aplikacji.** Dokument zapisany w aplikacji webowej albo mobilnej (także szkic i dokument magazynowy) wyzwala `document.created`, tak jak dokument z API, MCP albo reguły cyklicznej. Korekta wystawiona w aplikacji wyzwala też `document.updated` dla faktury korygowanej. Nie trzeba już okresowo czytać `GET /documents`, żeby zobaczyć dokumenty wystawione w aplikacji. Przewodnik: [Webhooki](/docs/webhooks). - **Wystawienie szkicu w aplikacji wyzwala `document.updated`.** To samo zdarzenie przychodzi po zamianie dokumentu z powrotem w szkic i po zmianie w aplikacji oznaczeń `isSent`, `isPrinted` i `isDownloaded` (także przy pierwszym wydruku albo pobraniu), zawsze tylko przy rzeczywistej zmianie. Integracja od razu wie, kiedy szkic stał się wystawionym dokumentem (`isDraft: false`). - **Faktury pobrane z KSeF: `document.ksefImported`.** Synchronizacja z KSeF zgłasza nowe faktury (zakupowe i sprzedażowe wystawione poza Fakturnią) jednym zdarzeniem z listą `data.documentIds`, do 100 identyfikatorów w dostawie; większy import przychodzi w kilku dostawach. Takie faktury nie wysyłają `document.created`. - **`source` i `direction` w dokumentach.** `GET /documents` i `GET /documents/{id}` zwracają `source` (`local` albo `ksef`) i `direction` (`income`, `expense` albo `order`), więc faktury zakupowej z KSeF nie pomylisz z własną sprzedażą. - **Więcej zdarzeń z aplikacji.** `document.updated` po potwierdzeniu albo cofnięciu wydania magazynowego, po usunięciu ostatniej korekty (dla faktury korygowanej), po wystawieniu WZ do faktury (dla faktury), po wysyłce e-mailem przez regułę cykliczną i gdy synchronizacja z KSeF dopisze numer KSeF; `client.created` dla kontrahenta zapisanego z danych nabywcy dokumentu z API albo MCP; `client.updated` po zapisaniu adresów e-mail kontrahenta. - **Limity zapytań liczone dla aplikacji.** Liczniki z tabeli w [Limitach zapytań](/docs/rate-limits) należą teraz do aplikacji (konta API), którą identyfikuje token, a nie do adresu IP, więc kilka integracji za jednym NAT-em nie dzieli już limitu. Doszedł ogólny limit 240 zapytań na minutę z jednego adresu IP, sprawdzany przed uwierzytelnieniem; zapytania z brakującym, błędnym albo wygasłym tokenem liczą się tylko do niego. Kod `429 RATE_LIMIT_EXCEEDED` i nagłówki bez zmian. - **Numer dokumentu unikalny na całym koncie.** `POST /documents` oraz `PUT` i `PATCH /documents/{id}` zwracają `409 DOCUMENT_NUMBER_EXISTS` także wtedy, gdy ten sam pełny numer ma już dokument innego typu (np. proforma i proforma bez VAT z tym samym symbolem) albo dokument przeniesiony na inny miesiąc. `PUT` i `PATCH` sprawdzają to, gdy zmieniasz numer, a komunikat w `errors[0]` podaje dokument, który ma ten numer. - **Schemat dokumentu w specyfikacji OpenAPI** opisuje wszystkie pola, które zwraca API: `isDraft`, `invoiceName`, `externalSource`, `externalOrderId`, `externalOrderNumber`, `correctionAdvanceAmountAfter`, `correctionSettlementAmountAfter`, `ksefNumber`, `ksefReferenceNumber`, `ksefStatus` i `ksefSent`. Odpowiedzi API się nie zmieniły, te pola były w nich już wcześniej. **2026-09-27 (wersja specyfikacji 1.3.0): dokumenty magazynowe i ustawienia konta** - **Dokumenty magazynowe przez API.** `POST /warehouse/documents` wystawia WZ, PZ, PW, RW i MM z pozycjami z kartoteki i księguje ruchy; `POST /warehouse/documents/validate` zwraca podgląd ze stanem przed i po. Nowy scope `warehouse:documents:create`. Przewodnik: [Magazyn](/docs/warehouse). - **Braki towaru blokują wydanie.** WZ, RW i MM (także `wz-from-invoice`), które zeszłyby ze stanem poniżej zera, dostają `422 STOCK_SHORTAGE`, jak w aplikacji; `allowNegativeStock: true` wystawia mimo braku. - **WZ do faktury** dostaje symbol WZ i ustawienia konta, sprawdza magazyn (`422 WAREHOUSE_INVALID`) i zapisuje numer WZ przy fakturze (wydruk i KSeF). - **Wartości domyślne z ustawień konta** przy tworzeniu dokumentów: uwagi, podpisy, miejsce wystawienia, automatyczne adnotacje (MPP z rachunkiem VAT, marża), podstawa zwolnienia dla stawki zw, forma i status płatności, stawka VAT pozycji, wyświetlanie PKWiU. Nowe pole `useAccountDefaults`, podgląd `preview.appliedDefaults` i `preview.resolved`. Przewodnik: [Tworzenie dokumentów](/docs/creating-documents). - **Korekty** przejmują nabywcę, odbiorcę, walutę i adnotacje z korygowanej faktury; faktury z korektą, rozliczonej zaliczki i zamówienia z zaliczkami nie da się zmienić ani usunąć (`409 DOCUMENT_CORRECTED`, `409 ADVANCE_INVOICE_SETTLED`, `400 ORDER_HAS_ADVANCE_INVOICES`). - **Faktura zaliczkowa bez pozycji** (pozycje to rozkład zaliczki) i bez daty sprzedaży; **proforma bez VAT** ma zawsze VAT 0. - **KSeF: procedura marży** trafia do XML (`P_PMarzy` ze znacznikiem procedury), nieznany numer procedury jest błędem walidacji. Zwolnienie z VAT włączone bez typu albo podstawy prawnej jest odrzucane. - **Produkty: pola magazynowe** `productType`, `sku`, `ean`, `purchasePrice` w odczycie, tworzeniu i aktualizacji (aktualizacja tylko przesłanych); `quantity` przy tworzeniu opcjonalne. - **Dokumenty magazynowe przez `/documents`:** usunięcie cofa ruchy, zmiana daty je przesuwa, zmiana pozycji zwraca `409 WAREHOUSE_LINES_LOCKED`. Korekta stanu z `quantity: 0` i `unitCost` to przecena (`WYCENA`). **2026-09-26: serwer MCP** - **Serwer MCP dla asystentów AI.** `https://prod.api.fakturnia.pl/mcp` (sandbox: `https://sandbox.api.fakturnia.pl/mcp`): ChatGPT, Claude, Claude Code i własni agenci pracują na koncie użytkownika po logowaniu OAuth 2.1 (odkrywanie RFC 9728/8414, PKCE, rejestracja dynamiczna, dokumenty client_id, rotacja refresh tokenów) albo z kluczem `fmcp_…`. Narzędzia generowane z tej specyfikacji plus prowadzone wystawianie dokumentu (`prepareDocument`, `issueDocument`), `confirm=true` na operacjach nieodwracalnych, uprawnienia pracownika, bramka planu. Przewodnik: [Serwer MCP](/docs/mcp). **2026-09 (wersja specyfikacji 1.2.0)** - **REST API jest modułem planu MAX.** Konta API, wszystkie endpointy `/external/v1` i webhooki wymagają aktywnego planu MAX. Endpoint tokenu odpowiada `403 access_denied` z `error_code: SUBSCRIPTION_REQUIRED`, wywołania z tokenem `403 SUBSCRIPTION_REQUIRED`. Opis w [Uwierzytelnianiu](/docs/authentication). - **Stałe kody błędów w całej kopercie.** `401` zwraca `UNAUTHORIZED` albo `TOKEN_EXPIRED`, `403` bez scope `NO_PERMISSIONS`, `429` `RATE_LIMIT_EXCEEDED`; zdanie wyjaśniające trafia do `errors[0]`. Błędy walidacji treści mają kod `VALIDATION_ERROR`, zwykle ze statusem `422`; tworzenie dokumentów (także magazynowych) zwraca `400` przy błędach kształtu żądania, więc traktuj oba statusy jako błąd walidacji. Nowa tabela kodów w [Błędach](/docs/errors). - **Unieważnienie konta API i lista adresów IP działają natychmiast**, przy każdym wywołaniu, nie dopiero po wygaśnięciu tokenu (`401 UNAUTHORIZED`, `403 FORBIDDEN`). - **Webhooki: pełne zarządzanie.** `POST /webhooks` z wyborem zdarzeń, `GET`/`PATCH`/`DELETE /webhooks/{id}`, rotacja sekretu, historia dostaw (`/deliveries`) i ping testowy (`/test`). Nowe scope'y `webhooks:update` i `webhooks:delete`. Przewodnik: [Webhooki](/docs/webhooks). - **Webhooki: nowa treść dostawy.** Każda dostawa ma `id` (także w nagłówku `X-Webhook-Delivery`) i blok `data` z identyfikatorem nazwanym po typie zasobu (`documentId`, `clientId`, ...). Pola `resourceId`/`resourceIds` zostały dla zgodności. Schemat `WebhookEventPayload` w specyfikacji. - **Nowe zdarzenia** `document.ksefRejected` (KSeF odrzucił dokument albo wysyłka zakończyła się błędem) i `document.paymentStatusChanged` (oznaczono albo cofnięto zapłatę). `document.sentToKSeF` przychodzi też dla wysyłek z aplikacji i z kolejki, a `document.updated` po powiązaniu z zamówieniem. - **Konta pocztowe (odczyt).** `GET /email-accounts` i `GET /email-accounts/{id}` ze scope'ami `email-accounts:browse` i `email-accounts:read`; bez sekretów. Przewodnik: [Konta pocztowe](/docs/email-accounts). - **Kompletna specyfikacja OpenAPI.** Każda operacja ma opis, schemat odpowiedzi z kształtem `data`, schemat i przykład treści żądania oraz udokumentowane `401`/`403`/`429`/`500`. Nowe nagłówki `X-Request-ID`, `X-RateLimit-*`, `Retry-After`, `Idempotent-Replayed`. Kolekcja Postman bierze przykłady żądań ze specyfikacji. - **Historia przez API.** `GET /documents/{id}/history` (dziennik dokumentu), `GET /activity` (aktywność całego konta, nowy scope `activity:read`), `GET /employees/{id}/activity` (aktywność pracownika) i `GET /warehouse/movements` (księga ruchów magazynowych). Przewodnik: [Historia i aktywność](/docs/activity). - **Nowe odczyty po id**: `GET /warehouse/{id}`, `GET /employees/{id}`; raport stanów przyjmuje `productId`. - **Paginacja list, które jej nie miały**: `GET /recurring-rules`, `GET /recurring-rules/{id}/history` i `GET /webhooks/{id}/deliveries` zwracają `currentPage`, `itemsPerPage`, `totalCount`, `totalPages` (`limit` nadal działa jako alias `itemsPerPage`). - **Listy do 100 pozycji na stronę** (`itemsPerPage`, wcześniej 20) w `GET /documents`, `/clients`, `/products`. - **Usuwanie zwraca `data.id`** także dla dokumentów, kontrahentów i produktów; `DELETE /documents/{id}/external-reference` zwraca wyczyszczone pola powiązania. **2026-07** - **Dokumenty cykliczne w API.** Nowe endpointy `/recurring-rules`: lista reguł z podsumowaniem (statusy, najbliższa generacja, miesięczny przychód brutto), szczegóły reguły, historia generacji oraz pauza i wznowienie. Nowe scope'y `recurring:*` (3). Wymaga planu MAX. Przewodnik: [Dokumenty cykliczne](/docs/recurring-rules). - **Moduł magazynowy w API.** Nowe endpointy `/warehouse`: magazyny (CRUD), korekty stanów (inwentaryzacja), raport stanów z parametrem `asOf` i automatyczne WZ do faktur sprzedaży. Wymaga planu MAX. Przewodnik: [Magazyn](/docs/warehouse). - **Pracownicy i uprawnienia w API.** Nowe endpointy `/employees`: rejestr uprawnień, tworzenie i edycja kont pracowniczych, dezaktywacja i usuwanie. Przewodnik: [Pracownicy i uprawnienia](/docs/employees). - **Nowe scope'y**: `warehouse:*` (7) i `employees:*` (4), opisane w [Uwierzytelnianiu](/docs/authentication). - **Filtr `documentType`** na liście dokumentów przyjmuje teraz także typy magazynowe (np. `WZ`). - **Filtry listy dokumentów.** `GET /documents` przyjmuje `documentType` (pojedynczy typ lub listę rozdzieloną przecinkami) oraz zakres dat wystawienia `dateFrom`/`dateTo`. Szczegóły w [Paginacji i filtrowaniu](/docs/pagination). - **Kolekcja Postman i CLI** obejmują całe API (produkty, ustawienia, webhooki, połączenie KSeF, `auth/me`, wyszukiwanie firmy po NIP, powiązania z zamówieniami). - **Nowe przewodniki.** [Kontrahenci](/docs/clients), [Produkty](/docs/products), [Ustawienia](/docs/settings), [Połączenie KSeF](/docs/ksef-connection), [Powiązania z zamówieniami](/docs/external-references) i [Dane firmy po NIP](/docs/company-lookup). **2026-06** - **Wersja beta dostępna w środowisku sandbox.** API można testować pod adresem `sandbox.app.fakturnia.pl`. - **Kwoty ujednolicone do dokładności co do grosza, zaokrąglanie w górę przy połowie.** Sumy w poszczególnych wierszach oraz sumy dla całego dokumentu są teraz dokładnie zgodne z silnikiem `fakturnia-shared`. W każdym wierszu zachodzi `netValue + amountOfVat === grossValue`. - **`FakturaRozliczeniowa`** wyznacza teraz `grossSum` na podstawie `settlementAmount` (pozostało do zapłaty = wiersze − powiązane zaliczki) zamiast pełnej sumy wierszy; metadane powiązanych zaliczek są wypełniane automatycznie. - Sumy **`FakturaZaliczka`** są wyznaczane na podstawie proporcjonalnego rozbicia zaliczki (`grossSum === advancePaymentAmount`). - **`FakturaProforma` / `FakturaProformaBezVAT`** akceptowane przez `POST /documents`. - **Korekty zaliczkowe i rozliczeniowe** (`FakturaKorygujacaZaliczkowa`, `FakturaKorygujacaRozliczeniowa`) obliczają różnicę kwoty z proporcjonalnym VAT. - `PUT` oraz `PATCH /documents/{id}`; wiele `bankAccounts[]` przy tworzeniu i aktualizacji. ## Polityka wersjonowania - **Wersjonowanie w ścieżce.** `v1` jest stabilne. Wersja `v2` pojawiłaby się wyłącznie w przypadku zmian łamiących zgodność. - **Addytywnie w ramach v1.** Nowe opcjonalne pola żądania oraz nowe pola odpowiedzi mogą pojawiać się bez zmiany wersji. - **Wycofania** są ogłaszane na tej stronie wraz z oknem migracyjnym przed usunięciem. Śledź zmiany, obserwując tę stronę lub [specyfikację OpenAPI](/openapi.json) (jej `info.version` odzwierciedla wydania). --- # Dokumentacja API Base URL: https://prod.api.fakturnia.pl/external/v1 ## auth - `GET /auth/me` — Pobierz informacje o aktualnym koncie ## documents - `GET /documents/` — Pobierz listę dokumentów (scope: documents:browse) - `POST /documents/` — Utwórz nowy dokument (scope: documents:create) - `GET /documents/{id}` — Pobierz szczegóły dokumentu (scope: documents:read) - `PUT /documents/{id}` — Pełna aktualizacja istniejącego dokumentu (scope: documents:update) - `PATCH /documents/{id}` — Częściowa aktualizacja pola lub pól dokumentu (scope: documents:update) - `DELETE /documents/{id}` — Usuń dokument (scope: documents:delete) - `GET /documents/{id}/download` — Pobierz dokument (scope: documents:download) - `POST /documents/validate` — Walidacja próbna danych dokumentu (bez zapisu do bazy danych) (scope: documents:create) - `PATCH /documents/{id}/payment-status` — Zaktualizuj status płatności dokumentu (scope: documents:update-payment-status) - `PATCH /documents/{id}/external-reference` — Powiąż dokument z zewnętrznym numerem zamówienia (scope: documents:update-external-reference) - `DELETE /documents/{id}/external-reference` — Usuń powiązanie z zewnętrznym zamówieniem z dokumentu (scope: documents:update-external-reference) - `PATCH /documents/{id}/margin-purchases` — Ustaw kwoty nabycia pozycji w procedurze marży (scope: documents:update) - `POST /documents/{id}/send-to-ksef` — Wyślij dokument do KSeF (scope: documents:send-to-ksef) - `GET /documents/{id}/ksef-status` — Pobierz status wysyłki dokumentu do KSeF (scope: documents:read) - `GET /documents/{id}/history` — Pobierz historię dokumentu (scope: documents:read) ## clients - `GET /clients/` — Pobierz listę klientów (scope: clients:browse) - `POST /clients/` — Utwórz nowego klienta (scope: clients:create) - `GET /clients/{id}` — Pobierz szczegóły klienta (scope: clients:read) - `PUT /clients/{id}` — Zaktualizuj istniejącego klienta (scope: clients:update) - `DELETE /clients/{id}` — Usuń klienta (scope: clients:delete) ## products - `GET /products/` — Pobierz listę produktów (scope: products:browse) - `POST /products/` — Utwórz nowy produkt (scope: products:create) - `GET /products/{id}` — Pobierz szczegóły produktu (scope: products:read) - `PUT /products/{id}` — Zaktualizuj istniejący produkt (scope: products:update) - `DELETE /products/{id}` — Usuń produkt (scope: products:delete) ## webhooks - `GET /webhooks/` — Lista webhooków (scope: webhooks:read) - `POST /webhooks/` — Utwórz webhook (scope: webhooks:create) - `POST /webhooks/initial` — Utwórz początkowy webhook wtyczki (wszystkie zdarzenia) (scope: webhooks:create) - `GET /webhooks/{id}` — Pobierz webhook (scope: webhooks:read) - `PATCH /webhooks/{id}` — Zaktualizuj webhook (scope: webhooks:update) - `DELETE /webhooks/{id}` — Usuń webhook (scope: webhooks:delete) - `POST /webhooks/{id}/rotate-secret` — Rotuj sekret podpisujący webhooka (scope: webhooks:update) - `GET /webhooks/{id}/deliveries` — Lista ostatnich dostaw webhooka (scope: webhooks:read) - `POST /webhooks/{id}/test` — Wyślij dostawę testową do webhooka (scope: webhooks:update) ## ksef - `GET /ksef/connection` — Pobierz status połączenia z KSeF (scope: ksef:read) - `POST /ksef/connection` — Utwórz lub zaktualizuj połączenie z KSeF (token albo certyfikat) (scope: ksef:configure) - `DELETE /ksef/connection` — Dezaktywuj połączenie z KSeF (scope: ksef:configure) - `POST /ksef/connection/test` — Przetestuj autoryzację i uprawnienia połączenia z KSeF (scope: ksef:test) ## settings - `GET /settings/` — Pobierz ustawienia ogólne i ustawienia numeracji (scope: settings:read) - `PATCH /settings/general` — Zaktualizuj ustawienia ogólne (format daty, newsletter) (scope: settings:update) - `PATCH /settings/numbering` — Zaktualizuj ustawienia numeracji (typ, format, symbole) (scope: settings:update) ## company - `GET /company/nip-lookup` — Wyszukaj dane firmy w GUS po numerze NIP (scope: gus:company:read) ## dictionaries - `GET /dictionaries/` — Pobierz słowniki statyczne (typy dokumentów, metody płatności itp.) ## activity - `GET /activity/` — Pobierz aktywność konta (scope: activity:read) ## email-accounts - `GET /email-accounts/` — Lista kont pocztowych (scope: email-accounts:browse) - `GET /email-accounts/{id}` — Pobierz konto pocztowe (scope: email-accounts:read) ## employees - `GET /employees/permissions` — Pobierz rejestr dostępnych uprawnień pracowniczych - `GET /employees/` — Pobierz listę kont pracowniczych (scope: employees:browse) - `POST /employees/` — Utwórz konto pracownicze (scope: employees:create) - `GET /employees/{id}` — Pobierz konto pracownicze (scope: employees:browse) - `PUT /employees/{id}` — Zaktualizuj konto pracownicze (dane, uprawnienia, aktywacja, hasło) (scope: employees:update) - `DELETE /employees/{id}` — Usuń konto pracownicze (miękkie usunięcie, jego adres e-mail wraca do puli) (scope: employees:delete) - `GET /employees/{id}/activity` — Pobierz aktywność pracownika (scope: employees:browse) ## recurring-rules - `GET /recurring-rules/` — Pobierz listę reguł dokumentów cyklicznych (scope: recurring:browse) - `GET /recurring-rules/{id}/history` — Pobierz historię generacji reguły cyklicznej (scope: recurring:read) - `GET /recurring-rules/{id}` — Pobierz szczegóły reguły cyklicznej (scope: recurring:read) - `PATCH /recurring-rules/{id}/toggle` — Wstrzymaj lub wznów regułę cykliczną (scope: recurring:toggle) ## warehouse - `GET /warehouse/` — Pobierz listę magazynów (scope: warehouse:browse) - `POST /warehouse/` — Utwórz magazyn (wymaga planu MAX) (scope: warehouse:create) - `GET /warehouse/stock` — Pobierz raport stanów (suma ruchów per produkt/magazyn) (scope: warehouse:stock:read) - `GET /warehouse/movements` — Lista ruchów magazynowych (scope: warehouse:stock:read) - `POST /warehouse/adjustments` — Utwórz ręczną korektę stanu (ruch INW) (scope: warehouse:adjust) - `POST /warehouse/wz-from-invoice/{id}` — Utwórz dokument WZ z faktury sprzedaży (tylko pozycje towarowe) (scope: warehouse:wz:create) - `POST /warehouse/documents/validate` — Walidacja dokumentu magazynowego bez zapisu (scope: warehouse:documents:create) - `POST /warehouse/documents` — Wystawienie dokumentu magazynowego (WZ, PZ, PW, RW, MM) (scope: warehouse:documents:create) - `GET /warehouse/{id}` — Pobierz magazyn (scope: warehouse:browse) - `PUT /warehouse/{id}` — Zaktualizuj magazyn (scope: warehouse:update) - `DELETE /warehouse/{id}` — Usuń magazyn (zablokowane, gdy ma ruchy magazynowe) (scope: warehouse:delete)