Panel →

API OBIEQ

Adres produkcyjny: https://app.obieq.pl/api/v1. Do testowania integracji służy osobna instalacja: https://sandbox.obieq.pl/api/v1 — patrz niżej.

Przekazujesz dane sprzedaży, w odpowiedzi dostajesz gotowy dokument: numer z serii, PDF i status wysyłki. Numerację, wysyłkę do KSeF i pilnowanie terminów ustawowych prowadzi OBIEQ.

Od czego zacząć

  1. Zaloguj się na app.obieq.pl i wejdź w Klucze API.
  2. Kliknij „Przejdź do środowiska testowego" — dostaniesz odnośnik do ustawienia hasła na sandbox.obieq.pl. To osobna instalacja OBIEQ-a: bliźniacza firma z Twoimi danymi sprzedawcy, ale własna baza, własne hasło i własny drugi składnik (dasz je spokojnie programiście, bez ryzyka dla konta produkcyjnego).
  3. Zaloguj się na sandbox.obieq.pl i tam w Klucze API wygeneruj klucz. Każdy klucz z tej instalacji zaczyna się od obq_test_ i działa wyłącznie pod adresem https://sandbox.obieq.pl/api/v1.
  4. Wystaw pierwszą fakturę na sandbox.obieq.pl: POST /api/v1/faktury.
  5. Po sprawdzeniu integracji wygeneruj klucz produkcyjny (obq_…) w panelu na app.obieq.pl i przełącz się na https://app.obieq.pl/api/v1.

⚠ Klucz z sandbox.obieq.pl nie zadziała pod app.obieq.pl (i odwrotnie)

sandbox.obieq.pl to osobna instalacja OBIEQ-a — własna baza, własne konta, własne klucze, rozdzielone od app.obieq.pl.

gdzie założono klucz adres API
sandbox.obieq.pl (środowisko testowe) https://sandbox.obieq.pl/api/v1
app.obieq.pl (produkcja) https://app.obieq.pl/api/v1

Klucz z jednej instalacji użyty pod adresem drugiej nie uwierzytelni się w ogóle — bazy kluczy są rozdzielone, więc żądanie kończy się 401, a nie wysyłką pod zły adres.


Środowisko testowe

⚠ NIE PODŁĄCZAJ INTEGRACJI DO PRODUKCJI, ŻEBY JĄ SPRAWDZIĆ

To jest najważniejsze zdanie w całej tej dokumentacji i dlatego stoi na początku.

Każde żądanie wysłane kluczem produkcyjnym (obq_…, wydanym na app.obieq.pl) tworzy prawdziwy dokument podatkowy: dostaje kolejny numer w serii i trafia do rejestru Ministerstwa.

⚠ Faktury przyjętej przez KSeF NIE DA SIĘ ANULOWAĆ. Jedyną drogą jest korekta — czyli drugi dokument, który też zostaje w rejestrze na zawsze. A numer, który zużyła nieudana próba, zostaje zajęty: w serii robi się luka, którą trzeba umieć wytłumaczyć przy kontroli.

Nie da się tego „posprzątać później". Jedna pętla testowa puszczona na produkcji potrafi zostawić kilkanaście dokumentów, z których każdy wymaga osobnej korekty.

Testy prowadzi się WYŁĄCZNIE na sandbox.obieq.pl. Bez wyjątków — także „tylko jedną fakturę, żeby sprawdzić, czy działa".

Środowisko zakłada się w panelu na app.obieq.pl: Klucze API → Przejdź do środowiska testowego. Dostaniesz odnośnik do ustawienia hasła na sandbox.obieq.pl — osobna instalacja z tymi samymi danymi sprzedawcy, własną numeracją i dokumentami bez znaczenia prawnego. PDF-y są oznaczone napisem „DOKUMENT TESTOWY".

Środowisko wynika z klucza:

klucz działanie
obq_test_… (wydany na sandbox.obieq.pl) dokumenty testowe
obq_… (wydany na app.obieq.pl) dokumenty produkcyjne

⚠ Treść żądania nie przełącza trybu — decyduje wyłącznie to, na której instalacji i jakim kluczem działasz. Testy prowadzone kluczem produkcyjnym dają prawdziwe faktury z prawdziwymi numerami.

Jak rozpoznać, że pracujesz na testowym

Trzy rzeczy naraz, i wszystkie widać bez pytania nas:

⚠ Jeśli któregokolwiek z tych trzech brakuje, przerwij i sprawdź klucz — najprawdopodobniej wystawiasz prawdziwe faktury.

Wdrażanie kilku sklepów po kolei

Sklep, który już działa, zostaje na produkcji — a kolejny podpinasz w środowisku testowym równolegle. Te dwa nie mają ze sobą nic wspólnego: osobne dokumenty, osobna numeracja, osobny rejestr Ministerstwa.

⚠ Nie przełączaj działającego sklepu na klucz testowy „na chwilę". Faktury wystawione w tym czasie nie trafią do rejestru, a termin ustawowy biegnie dalej — i nikt się o tym nie dowie, dopóki nie zapyta kontrahent albo urząd.


Uwierzytelnianie

Authorization: Bearer obq_klucz

Klucze wydaje się w panelu (Klucze API). Wartość klucza jest pokazywana jednorazowo — w bazie przechowywany jest wyłącznie skrót.

Firma jest ustalana na podstawie klucza i nie występuje w adresie ani w treści żądania.

Klucz na sklep (projekt)

Firma prowadząca kilka sklepów pod jednym NIP-em może wydać osobny klucz dla każdego z nich. Klucz przypisany do projektu zmienia dwie rzeczy:

Dzięki temu integracja nie musi znać kodów naszych serii, a literówka w jej konfiguracji nie wkłada faktur do numeracji sąsiedniego sklepu.

Sklep wskazuje się przy wydawaniu klucza. Klucz firmowy, który już działa, da się do sklepu przypisać później — ale tylko raz: przypisanego nie przenosi się na inny sklep, bo dotychczasowe faktury zostały w poprzedniej serii, a nowe poszłyby do następnej. Faktury wystawione przed przypisaniem zostają przy całej firmie. Klucz dla innego sklepu wydaje się osobno.

Nie musisz zmieniać series w swojej integracji. W tym API kod serii wskazuje nie tylko ciąg numerów, ale i typ dokumentu (PF to jedyny sposób poproszenia o proformę), więc przy kluczu sklepu czytamy go jako wskazanie typu, a numerację bierzemy z ustawień sklepu. Wysyłasz PF — dostajesz proformę w serii proform tego sklepu. Typ, którego sklep nie ma rozpisanego, zostaje na serii firmowej, tak jak podałeś.

⚠ Kluczem sklepu nie da się wystawić w serii INNEGO sklepu. Tu podmiany nie ma: 422, bo to nie jest wskazanie typu, tylko próba wystawienia w cudzej numeracji — dokument zniknąłby z wyniku swojego sklepu i doliczył do sąsiada.

Dokumenty wtórne — faktura z proformy, rozliczenie zaliczki, korekta — biorą serię ze sklepu dokumentu źródłowego, a nie z klucza, którym o nie prosisz.

Klucz firmowy (bez projektu) działa dokładnie tak jak dotąd: widzi wszystkie dokumenty firmy i numeruje serią domyślną. Nic nie trzeba zmieniać.


Wystawienie faktury

POST /api/v1/faktury
Authorization: Bearer obq_klucz
Idempotency-Key: order-4821
Content-Type: application/json
{
  "buyer": {
    "name": "Przykładowa Firma Sp. z o.o.",
    "tax_id": "7771002503",
    "street": "Lipowa 12",
    "city": "Poznań",
    "postal_code": "61-001",
    "email": "kontakt@example.com"
  },
  "lines": [
    {
      "name": "Przegląd serwisowy urządzeń",
      "quantity": 2,
      "unit_net_cents": 30000,
      "vat_rate": "23",
      "unit": "usł.",
      "classification": "33.12.29.0"
    }
  ],
  "price_mode": "net",
  "expected_total_gross_cents": 73800,
  "issue_date": "2026-09-03",
  "sale_date": "2026-09-03",
  "due_date": "2026-09-17",
  "payment_method": "transfer",
  "series": "FV"
}

⚠ expected_total_gross_cents to suma kontrolna i jest wymagana — kwota brutto, jakiej spodziewa się Twój system (tu: 2 × 300,00 + 23% = 738,00 zł). Gdy nasze wyliczenie da co innego, faktura nie powstaje. Patrz Kwota do zapłaty.

Pola dokumentu

pole wymagane opis
buyer.name tak* nazwa nabywcy. *Nieobowiązkowa na fakturze uproszczonej
buyer.tax_id nie* polski NIP. ⚠ Przesądza o KSeF: z numerem faktura idzie do rejestru obowiązkowo, bez niego — tylko gdy firma tak ustawiła (szczegóły). *Obowiązkowy na fakturze uproszczonej
buyer.eu_country, buyer.eu_vat_id nie numer VAT UE — patrz Nabywca z Unii
buyer.street, buyer.city, buyer.postal_code nie adres nabywcy
buyer.email nie adres, na który idzie faktura
lines tak co najmniej jedna pozycja
series nie kod serii; bez niego seria faktur sprzedaży — a przy kluczu na sklep seria tego sklepu (⚠ kod wskazuje wtedy TYP, numerację bierze sklep; seria innego sklepu kończy się błędem 422). PF wystawia proformę, UPR — fakturę uproszczoną
issue_date, sale_date nie RRRR-MM-DD; bez nich — data bieżąca
due_date nie RRRR-MM-DD; bez niego — domyślny termin firmy albo brak terminu
payment_method nie patrz Płatność
price_mode tak net albo gross. ⚠ To nie tylko jednostka: gross znaczy, że podatek liczymy od kwoty brutto (art. 106e ust. 7). Patrz Ceny netto czy brutto
expected_total_gross_cents tak suma kontrolna brutto, patrz Kwota do zapłaty
expected_total_vat_cents nie suma kontrolna podatku — zalecana przy price_mode: gross
advance_gross_cents przy serii ZAL otrzymana zaliczka brutto — patrz Faktura zaliczkowa
vat_exemption_basis przy stawce zw przepis, z którego wynika zwolnienie
vat_exemption_kind nie ustawa (domyślnie), dyrektywa albo inna — patrz Zwolnienie z VAT
cash_method nie metoda kasowa, patrz Metoda kasowa
recipient nie odbiorca inny niż nabywca — patrz Odbiorca inny niż nabywca
paid_on nie zapłacono w CAŁOŚCI przed wystawieniem — patrz Płatności
payments nie wpłaty częściowe przed wystawieniem — patrz Płatności
notes nie uwagi drukowane na fakturze, do 1000 znaków
external_id nie Twój identyfikator dokumentu, do 191 znaków — patrz external_id
platnosc_url nie adres do zapłaty, drukowany jako kod QR — patrz Link do zapłaty

Pola pozycji

pole wymagane opis
name tak nazwa towaru lub usługi
quantity tak ilość, większa od zera
unit_net_cents przy price_mode: net cena jednostkowa netto w groszach
unit_gross_cents przy price_mode: gross cena jednostkowa brutto w groszach
vat_rate tak patrz Stawki VAT
unit nie jednostka miary; bez niej szt.
classification nie symbol PKWiU
gtu nie patrz Kody GTU
discount_cents / discount_percent nie patrz Rabaty

Płatności

⚠ To są DWIE różne sprawy i schemat FA(3) traktuje je inaczej.

1. Zapłata PRZED wystawieniem jest treścią dokumentu — idzie do KSeF i tam zostaje. Podaje się ją przy wystawianiu:

{
  "paid_on": "2026-09-03"
}

…albo, gdy wpłynęła w kilku ratach:

{
  "payments": [
    { "amount_cents": 50000, "date": "2026-09-01", "note": "zaliczka" },
    { "amount_cents": 30000, "date": "2026-09-03" }
  ]
}

⚠ Jedno albo drugie — w schemacie to xsd:choice. paid_on znaczy „całość jednym przelewem", payments — listę wpłat.

2. Zapłata PO wystawieniu to rozrachunek, a nie treść faktury:

POST /api/v1/faktury/{id}/platnosci
{ "amount_cents": 123000, "date": "2026-09-20", "note": "przelew" }

⚠ KSeF o niej nie wie i nie ma jak się dowiedzieć. Faktura trafiła do rejestru w chwili wystawienia, a dokumentu przyjętego przez KSeF nie da się zaktualizować ani anulować. Ta wpłata jest wiedzą sprzedawcy o tym, ile wpłynęło — służy do tego, żeby nie windykować kogoś, kto zapłacił.

⚠ Kwota może być UJEMNA — tak zapisuje się zwrot nadpłaty albo pieniędzy po korekcie.

Odpowiedź każdej faktury niesie paid_cents, outstanding_cents i listę payments. ⚠ outstanding_cents bywa ujemne — to nadpłata do zwrotu.

Żeby nie zaksięgować przelewu dwa razy

Ten sam przelew potrafi trafić do rozrachunków dwoma drogami: raz z Twojego systemu, raz z ręki człowieka w panelu OBIEQ-a. Faktura pokazuje wtedy „rozliczona", choć pieniędzy wpłynęła połowa — a widać to dopiero przy uzgadnianiu sald. Są na to trzy zabezpieczenia i żadne nie działa samo:

Idempotency-Key — nieobowiązkowy tutaj (przy wystawianiu jest wymagany), ale honorowany. Powtórka po timeoucie z tym samym kluczem oddaje tę samą odpowiedź, zamiast księgować przelew drugi raz. To jedyne z trzech, które niczego nie zgaduje — podawaj go.

expected_outstanding_cents — „wierzę, że do zapłaty zostało tyle". To samo, co expected_total_gross_cents przy wystawianiu, tylko o saldzie. Skoro Twój system przyjmuje płatności, wie, ile klient był winien; jeśli nasze saldo mówi co innego, ktoś zaksięgował ten przelew przed Tobą.

Ostrzeżenia wykrywane po naszej stronie — nadpłata albo taka sama kwota tego samego dnia.

Wszystkie trzy kończą się kodem 422 i wpłata NIE zostaje zapisana:

{
  "error": "Do zapłaty zostało 1 230,00 zł, a wpłata to 1 500,00 zł — wyjdzie nadpłata 270,00 zł.",
  "code": "nadplata",
  "outstanding_cents": 123000
}
code co się stało co z tym zrobić
stan_sie_zmienil saldo jest inne niż expected_outstanding_cents odczytaj fakturę na nowo i sprawdź, czy to nie ta sama wpłata
blizniacza_wplata jest już wpłata o tej kwocie i z tego dnia sprawdź, czy nie księgujesz jej drugi raz
nadplata wpłata przekracza to, co zostało zwykle trzeba dopisać zwrot, a nie kolejną wpłatę

Każdy z tych przypadków bywa prawdziwy — klient nadpłaca, a dwie równe raty tego samego dnia się zdarzają. Gdy wiesz, że wpłata jest poprawna, powtórz żądanie z "force": true. Nie wysyłaj force domyślnie: wtedy zabezpieczenia nie ma.

Odbiorca inny niż nabywca

Czasem faktura dotyczy kogoś, kto nie jest ani sprzedawcą, ani nabywcą: oddziału odbierającego towar, faktora, jednostki podrzędnej gminy, płatnika regulującego cudze zobowiązanie. Nabywcą — czyli podatnikiem — pozostaje buyer.

{
  "buyer": { "name": "Gmina Przykładowa", "tax_id": "7782003400" },
  "recipient": {
    "role": "8",
    "name": "Szkoła Podstawowa nr 7",
    "tax_id": "7831002703"
  }
}
rola znaczenie
1 faktor
2 odbiorca (oddział, jednostka nabywcy)
4 dodatkowy nabywca
6 dokonujący płatności za nabywcę
7 / 8 jednostka samorządu — wystawca / odbiorca
9 / 10 członek grupy VAT — wystawca / odbiorca
11 pracownik

⚠ role włącza cały blok, a name jest z nią wymagana. Sam odbiorca bez roli nie mówi nic: faktor i oddział to dwie zupełnie różne informacje dla urzędu.

⚠ tax_id może być puste — oddział wewnętrzny nabywcy własnego NIP-u nie ma i wtedy poprawną odpowiedzią jest jego brak, a nie przepisanie numeru nabywcy. Ten drugi wskazywałby ten sam podmiot dwa razy, w dwóch rolach.

⚠ Rola 8 i 10 włączają znaczniki JST i GV przy nabywcy. Nie ma na nie osobnych pól i to jest celowe: dałoby się wtedy zadeklarować jednostkę podrzędną i nie wskazać której.

Ról 3 (podmiot pierwotny) i 5 (wystawca faktury w imieniu podatnika) nie przyjmujemy — opisują rzeczy, których OBIEQ nie potrafi zrobić.

Nabywca z Unii

Kontrahent z innego kraju UE identyfikuje się numerem VAT UE, a nie polskim NIP-em. Podaje się go dwoma polami, bo schemat FA(3) nie zna jednego bez drugiego:

{
  "buyer": {
    "name": "Handel GmbH",
    "eu_country": "DE",
    "eu_vat_id": "123456789"
  }
}

⚠ eu_vat_id BEZ prefiksu kraju — kraj niesie eu_country.

⚠ Dwie pułapki w słowniku krajów, obie wyglądające na literówki: Grecja ma kod EL, a nie GR, a Irlandia Północna własny kod XI. Wielkiej Brytanii na liście nie ma — po brexicie nie jest w UE.

⚠ Numer unijny ma pierwszeństwo przed tax_id. Kontrahent z UE bywa zarejestrowany także w Polsce; skoro podałeś numer unijny, to o tę rejestrację chodziło — a przy dostawie wewnątrzwspólnotowej to ona jest podstawą stawki 0%.

⚠ Nabywca z numerem VAT UE liczy się jako FIRMA. Faktura idzie więc do KSeF obowiązkowo, niezależnie od ustawienia „wysyłaj faktury konsumenckie".

Ceny netto czy brutto

Domyślnie podajesz ceny netto i podatek jest do nich doliczany. Ustawienie price_mode na gross odwraca kierunek: ceny zawierają już podatek, a my wyjmujemy go metodą „w stu" (art. 106e ust. 7 i 8 ustawy o VAT).

⚠ price_mode to nie tylko jednostka — wybierasz metodę liczenia podatku

To jest jedna decyzja o dwóch skutkach i warto znać oba, ZANIM zaczniesz wysyłać gross.

1. Podatek liczy się inaczej — i to wynika z arytmetyki, nie z naszego wyboru. Przysyłając ceny brutto, nie da się policzyć podatku inaczej niż wyjmując go „w stu": KP = WB × SP / (100 + SP). To jest metoda z art. 106e ust. 7 ustawy o VAT. Wysyłając gross, stosujesz ten przepis — niezależnie od tego, czy o nim myślałeś.

⚠ Praktyczny skutek: przy tej samej kwocie wyniki potrafią różnić się o grosz. 100,00 netto + 23% daje 123,00 brutto, ale 123,00 brutto rozbite w stu daje 100,00 netto — a 123,45 brutto daje 100,37 netto i 23,08 podatku, czego nie da się odtworzyć doliczaniem od okrągłego netto.

2. Na fakturze pojawia się dodatkowa kolumna. Do KSeF idą wtedy P_9B (cena jednostkowa brutto) i P_11A (wartość sprzedaży brutto), a na wydruku dochodzi kolumna „Cena brutto". Schemat FA(3) wiąże te pola z art. 106e ust. 7 i 8 — osobnego znacznika tej metody struktura nie ma, więc ich obecność jest jedyną informacją o niej.

⚠ Nie odbieramy Ci przez to ceny netto. P_9A i P_11 wysyłamy ZAWSZE, w obu trybach. Art. 106e ust. 8 pozwala wykazać brutto zamiast netto — my z tego podstawienia nie korzystamy i pokazujemy jedno obok drugiego, bo cena netto jest wymagana przez art. 106e ust. 1 pkt 9.

⚠ Nie wysyłamy P_9B „na wszelki wypadek" przy dokumencie liczonym od netto — byłaby to informacja o metodzie, której nie zastosowano. (Sprawdziliśmy na testowym KSeF: taki dokument zostaje PRZYJĘTY, więc reguła ze schematu nie jest egzekwowana. Przyjęcie nie jest jednak wykładnią przepisu.)

Jak wybrać: gross, jeśli Twój system zna cenę, którą płaci klient (sprzedaż detaliczna, koszyk z ceną „300 zł"). net, jeśli kwotujesz netto i podatek jest doliczany (typowe B2B). Jedna integracja może wysyłać jedno i drugie — decyduje pojedyncze żądanie, nie ustawienie firmy.

{
  "price_mode": "gross",
  "lines": [
    { "name": "Tort urodzinowy", "quantity": 1, "unit_gross_cents": 30000, "vat_rate": "23" }
  ]
}

Faktura wyjdzie na 300,00 zł brutto: 243,90 netto i 56,10 podatku.

Po co to jest. Sklep detaliczny zna cenę z podatkiem — pokazuje „300 zł", a nie „243,90 + VAT". Gdybyś musiał przeliczać ją na netto po swojej stronie, Twoje zaokrąglenie i nasze mogłyby rozejść się o grosz, a wtedy faktura mówi co innego niż kwota, którą klient zapłacił. W trybie gross przysyłasz to, co naprawdę masz, i przeliczenie dzieje się raz.

⚠ Pola nie da się pomylić bezkarnie i to jest celowe. Podanie unit_net_cents przy price_mode: gross (albo obu naraz) kończy się 422. Nie zgadujemy, o którą cenę chodziło: pomyłka przeliczona po cichu dałaby dokument zawyżony o 23%, a faktury przyjętej przez KSeF nie da się anulować.

Co ląduje na fakturze w trybie brutto

⚠ Cena netto nie znika. Faktura musi nieść cenę jednostkową bez kwoty podatku i wartość sprzedaży netto — wymaga tego art. 106e ust. 1 pkt 9 i 11 i nie zależy to od sposobu liczenia. price_mode: gross dokłada wartości brutto, a nie zastępuje nimi netto.

Dlatego dokument wystawiony w trybie gross niesie jedno i drugie:

co pole w KSeF na wydruku
cena jednostkowa netto P_9A kolumna „Cena netto"
cena jednostkowa brutto P_9B kolumna „Cena brutto"
wartość pozycji netto P_11 kolumna „Wartość netto"
wartość pozycji brutto P_11A kolumna „Wartość brutto"

Wartości netto liczymy my — nie musisz ich przysyłać i nie ma jak ich podać (unit_net_cents przy price_mode: gross kończy się 422). W odpowiedzi dostajesz je wyliczone: pozycja niesie unit_net_cents także wtedy, gdy przysłałeś cenę brutto.

⚠ Podsumowanie dokumentu (P_13_x, P_14_x) zostaje w OBU trybach netto plus podatek. Ustawa pozwala pokazać cenę brutto w wierszu, a nie przebudować podsumowanie faktury.

⚠ Tryb dotyczy CAŁEGO dokumentu, nie pojedynczej pozycji. Faktura, na której część wierszy ma cenę netto, a część brutto, jest nie do sprawdzenia kalkulatorem przez nabywcę.

⚠ price_mode jest WYMAGANY przy każdym dokumencie z API. Żądanie bez niego kończy się kodem 422. Wartość domyślna byłaby tu gorsza niż jej brak: integracja wystawiająca i B2B, i B2C, która zapomni trybu na JEDNEJ ze ścieżek, nie dostałaby żadnego błędu — tylko cichą fakturę policzoną nie tak.

Odpowiedź zawsze zawiera price_mode z tym, jak dokument NAPRAWDĘ został policzony.

⚠ Ustawienie firmy „ceny brutto" dotyczy WYŁĄCZNIE panelu. Sklep, który konsumentowi pokazuje cenę brutto („300 zł za wizytę"), a firmie kwotuje netto, wysyła jedne i drugie faktury tym samym kluczem API — wskazując price_mode przy każdej. Tryb zapisuje się na dokumencie, więc późniejsza zmiana ustawienia firmy nie rusza tego, co już wystawione.

⚠ Korekta dziedziczy tryb po fakturze pierwotnej i nie da się go tam zmienić — inaczej korekta niczego niezmieniająca dawałaby różnicę różną od zera. Przy korekcie faktury liczonej od brutto podaje się unit_gross_cents.

Kwota do zapłaty

⚠ expected_total_gross_cents jest WYMAGANE przy każdym dokumencie — B2B i B2C tak samo, fakturze i proformie tak samo. Żądanie bez niego kończy się kodem 422.

To suma kontrolna: kwota brutto, jakiej spodziewa się Twój system. Jeśli nasze wyliczenie jest inne, odpowiadamy 422 i faktura nie powstaje.

{
  "expected_total_gross_cents": 30000,
  "lines": [ … ]
}

⚠ To nie są dane wejściowe. Pole nie wchodzi do dokumentu i nie potrafi zmienić ani grosza — może wyłącznie zatrzymać wystawienie. Kwoty liczy serwer.

⚠ Sprawdzenie po fakcie nie miałoby wartości. Odpowiedź i tak zawiera pełne wyliczenie, więc mógłbyś porównać sumy po wystawieniu — ale wtedy dokument już istnieje i już poszedł do rejestru, skąd nie da się go wycofać.

⚠ Dlaczego wymagamy, zamiast pozwolić wybrać. To jedyne zabezpieczenie, które łapie pomyłkę z pewnością, a nie z podobieństwa. Żądanie, w którym unit_net_cents wypełniono cenami brutto, jest formalnie bez zarzutu — pola się zgadzają, walidacja przechodzi — a dokument wychodzi zawyżony o 23%. Nic poza porównaniem z kwotą, której spodziewa się Twój system, tego nie zatrzyma, a faktury przyjętej przez KSeF nie da się anulować.

Skoro wystawiasz fakturę, znasz kwotę, którą klient ma zapłacić — to nie jest nowa informacja do wyliczenia, tylko liczba, którą już masz.

⚠ Przy price_mode: gross sama suma brutto to za mało

Suma kontrolna sprawdza coś tylko wtedy, gdy porównuje liczbę wyliczoną, a nie tę, którą sam przysłałeś:

tryb co przysyłasz co wyliczamy co sprawdza expected_total_gross_cents
net ceny netto brutto i podatek dużo — zła stawka zmienia brutto
gross ceny brutto netto i podatek prawie nic — porównujemy Twoją liczbę z Twoją liczbą

Przykład, który przechodził: 123,00 zł brutto ze stawką 8 wpisaną zamiast 23 daje 113,89 + 9,11 zamiast 100,00 + 23,00. Brutto zgadza się co do grosza w obu wersjach.

Dlatego w trybie gross dołóż expected_total_vat_cents — podatek jest liczbą wyliczaną w obu trybach, więc to on domyka lukę:

{
  "price_mode": "gross",
  "expected_total_gross_cents": 12300,
  "expected_total_vat_cents": 2300
}

⚠ Nieobowiązkowe i to jest celowe. Wymóg podania podatku w trybie gross kazałby Ci wykonać dokładnie to dzielenie „w stu", od którego ten tryb miał Cię uwolnić. Kto potrafi je zrobić, dostaje sprawdzenie; kto nie — nie zostaje odcięty od API.

⚠ Panelu to nie dotyczy. Człowiek wpisujący pozycje nie ma skąd wziąć sumy, której jeszcze nie policzył — od jej policzenia jest formularz.

⚠ Korekty też nie: tam przysyłasz stan docelowy, a różnicę liczy serwer.

Zwolnienie z VAT

Przy stawce zw obowiązkowe jest vat_exemption_basis — przepis, z którego wynika zwolnienie. Schemat FA(3) ma na to trzy osobne rubryki, a wskazuje je vat_exemption_kind:

wartość rubryka FA(3) co się wpisuje
ustawa (domyślnie) P_19A przepis ustawy o VAT, np. art. 113 ust. 1 ustawy o VAT
dyrektywa P_19B przepis dyrektywy 2006/112/WE
inna P_19C inna podstawa prawna

⚠ Do 04.09.2026 wszystko szło do P_19A. Zwolnienie z dyrektywy — typowe przy usługach dla kontrahentów z UE — trafiało więc do rubryki o ustawie. Jeśli Twoja podstawa nie jest przepisem polskiej ustawy, podaj rodzaj.

external_id — Twój identyfikator dokumentu

Opcjonalne pole tekstowe (do 191 znaków), które przyjmujemy przy tworzeniu dokumentu i oddajemy wszędzie, gdzie ten dokument się pojawia. Nie sprawdzamy jego formatu, nie wymuszamy unikalności i niczego po naszej stronie nim nie sterujemy — to przelotka.

{ "external_id": "ZAM-2026-0042" }

Przyjmują je: POST /faktury, POST /faktury/{id}/korekta, POST /faktury/{id}/faktura (z proformy) i POST /faktury/{id}/rozliczenie.

Po co to jest

Wystawienie idzie przez sieć, więc możliwa jest sytuacja, w której dokument powstał u nas, a odpowiedź do Ciebie nie dotarła — nie zapisałeś jego id. Faktura jest wtedy w KSeF, a Twój system o niej nie wie.

⚠ Bez znacznika nie da się jej wykryć. W tej samej firmie wystawia się też dokumenty ręcznie z panelu, więc „dokument, którego nie mam u siebie" znaczy jednocześnie „zgubiony przez integrację" i „wystawiony przez człowieka" — a tylko pierwszy przypadek wymaga reakcji.

Znacznik rozcina tę dwuznaczność: dokument z external_id pochodzi z Twojego systemu, bez — został wystawiony ręcznie.

Jak z tego korzystać

external_id wraca w skrócie na liście, nie tylko w dokumencie pełnym — więc jednym przebiegiem synchronizacji wyłapujesz swoje dokumenty:

GET /api/v1/faktury?updated_since=2026-09-06T00:00:00%2B02:00
{ "data": [
  { "id": 41, "number": "FV/12/09/2026", "external_id": "ZAM-2026-0042" },
  { "id": 42, "number": "FV/13/09/2026", "external_id": null }
] }

Pierwszy jest Twój — jeśli nie masz go u siebie, zgubiła się odpowiedź. Drugi wystawił człowiek w panelu i nie należy go ruszać.

Da się też zapytać wprost, czy konkretny dokument u nas jest:

GET /api/v1/faktury?external_id=ZAM-2026-0042

⚠ Zwraca LISTĘ, a nie jeden dokument — bo pole nie jest unikalne. Zwykle przyjdzie jeden element; dwa znaczą, że coś po Twojej stronie wystawiło dokument dwa razy, i lepiej, żebyś to zobaczył.

⚠ To NIE jest klucz idempotencji

external_id nie chroni przed dublowaniem. Dwa żądania z tym samym znacznikiem dadzą dwa dokumenty — a faktury nie da się anulować.

Przed dublowaniem broni Idempotency-Key, obowiązkowy przy wystawianiu i działający na całym żądaniu. Te dwa pola robią różne rzeczy i potrzebne są oba: klucz idempotencji chroni przed powtórką teraz, external_id pozwala rozpoznać dokument później.

⚠ Dokumenty wtórne dostają WŁASNY znacznik, nie odziedziczony. Korekta, faktura z proformy i rozliczenie nie przejmują external_id dokumentu źródłowego — bo to inne dokumenty w Twoim systemie. Podaj im własny albo zostaw puste.

Adres, pod którym nabywca zapłaci za ten dokument. Drukujemy go na PDF-ie jako kod QR w karcie płatności — nabywca skanuje telefonem, zamiast przepisywać numer konta.

{ "platnosc_url": "https://platnosci.twojsklep.pl/z/9f3a2b7c1e" }

Przyjmujemy wyłącznie https, do 512 znaków. Adres http odbijamy kodem 422: pod nim dane płatności szłyby otwartym tekstem, a link drukujemy na dokumencie, który idzie do obcej firmy.

⚠ DOMENA MUSI BYĆ NA LIŚCIE ZATWIERDZONEJ W PANELU (Ustawienia → „Link do zapłaty — dozwolone domeny", od 23.09.2026). Klucz API wskazuje jedną z zatwierdzonych domen dla konkretnego dokumentu — nie rozszerza listy. Bez tego skompromitowany klucz mógłby podmienić kod QR na fakturze na stronę podszywającą się pod płatność, a nabywca nie ma jak tego zauważyć przed zeskanowaniem. Listę ustawia wyłącznie administrator firmy w panelu.

⚠ PUSTA LISTA U FIRMY PRZEPUSZCZA KAŻDY ADRES https — to stan wyjściowy, żeby wdrożenie tej reguły nie wyłączyło z dnia na dzień kodu QR integracjom, które jeszcze nic nie skonfigurowały. Realną ochronę dostaje firma dopiero po wypełnieniu listy. Domena z listy obejmuje też jej subdomeny (tpay.com przepuszcza sandbox.tpay.com).

⚠ Kod pokazuje się TYLKO na dokumencie niezapłaconym i na plus. Znika sam, gdy dopiszesz zapłatę (paid_on, payments albo POST /platnosc) — kod „zapłać" na fakturze już opłaconej to zaproszenie do drugiej wpłaty. Na korekcie na minus nie pojawia się nigdy: tym dokumentem pieniądze wracają do nabywcy. Przy zapłacie częściowej kod zostaje, bo jest co dopłacić.

⚠ Nie zastępuje numeru konta. Rachunek zostaje na dokumencie tak jak był — kod jest drogą na skróty, a nie jedyną drogą.

⚠ Nie idzie do KSeF. Schemat FA(3) nie ma pola na adres płatności. To jest wygoda dla nabywcy, a nie treść podatkowa — w rejestrze go nie będzie.

⚠ Adresu nie wypisujemy na dokumencie — jest wyłącznie w kodzie. Nabywca zobaczy, dokąd trafił, dopiero po zeskanowaniu, więc strona pod tym adresem musi sama mówić, czyja jest: nazwa sprzedawcy, numer dokumentu i kwota. Strona, która wita go samym formularzem płatności, wygląda jak oszustwo — i coraz częściej nim jest.

⚠ Dokumenty wtórne dostają własny adres, tak samo jak external_id. Faktura wystawiona z proformy nie przejmuje jej linku — proforma bywa już opłacona, a rozliczenie zaliczki dotyczy innej kwoty niż zaliczka.

Adres wraca w odpowiedzi i na liście jako platnosc_url — zawsze ten zapisany, także po zapłacie. „Czy pokazać kod" jest pytaniem wydruku, nie danych.

Termin płatności

due_date podane w żądaniu wygrywa zawsze — Twój system zna warunki konkretnej umowy, my znamy tylko regułę domyślną.

Bez tego pola termin bierze się z ustawień:

gdzie co ustawia
firma (panel → Ustawienia) domyślna liczba dni od daty wystawienia
projekt (panel → Projekty) własna liczba dni, 0 = bez terminu

Firma bez ustawionego terminu wystawia dokumenty bez terminu płatności — czyli dokładnie tak, jak przed 06.09.2026. Nic nie zmienia się samo.

⚠ "due_date": null to co innego niż pominięcie pola. Jawny null znaczy „ten dokument świadomie nie ma terminu" i ustawienie go nie nadpisze. Dzięki temu integracja wystawiająca sprzedaż za gotówkę nie zacznie nagle dostawać terminów, gdy właściciel firmy ustawi domyślne 14 dni dla przelewów.

⚠ Termin liczy się od daty wystawienia tego dokumentu, a nie od dnia wysłania żądania — faktura wystawiona wstecz dostaje termin liczony od swojej własnej daty.

Metoda kasowa

cash_method: true znaczy, że obowiązek podatkowy powstaje z chwilą zapłaty (art. 21 ust. 1 albo art. 19a ust. 5 pkt 1 ustawy). Faktura dostaje wtedy znacznik P_16 w KSeF i wyrazy „metoda kasowa" na wydruku — nabywca odlicza podatek dopiero po zapłacie.

Bez tego pola rozstrzyga ustawienie firmy: podatnik rozliczający się kasowo ma je włączone i nie musi go podawać przy każdym dokumencie. false przekazane wprost zdejmuje adnotację mimo ustawienia firmy — bo art. 19a ust. 5 pkt 1 dotyczy pojedynczych transakcji.

Faktura do proformy

Proforma poprzedza fakturę — to cała jej racja bytu. Zamiast przepisywać dokument, wystawiasz go z proformy:

POST /api/v1/faktury/{id}/faktura
Idempotency-Key: sprzedaz-4821
{ "type": "vat" }
pole wymagane opis
type tak vat — sprzedaż już nastąpiła; zaliczkowa — wpłata przed sprzedażą
series nie kod serii; bez niego pierwsza seria danego typu
issue_date, sale_date, due_date nie jak przy zwykłym wystawianiu
external_id nie Twój identyfikator — własny dla tej faktury, patrz external_id
platnosc_url nie adres do zapłaty — własny, patrz Link do zapłaty
buyer nie poprawione dane nabywcy: name, street, city, postal_code, email — patrz niżej

Nabywca, pozycje, tryb cen, forma płatności i oznaczenia przechodzą z proformy — nie podaje się ich ponownie. Pozycji, trybu cen i oznaczeń nie da się tu zmienić.

Poprawka danych nabywcy (buyer)

Nabywca dostał proformę ze skróconą nazwą albo starym adresem i prosi o właściwe dane na fakturze. Proforma nie jest fakturą i nie idzie do KSeF, więc nie ma czego korygować — wystarczy, że faktura wyjdzie z dobrymi danymi:

{
  "type": "vat",
  "buyer": { "name": "Przedszkole Publiczne nr 3 im. Jana Brzechwy w Warszawie" }
}

⚠ external_id i platnosc_url NIE przechodzą — w odróżnieniu od wszystkiego powyżej. To inny dokument w Twoim systemie, więc znacznik ma być inny; a odnośnik do zapłaty przeniesiony z proformy wskazywałby transakcję, która bywa już opłacona.

⚠ Idempotency-Key jest OBOWIĄZKOWY. Powtórka po timeoucie dałaby drugą fakturę do tej samej wpłaty, a takiego dokumentu nie da się anulować.

⚠ type jest wymagany i nie zgadujemy go za Ciebie. Z danych nie wynika, czy usługa została wykonana — a od tego zależy, czy właściwa jest zwykła faktura, czy zaliczkowa (art. 106b ust. 1 pkt 4).

⚠ Wpłaty PRZECHODZĄ na nowy dokument

Pieniądze wpłynęły raz i mają być policzone raz. Wpłaty zapisane na proformie zostają przepięte na fakturę — z tą samą datą, kwotą i opisem.

gdyby zamiast tego… skutek
skopiować ta sama kwota policzona dwa razy w rozrachunkach
zostawić na proformie faktura oznaczona jako NIEOPŁACONA mimo otrzymanych pieniędzy

⚠ Skutek uboczny jest zamierzony: przeniesiona wpłata ma datę sprzed wystawienia faktury, więc trafia także do jej treści w KSeF (Zaplacono albo ZaliczkaCzesciowa) — schemat FA(3) ma na to własne pola.

Przy type: "zaliczkowa" kwota zaliczki bierze się z tych wpłat, a pozycje proformy stają się order (zamówieniem). Bez zapisanej wpłaty żądanie kończy się 422: dokument zaliczkowy dokumentuje OTRZYMANIE zapłaty.

⚠ Suma kontrolna nie jest tu wymagana, inaczej niż przy POST /faktury. Pozycje nie przychodzą z żądania, tylko z naszej proformy — nie ma więc drugiego, niezależnego źródła, z którym można by je porównać.

⚠ Do jednej proformy wystawia się jeden dokument. Druga próba kończy się 422 — byłaby to ta sama sprzedaż policzona dwa razy.

Faktura zaliczkowa

Klient wpłaca zadatek, a sprzedaż nastąpi później. Dokument wystawia się serią ZAL, tak samo jak proformę:

{
  "series": "ZAL",
  "buyer": { "name": "Przykładowa Firma Sp. z o.o.", "tax_id": "7771002503" },
  "price_mode": "net",
  "lines": [
    { "name": "Montaż instalacji", "quantity": 1, "unit_net_cents": 100000, "vat_rate": "23" }
  ],
  "advance_gross_cents": 30000,
  "expected_total_gross_cents": 30000
}

⚠ lines opisują ZAMÓWIENIE, a nie to, za co dokument żąda pieniędzy. To jest jedyna rzecz, która odróżnia to żądanie od zwykłej faktury — i jedyna, którą łatwo pomylić. Zamówienie musi być na dokumencie, bo bez niego nabywca nie wie, na poczet czego wpłacił (art. 106f ust. 1 pkt 4 ustawy).

⚠ advance_gross_cents to kwota, która WPŁYNĘŁA — brutto, w groszach. Podatek wyjmujemy z niej „w stu" (art. 106f ust. 1 pkt 3), a nie doliczamy do wartości zamówienia. W przykładzie: zamówienie 1 230,00 zł, zaliczka 300,00 zł → dokument na 300,00 brutto = 243,90 netto + 56,10 podatku.

⚠ expected_total_gross_cents to ZALICZKA, a nie wartość zamówienia. Sumy dokumentu niosą wpłatę, więc suma kontrolna dotyczy jej. Podanie tu wartości całości kończy się kodem 422 — to najbardziej prawdopodobna pomyłka przy tym dokumencie i dlatego kończy się głośno.

Czego nie wolno:

sytuacja odpowiedź
seria ZAL bez advance_gross_cents 422 — dokument zaliczkowy bez wpłaty nic nie dokumentuje
zaliczka większa niż zamówienie 422 — to nadpłata, nie zaliczka
advance_gross_cents przy zwykłej serii pole jest ignorowane; o trybie rozstrzyga seria

Co dostajesz w odpowiedzi

⚠ lines jest PUSTE i tak ma być. Schemat FA(3) mówi wprost, że pozycje faktury (FaWiersz) są dla faktury zaliczkowej węzłem opcjonalnym — zamówienie jedzie osobno. Dlatego odpowiedź niesie blok order:

{
  "type": "zaliczkowa",
  "number": "ZAL/1/09/2026",
  "total_gross_cents": 30000,
  "total_net_cents": 24390,
  "total_vat_cents": 5610,
  "lines": [],
  "order": {
    "total_gross_cents": 123000,
    "lines": [
      { "position": 1, "name": "Montaż instalacji", "quantity": 1,
        "unit_net_cents": 100000, "vat_rate": "23",
        "net_cents": 100000, "vat_cents": 23000, "gross_cents": 123000 }
    ]
  }
}

⚠ total_gross_cents to zaliczka, order.total_gross_cents to całość. Te liczby celowo się nie zgadzają — pierwsza mówi, ile wpłynęło, druga, na poczet czego.

⚠ Zaliczka idzie do KSeF — to faktura w rozumieniu ustawy, inaczej niż proforma.

Faktura rozliczeniowa

Domyka fakturę zaliczkową (art. 106f ust. 3): pokazuje całe zamówienie, wskazuje wystawione zaliczki i zostawia do zapłaty resztę.

POST /api/v1/faktury/{id}/rozliczenie
Idempotency-Key: rozliczenie-4821

{id} to faktura zaliczkowa. Treść żądania może być pusta — wszystko jest już na zaliczce: zamówienie, nabywca, tryb cen, kwota wpłaty i jej data. Opcjonalnie series, issue_date, sale_date, due_date, a także external_id i platnosc_url — własne dla rozliczenia, nie odziedziczone po zaliczce. Odnośnik z zaliczki dotyczyłby innej kwoty niż ta, która zostaje tu do dopłaty.

⚠ Idempotency-Key jest OBOWIĄZKOWY — powtórka dałaby drugi dokument odejmujący tę samą zaliczkę.

⚠ Dwie liczby, które celowo się nie zgadzają

Schemat FA(3) mówi o tym dokumencie dwie rzeczy naraz:

co ile
pozycje (lines, total_*_cents) całe zamówienie
P_15 w KSeF — kwota do zapłaty całość minus zaliczki

Przykład: zamówienie 1 230,00 zł, wpłacona zaliczka 300,00 zł → pozycje niosą 1 230,00, a do zapłaty zostaje 930,00.

⚠ Pomylenie tych dwóch daje albo żądanie zapłaty całości po raz drugi, albo fakturę na ułamek sprzedaży — i jedno, i drugie przechodzi walidację schematu, bo obie liczby są poprawnymi kwotami.

Co trafia do KSeF

⚠ Jedna zaliczka rozlicza się raz. Druga próba kończy się 422: ta sama wpłata odjęta dwa razy.

⚠ Kilka zaliczek do jednego zamówienia rozlicza się jednym dokumentem — rozpoznajemy je po wspólnej proformie i wykazujemy wszystkie. Odjęcie tylko części znaczyłoby żądanie pieniędzy, które klient już wpłacił.

Proforma

Proformę wystawia się serią, a nie osobnym polem type:

{
  "series": "PF",
  "buyer": { "name": "Przykładowa Firma Sp. z o.o." },
  "lines": [ … ]
}

Reszta żądania wygląda identycznie jak przy fakturze — te same pozycje, te same kwoty, ta sama odpowiedź z pdf_url. Różnice są trzy:

⚠ Do 05.09.2026 pisało tu, że „nie ma do niej rozrachunków". To było błędne i zgłosili to integratorzy: należność JEST, tylko dokument nie jest podatkowy.

⚠ Wpłata na proformę to ZALICZKA i sama z siebie niczego nie rozlicza. Gdy sprzedaż jeszcze nie nastąpiła, otrzymana zaliczka wymaga faktury zaliczkowej (series: "ZAL") — art. 106b ust. 1 pkt 4 ustawy. Gdy sprzedaż już nastąpiła, wystawia się zwykłą fakturę VAT. Proforma nie zamienia się w żaden z tych dokumentów — zostaje obok, jako dokument handlowy.

Kiedy dojdzie do sprzedaży, wystawiasz osobną fakturę zwykłym żądaniem. Proforma nie zamienia się w fakturę i nie znika — zostaje jako dokument handlowy.

⚠ Serią korekt (KOR) nie wystawisz dokumentu przez POST /faktury — kończy się to kodem 422. Korekta zawsze wskazuje dokument pierwotny, więc idzie przez POST /faktury/{id}/korekta.

Faktura uproszczona

Potocznie „paragon z NIP-em" (art. 106e ust. 5 pkt 3). Wystawia się ją serią UPR:

{
  "series": "UPR",
  "buyer": { "tax_id": "7771002503" },
  "lines": [
    { "name": "Kawa duża", "quantity": 2, "unit_net_cents": 1200, "vat_rate": "23" }
  ],
  "price_mode": "net",
  "expected_total_gross_cents": 2952
}

⚠ buyer.name jest tu NIEOBOWIĄZKOWE — i to jest cały sens tego dokumentu. Kasa sklepowa zna NIP z paragonu i nie zna nazwy nabywcy; wymuszanie nazwy kazałoby jej wysyłać „Klient", czyli wymyśloną wartość, która trafi do rejestru państwowego i której nie da się stamtąd wycofać. Nazwę, którą znasz, podaj — wtedy pojedzie do KSeF normalnie.

To pełnoprawna faktura: idzie do KSeF jak każda inna i daje nabywcy prawo do odliczenia. Ma tylko własny licznik (UPR/1/09/2026) i "type": "uproszczona".

⚠ Kiedy OBIEQ odmówi jej wystawienia

Odmowa to kod 422 i dokument nie powstaje. Każdy z tych warunków wynika z ustawy, a nie z naszego wyboru — i żadnego nie złapie walidacja schematu FA(3), bo UPR jest w nim wartością poprawną zawsze:

warunek dlaczego
brutto ponad 450,00 zł art. 106e ust. 5 pkt 3 — próg liczony od należności ogółem, czyli od BRUTTO. Dokument równo na 450,00 zł jeszcze przechodzi
brak buyer.tax_id (ani buyer.eu_vat_id) bez numeru nabywcy to paragon, a nie faktura — ust. 5 pkt 3 pozwala pominąć nazwę, ale nie numer
stawka 0-wdt, np-1 albo np-2 art. 106e ust. 6 wyłącza WDT i sprzedaż rozliczaną przez nabywcę w innym państwie UE
waluta inna niż PLN granica wynosi wtedy 100 EUR, a OBIEQ nie przelicza jeszcze kursów

W każdym z tych przypadków tę samą sprzedaż wystawisz zwykłą fakturą (seria FV) — bez żadnych innych zmian w żądaniu.

⚠ Eksport (0-ex) i krajowe odwrotne obciążenie (oo) przechodzą — ustawa ich nie wyłącza, więc my też nie.

Stawki VAT

wartość znaczenie w KSeF
23, 8, 5 stawki procentowe 23, 8, 5
0 0% krajowa 0 KR
0-wdt 0% — wewnątrzwspólnotowa dostawa towarów 0 WDT
0-ex 0% — eksport poza Unię 0 EX
zw zwolniona — wymaga vat_exemption_basis zw
np-1 nie podlega — usługi z art. 100 ust. 1 pkt 4 np I
np-2 nie podlega — pozostała sprzedaż poza terytorium kraju np II
oo odwrotne obciążenie — podatek rozlicza nabywca oo

⚠ Zero ma trzy warianty i nie są wymienne. Sprzedaż krajowa, dostawa wewnątrzwspólnotowa i eksport trafiają w trzy różne pozycje deklaracji. Wysłanie 0 przy eksporcie to wykazanie go jako sprzedaży krajowej.

⚠ Tak samo np-1 i np-2: pierwsze dotyczy usług, dla których podatek rozlicza nabywca z innego kraju UE, drugie — pozostałej sprzedaży poza terytorium kraju.

⚠ Faktura ze stawką zw bez wskazania przepisu kończy się kodem 422. Dokument bez tej informacji jest wadliwy i trzeba go korygować, więc lepiej odmówić niż wydać numer z serii:

{
  "lines": [{ "name": "Szkolenie", "quantity": 1, "unit_net_cents": 10000, "vat_rate": "zw" }],
  "vat_exemption_basis": "art. 113 ust. 1 ustawy o VAT"
}

Idempotency-Key

Nagłówek jest wymagany, a jego wartość musi być unikalna dla każdego dokumentu (np. identyfikator zamówienia).

Użyj identyfikatora, który już masz po swojej stronie i który nie powtórzy się dla innej sprzedaży — numeru zamówienia, płatności, rezerwacji. ⚠ Klucz generowany losowo przy każdej próbie nie chroni przed niczym: ponowienie po timeoucie wystawi wtedy drugą fakturę na tę samą sprzedaż.

sytuacja odpowiedź
ten sam klucz, ta sama treść ta sama faktura, ten sam numer
ten sam klucz, żądanie w toku 409
ten sam klucz, inna treść 409
poprzednia próba zakończona błędem klucz zwolniony, można ponowić

Klucz zabezpiecza przed powstaniem drugiego dokumentu, nie przed ponowną próbą.

Kwoty

Kwoty przekazuje się wyłącznie w groszach, jako liczby całkowite (unit_net_cents: 30000 odpowiada 300,00 zł).

⚠ Ułamek kończy się kodem 422 — "unit_net_cents": 300.50 jest odbijane, a nie zaokrąglane. W kwocie podatkowej nie ma czegoś takiego jak pół grosza, a ułamek zwykle znaczy, że po drugiej stronie ktoś liczy w złotych: 300.50 wzięte za grosze dałoby fakturę na 3,01 zł zamiast 300,50 zł.

⚠ Liczby zmiennoprzecinkowe w JSON-ie podlegają zaokrągleniom binarnym (300.00 bywa odczytane jako 299.99999), co prowadzi do rozbieżności groszowych między zamówieniem a fakturą. Dlatego grosze, a nie złote z przecinkiem — to nie jest wygoda, tylko jedyny zapis, który przechodzi przez JSON bez zmiany wartości.

Sumy dokumentu wylicza serwer na podstawie pozycji; wartości przekazane w żądaniu są pomijane. Jedynym wyjątkiem jest expected_total_gross_cents — ale i ono nie wchodzi do dokumentu: może wystawienie wyłącznie zatrzymać.

Rabaty

Rabat podaje się na pozycji, jako kwotę albo procent:

{
  "name": "Przegląd serwisowy urządzeń",
  "quantity": 2,
  "unit_net_cents": 30000,
  "vat_rate": "23",
  "discount_cents": 6000
}

albo

{ "discount_percent": 10 }

⚠ Pola wykluczają się wzajemnie — podanie obu kończy się kodem 422.

unit_net_cents pozostaje ceną katalogową. Rabat pomniejsza wartość pozycji, a VAT jest liczony od kwoty po obniżce.

Dla przykładu powyżej: 2 × 300,00 = 600,00, rabat 60,00, wartość netto 540,00, VAT 124,20.

Rabat procentowy jest przeliczany na kwotę w chwili wystawienia i tak zapisywany. Na dokumencie widnieje jako osobna informacja przy pozycji, a do KSeF trafia w polu P_10 schematu FA(3).

⚠ Rabat nie może przekraczać wartości pozycji. Rabat od całości dokumentu nie jest obsługiwany — należy rozłożyć go na pozycje.

Płatność

Termin i forma płatności trafiają na fakturę i do KSeF:

{
  "due_date": "2026-09-17",
  "payment_method": "transfer"
}
wartość forma
cash gotówka
card karta
voucher bon
cheque czek
credit kredyt kupiecki
transfer przelew
mobile płatność mobilna

Oba pola są opcjonalne. Bez payment_method dokument nie deklaruje formy — nie jest podstawiana żadna wartość domyślna. Wartość spoza tabeli kończy się kodem 422.

⚠ Przy cash, card, voucher, cheque i mobile numer rachunku nie pojawia się ani na PDF-ie, ani w KSeF: na dokumencie zapłaconym przy kasie sugerowałby, że jest jeszcze coś do przelania.

Kody GTU

Trzynaście grup towarów i usług, które trzeba oznaczyć w ewidencji VAT. Znacznik podaje się na pozycji:

{
  "name": "Laptop",
  "quantity": 1,
  "unit_net_cents": 400000,
  "vat_rate": "23",
  "gtu": "GTU_06"
}
kod grupa
GTU_01 napoje alkoholowe
GTU_02 paliwa silnikowe
GTU_03 oleje opałowe i smarowe
GTU_04 wyroby tytoniowe i płyny do e-papierosów
GTU_05 odpady
GTU_06 elektronika i części do niej
GTU_07 pojazdy i części samochodowe
GTU_08 metale szlachetne i nieszlachetne
GTU_09 leki i wyroby medyczne
GTU_10 budynki, budowle i grunty
GTU_11 uprawnienia do emisji gazów cieplarnianych
GTU_12 usługi niematerialne (doradcze, księgowe, prawne, marketingowe)
GTU_13 usługi transportowe i magazynowe

Przyjmowane są też zapisy GTU06, gtu 6 i 6. Kod spoza tabeli kończy się kodem 422 — brak znacznika tam, gdzie jest wymagany, to błąd w JPK_V7, więc literówki nie pomijamy po cichu.

⚠ Znacznik dotyczy pojedynczej pozycji, nie całego dokumentu. Faktura może mieć pozycje z różnymi kodami i bez żadnego.

Pole classification (PKWiU) odpowiada na inne pytanie — czym jest towar — i obie wartości mogą stać na tej samej pozycji.

Znaczniki dla ewidencji VAT

Pola, których nie ma na wydruku, a które trafiają do KSeF i do JPK_V7. Wszystkie są opcjonalne i domyślnie wyłączone.

Na dokumencie:

pole znaczenie
split_payment mechanizm podzielonej płatności (MPP)
for_receipt faktura wystawiona do paragonu (art. 109 ust. 3d)
related_party sprzedaż podmiotowi powiązanemu

Na pozycji:

pole znaczenie
annex_15 towar lub usługa z załącznika nr 15 do ustawy
procedure oznaczenie procedury — tabela niżej
{
  "split_payment": true,
  "lines": [
    { "name": "Dysk SSD", "quantity": 1, "unit_net_cents": 40000, "vat_rate": "23", "annex_15": true }
  ]
}

⚠ Podzielona płatność jest obowiązkowa przy sprzedaży towarów z załącznika nr 15, gdy należność przekracza 15 000 zł. Brak oznaczenia to sankcja w VAT po stronie sprzedawcy. Nie ustawiamy tego za Ciebie — split_payment musi przyjść z żądania.

⚠ Ale też nie pozwolimy wystawić dokumentu bez niej. Są dwa przypadki, w których POST /faktury kończy się kodem 422:

Faktury przyjętej przez KSeF nie da się anulować, a brakującego oznaczenia nie poprawia się inaczej niż korektą. Dlatego wolimy odbić żądanie, niż wydać dokument, który trzeba będzie prostować.

procedure znaczenie
WSTO_EE sprzedaż na odległość do konsumenta z UE, także usługi elektroniczne
IED dostawa przez platformę internetową (art. 7a)
TT_D transakcja trójstronna uproszczona
I_42, I_63 import w procedurze celnej 42 / 63
B_SPV transfer bonu jednego przeznaczenia
B_SPV_DOSTAWA dostawa objęta bonem jednego przeznaczenia
B_MPV_PROWIZJA pośrednictwo przy bonie różnego przeznaczenia

Wartość spoza tabeli kończy się kodem 422. procedure odpowiada na inne pytanie niż gtu: GTU mówi, co sprzedano, procedura — w jakim trybie. Pozycja może mieć oba naraz.

Odpowiedź

201 Created

{
  "data": {
    "id": 128,
    "number": "FV/17/09/2026",
    "external_id": "ZAM-2026-0042",
    "platnosc_url": "https://platnosci.twojsklep.pl/z/9f3a2b7c1e",
    "type": "vat",
    "status": "issued",
    "issue_date": "2026-09-03",
    "sale_date": "2026-09-03",
    "due_date": "2026-09-17",
    "payment_method": "transfer",
    "price_mode": "net",
    "vat_exemption_basis": null,
    "vat_exemption_kind": "ustawa",
    "cash_method": false,
    "paid_on": null,
    "buyer": { "name": "Przykładowa Firma Sp. z o.o.", "tax_id": "7771002503", "eu_country": null, "eu_vat_id": null, "email": null },
    "total_net_cents": 60000,
    "total_vat_cents": 13800,
    "total_gross_cents": 73800,
    "vat_summary": [
      { "vat_rate": "23", "net_cents": 60000, "vat_cents": 13800, "gross_cents": 73800 }
    ],
    "currency": "PLN",
    "pdf_url": "https://app.obieq.pl/api/v1/faktury/128/pdf",
    "ksef": { "required": true, "number": null },
    "corrects": null,
    "from_proforma": null,
    "lines": [
      {
        "position": 1,
        "name": "Przegląd serwisowy urządzeń",
        "quantity": 2,
        "unit": "usł.",
        "unit_net_cents": 30000,
        "unit_gross_cents": 36900,
        "discount_cents": 0,
        "vat_rate": "23",
        "classification": null,
        "gtu": null,
        "net_cents": 60000,
        "vat_cents": 13800,
        "gross_cents": 73800,
        "before_correction": false
      }
    ],
    "delivery": {
      "sent_by": "client_system",
      "send_it_yourself": true,
      "sent_at": null,
      "sent_to": null
    }
  }
}

⚠ Kwoty dostajesz w komplecie — nie dolicza się ich u siebie

Odpowiedź niesie wszystkie wartości netto, VAT i brutto: dla każdej pozycji, w rozbiciu na stawki i dla całego dokumentu. Możesz je wprost pobrać do swojego systemu.

gdzie pola
pozycja unit_net_cents, unit_gross_cents, net_cents, vat_cents, gross_cents
wg stawek vat_summary[]: vat_rate, net_cents, vat_cents, gross_cents
dokument total_net_cents, total_vat_cents, total_gross_cents

⚠ Nie licz tych kwot ponownie u siebie. Podatek wyliczamy na grupach stawek, a nie przez sumowanie pozycji — te dwie metody potrafią różnić się o grosz (trzy pozycje po 0,01 zł przy 23% dają 0,00 zł z pozycji i 0,01 zł z grupy). Kto policzy rozbicie po swojemu, wpisze do ewidencji co innego, niż mówi faktura, która poszła do KSeF.

⚠ vat_summary to właśnie to rozbicie, którego wymaga ewidencja VAT. Na fakturze zaliczkowej niesie rozbicie zaliczki, bo takie są pola P_13 i P_14 tego dokumentu.

⚠ Obie ceny jednostkowe są zawsze wypełnione, niezależnie od price_mode — jedną podałeś, drugą wyliczyliśmy. Którą podałeś, mówi price_mode.

⚠ Pole delivery.send_it_yourself określa, czy wiadomość do nabywcy wysyła Twój system, czy OBIEQ. Zignorowanie go prowadzi do braku wysyłki albo do podwójnej wysyłki od dwóch różnych nadawców.

⚠ ksef.number jest początkowo null — wystawienie nie czeka na odpowiedź KSeF. Numer pojawia się zwykle w ciągu kilkunastu sekund; odczytasz go przez GET /api/v1/faktury/{id}. Nie blokuj na nim sprzedaży.


Odczyt

metoda adres opis
GET /api/v1/faktury lista, ?per_page= do 100
GET /api/v1/faktury/{id} dokument z pozycjami
GET /api/v1/faktury/{id}/pdf plik PDF
POST /api/v1/faktury/{id}/korekta korekta dokumentu
POST /api/v1/faktury/{id}/platnosci wpłata po wystawieniu — patrz Płatności
POST /api/v1/faktury/{id}/faktura faktura do proformy — patrz Faktura do proformy
POST /api/v1/faktury/{id}/rozliczenie rozliczenie zaliczki — patrz Faktura rozliczeniowa

Lista zwraca skrócone dokumenty (bez pozycji) i licznik stron:

{
  "data": [ … ],
  "meta": { "page": 1, "pages": 3, "total": 68 }
}

Wartości pola status:

status znaczenie
draft szkic — numer jeszcze nie nadany
issued wystawiony, numer nadany
cancelled anulowany — możliwe wyłącznie przed wysłaniem do KSeF

Pole type mówi, czym dokument jest: vat (faktura), proforma, correction (korekta), zaliczkowa, rozliczeniowa, uproszczona. Korekta wskazuje dokument pierwotny w corrects.

Faktura wystawiona z proformy niesie jej numer w from_proforma. ⚠ To co innego niż corrects: korekta zmienia wskazany dokument i jedzie do KSeF, a proforma jest tylko tym, z czego faktura powstała — rejestr o niej nie wie.

Numer KSeF pojawia się w ksef.number, zwykle w ciągu kilkunastu sekund od wystawienia. Wysyłką, ponawianiem i terminami zarządza OBIEQ.

⚠ Nie każdy dokument idzie do KSeF. Proforma nigdy (nie jest fakturą w rozumieniu ustawy o VAT), a faktura dla konsumenta — tylko gdy firma tak zdecydowała w panelu. Rozpoznasz to po ksef.required; co dokładnie o tym przesądza, opisuje ksef.required.


Faktury zakupowe

Faktury wystawione Twojej firmie, pobrane z KSeF. OBIEQ ciągnie je sam, co godzinę.

GET /api/v1/zakupy?unpaid=1&updated_since=2026-09-06T08:00:00Z
Authorization: Bearer obq_klucz
{
  "data": [
    {
      "id": 12,
      "ksef_number": "7771002503-20260902-0A1B2C000000-7F",
      "invoice_number": "FV/128/2026",
      "seller": { "name": "Hurtownia Sp. z o.o.", "tax_id": "7771002503" },
      "issue_date": "2026-09-01",
      "due_date": "2026-09-15",
      "net_cents": 100000,
      "vat_cents": 23000,
      "gross_cents": 123000,
      "currency": "PLN",
      "paid_cents": 0,
      "outstanding_cents": 123000,
      "has_content": true,
      "updated_at": "2026-09-06T09:12:00+02:00"
    }
  ],
  "meta": { "page": 1, "pages": 1, "total": 1 }
}

⚠ Nie ma POST /zakupy i nie będzie. To cudze dokumenty. Faktura zakupowa wpisana ręcznie byłaby kopią przepisaną z PDF-a — czyli dokładnie tym, przed czym chroni pobieranie ich z rejestru.

⚠ has_content — dokument dochodzi PÓŹNIEJ niż wpis

Faktura powstaje u nas z metadanych (jedno zapytanie na całą stronę wyników), a jej XML pobieramy osobnym żądaniem na każdy dokument. Przez chwilę — a przy pierwszym pobraniu przez kilka godzin — wpis istnieje bez treści i bez terminu płatności, bo termin bierze się z dokumentu.

Dlatego:

Pola i filtry

filtr znaczenie
seller_nip NIP dostawcy
issued_from, issued_to zakres dat wystawienia
unpaid tylko niezapłacone
updated_since co się zmieniło od Twojego ostatniego przebiegu

⚠ outstanding_cents bywa UJEMNE — to nadpłata do odzyskania od dostawcy, a nie błąd.

Pole project mówi, na koszt którego sklepu idzie zakup (code i name) — albo jest null, co znaczy koszt ogólny firmy, a nie „jeszcze nieprzypisany". Przypisuje je człowiek w panelu: KSeF pobiera zakupy dla całego NIP-u i nie niesie informacji o tym, którego sklepu dotyczy koszt. ⚠ Klucz sklepu widzi wyłącznie zakupy swojego sklepu — kosztów wspólnych nie widzi żaden z nich.

⚠ Kwoty pochodzą z metadanych KSeF (amounts_source: ksef_metadata), a nie z naszego czytania cudzego XML-a. Gdybyśmy wyciągali je sami, każdy błąd w odczycie obcego dokumentu dawałby fakturę z cichą złą kwotą.

Oryginalny dokument

GET /api/v1/zakupy/{id}/xml

Oddaje bajty, które dostaliśmy z KSeF — dokument sprzedawcy, nie nasza wizualizacja. Zanim treść dojdzie, zwraca 404.

Zapłata dostawcy

POST /api/v1/zakupy/{id}/platnosci
{ "amount_cents": 123000, "date": "2026-09-14", "note": "przelew" }

⚠ KSeF o tym nie wie i nie ma jak się dowiedzieć — rejestr nie prowadzi rozrachunków po żadnej ze stron. To Twój zapis o tym, ile poszło z konta.

⚠ Kwota może być UJEMNA — tak zapisuje się zwrot od dostawcy.

Te same zabezpieczenia, co przy wpłatach od nabywców: podejrzana zapłata wraca kodem 409 i polem code (nadplata, blizniacza_wplata, stan_sie_zmienil). Powtórz żądanie z "confirm": true, gdy kwota jest poprawna.


Korekta

POST /api/v1/faktury/{id}/korekta
Idempotency-Key: korekta-4821
{
  "reason": "Obniżka ceny po reklamacji",
  "lines": [
    { "name": "Przegląd serwisowy urządzeń", "quantity": 2, "unit_net_cents": 25000, "vat_rate": "23" }
  ]
}

⚠ Korekta faktury zaliczkowej idzie innym polem

Faktura zaliczkowa nie ma pozycji — opisuje zamówienie, a nie sprzedaż. Koryguje się na niej kwotę otrzymanej zaliczki, więc zamiast lines podaje się advance_gross_cents:

{
  "reason": "Zaliczka wpłynęła w niższej kwocie",
  "correction_type": "1",
  "advance_gross_cents": 30000
}

To ten sam adres (POST /faktury/{id}/korekta) — rozstrzyga typ dokumentu, który korygujesz. Osobny adres kazałby Ci najpierw sprawdzać, czym jest dokument, żeby wiedzieć, gdzie zapukać.

⚠ Rodzaj w KSeF wynika z tego, CO korygujesz, a nie z osobnego pola: KOR (faktura zwykła), KOR_ZAL (zaliczkowa), KOR_ROZ (rozliczeniowa). Ustawiamy go sami.

⚠ Zamówienie trafia na korektę dwa razy — przed korektą i po niej, nawet gdy się nie zmieniło. Tak każe schemat: „w celu potwierdzenia braku zmiany wartości danej pozycji". Nie trzeba tego podawać.

lines to stan docelowy — jak pozycje mają wyglądać po korekcie. Różnicę liczy serwer, tak samo jak podatek. Pusta lista znaczy „wycofujemy wszystko" i jest poprawnym dokumentem: tak wygląda zwrot całości.

pole wymagane opis
reason tak powód korekty, trafia na dokument i do KSeF
lines tak stan docelowy; pusta lista = zwrot całości
series nie kod serii korekt; bez niego seria domyślna
correction_type tak 1 — w dacie faktury pierwotnej, 2 — w dacie korekty, 3 — w innej
issue_date nie data wystawienia korekty
external_id nie Twój identyfikator — własny dla korekty, patrz external_id
platnosc_url nie adres do zapłaty; ⚠ ma sens tylko przy dopłacie — patrz Link do zapłaty

⚠ price_mode PRZY KOREKCIE NIE ISTNIEJE — tryb dziedziczy się po fakturze pierwotnej. Korygując dokument liczony od brutto, podajesz w pozycjach unit_gross_cents; podanie unit_net_cents kończy się wtedy 422 (i na odwrót). Nie jest to nadmierna surowość: korekta niczego niezmieniająca musi dać różnicę równą zeru, a wyjdzie tak wyłącznie wtedy, gdy oba dokumenty przechodzą przez tę samą arytmetykę. Patrz Ceny netto czy brutto.

⚠ correction_type jest OBOWIĄZKOWY i nie ma wartości domyślnej. Do 04.09.2026 przyjmowaliśmy jego brak i podstawialiśmy „1" — a to nie jest wartość domyślna, tylko rozstrzygnięcie, w którym okresie VAT korekta się rozlicza. Przy obniżce po reklamacji poprawną odpowiedzią jest zwykle 2.

⚠ Idempotency-Key jest wymagany tak samo jak przy wystawianiu. Korekta jest fakturą: dostaje numer i nie da się jej anulować. Ponowione żądanie bez klucza dałoby dwa dokumenty korygujące ten sam błąd.

⚠ Korektę wolno skorygować. Kolejna korekta liczy różnicę od stanu obowiązującego — czyli od tego, co ustaliła poprzednia — a nie od faktury pierwotnej. Podaj id ostatniego dokumentu w łańcuchu.

Korekta na plus (dopłata)

Korekta nie służy tylko do obniżek i zwrotów. Ten sam adres wystawia dopłatę: gdy faktura wyszła na za niską kwotę albo klient dokupił usługę rozliczaną na tym samym dokumencie, podajesz w lines wyższą cenę lub ilość. Osobnego pola ani osobnego trybu nie ma — kierunek korekty wynika ze znaku różnicy, którą liczy serwer.

Faktura za wizytę wyszła na 300,00 zł netto, a powinna na 350,00 zł:

POST /api/v1/faktury/4821/korekta
Idempotency-Key: korekta-doplata-4821
{
  "reason": "Dopłata za dodatkową godzinę",
  "correction_type": "2",
  "lines": [
    { "name": "Wizyta", "quantity": 1, "unit_net_cents": 35000, "vat_rate": "23" }
  ],
  "platnosc_url": "https://platnosci.example.pl/doplata/7"
}

W odpowiedzi (201) kwoty korekty są dodatnie — to różnica do dopłaty:

{
  "data": {
    "type": "correction",
    "corrects": "FV/12/10/2026",
    "total_net_cents": 5000,
    "total_gross_cents": 6150,
    "lines": [
      { "name": "Wizyta", "unit_net_cents": 30000, "before_correction": true },
      { "name": "Wizyta", "unit_net_cents": 35000, "before_correction": false }
    ]
  }
}

(Odpowiedź skrócona — pozostałe pola jak przy każdej fakturze.)

korekta na minus korekta na plus (dopłata)
total_net_cents, total_gross_cents ujemne dodatnie
kto komu płaci sprzedawca oddaje nabywcy nabywca dopłaca różnicę
platnosc_url i kod QR nigdy się nie pokazują pokazują się, dopóki dopłata nie jest zapłacona
correction_type zwykle 2 (obniżka po reklamacji) 1 — gdy kwotę zaniżono przez pomyłkę już na fakturze pierwotnej; 2 albo 3 — gdy przyczyna powstała po sprzedaży

⚠ correction_type przy dopłacie to też rozstrzygnięcie podatkowe. Korekta zwiększająca podstawę opodatkowania rozlicza się w okresie, w którym powstała jej przyczyna (art. 29a ust. 17 ustawy o VAT):

W razie wątpliwości zapytaj księgową. My tego za Ciebie nie zgadujemy.

⚠ Dopłata to osobny dokument z własnym numerem. Faktura pierwotna się nie zmienia, a do zapłaty jest kwota z korekty. Zapłatę dopisujesz do korekty, nie do faktury pierwotnej.

Jak wypełnić lines — zasady od księgowej

⚠ Podajesz STAN, a nie różnicę. To jest jedyna rzecz, którą tu łatwo pomylić: lines opisuje, jak pozycje mają wyglądać po korekcie. Różnicę liczy serwer i to ona trafia na dokument oraz do KSeF.

Wymieniasz WSZYSTKIE pozycje, nie tylko zmienione. Dokument korygujący pokazuje dwa komplety wierszy — „przed korektą" (odtwarzany przez nas z faktury pierwotnej) i „po korekcie" (to, co przyślesz). Pominięcie niezmienionej pozycji znaczy więc, że została wycofana.

co się stało co wpisujesz w quantity
klient zamówił 10, zwraca 3 7 — tyle ma zostać policzone
klient zwraca całą pozycję 0 — pozycja ZOSTAJE na dokumencie
pozycja bez zmian tyle, ile było

⚠ quantity: 0 to jedyny sposób na zwrot całej pozycji. Nie usuwaj jej z listy: wiersz ma zostać widoczny, żeby dało się zestawić korektę z fakturą pierwotną. Cenę jednostkową podajesz zawsze, także przy zerowej ilości — bez niej nie widać, czego zwrot dotyczy i za ile.

⚠ Rabat też trzeba powtórzyć. discount_cents / discount_percent działają w korekcie tak samo jak przy wystawianiu — patrz Rabaty. Pozycja przysłana bez rabatu, który był na fakturze pierwotnej, znaczy „opust zdejmujemy", a nie „opust bez zmian". Rabat wolno też dołożyć dopiero w korekcie — tak zapisuje się upust udzielony po sprzedaży.

Zwrot trzech świec z dziesięciu, przy zachowanym rabacie:

{
  "reason": "Zwrot towaru",
  "correction_type": "2",
  "lines": [
    {
      "name": "Świeca zapachowa",
      "quantity": 7,
      "unit_net_cents": 2000,
      "discount_cents": 5000,
      "vat_rate": "23"
    },
    {
      "name": "Świecznik",
      "quantity": 0,
      "unit_net_cents": 8000,
      "vat_rate": "23"
    }
  ]
}

Powyżej: świece zostają w liczbie 7, świecznik wraca w całości (quantity: 0), a rabat 50,00 zł obowiązuje dalej.


Dopisanie pozycji przy fakturze z proformy

Proforma idzie do nabywcy pierwsza i w chwili jej wystawiania nie wiadomo, jak on zapłaci. Przelewem — nic nie dochodzi. BLIKIEM albo kartą przez operatora — dochodzi prowizja, którą trzeba zafakturować.

Dlatego POST /faktury/{id}/faktura przyjmuje extra_lines:

{
  "type": "vat",
  "extra_lines": [
    {
      "name": "Opłata za płatność BLIK",
      "quantity": 1,
      "unit_net_cents": 200,
      "vat_rate": "23"
    }
  ]
}

⚠ Pozycje są DOPISYWANE, nie podmieniane. Pozycje z proformy idą na fakturę bez zmian i nie da się ich tędy poprawić — nabywca zapłacił na ich podstawie, więc faktura ma się z proformą zgadzać. Dopisane trafiają na koniec listy, żeby dokument czytał się jak proforma z dodatkiem.

Kształt pozycji jest ten sam co przy wystawianiu. Tryb cen dziedziczy się po proformie: przy dokumencie liczonym od brutto podajesz unit_gross_cents.

⚠ Nie na fakturze zaliczkowej (type: "zaliczkowa") — żądanie kończy się 422. Tam pozycje opisują zamówienie (art. 106f ust. 1 pkt 4), a kwota dokumentu bierze się z wpłaty; dopisana prowizja powiększyłaby więc wartość zamówienia, a nie kwotę do zapłaty. Prowizję dopisz na fakturze zwykłej albo rozliczeniowej — czyli na tej, która zamyka sprzedaż.


Sprawdzanie stanu w KSeF

Wystawienie nie czeka na odpowiedź Ministerstwa. Wynik odczytujesz później:

"ksef": {
  "required": true,
  "status": "accepted",
  "number": "5251004802-20260904-0100A1B2C3-D4",
  "sent_at": "2026-09-04T08:12:03+02:00",
  "accepted_at": "2026-09-04T08:12:19+02:00",
  "message": null,
  "final": true
}
status znaczenie odpytywać dalej?
pending czeka w kolejce tak
sent wysłana, czekamy na odpowiedź tak
accepted przyjęta, jest numer nie
rejected odrzucona, powód w message nie
duplicate dokument już był w rejestrze nie

⚠ Kieruj się polem final, a nie samym number. Dokument odrzucony też nie ma numeru KSeF — bez status wygląda identycznie jak taki, który dopiero czeka, i odpytywałbyś go bez końca.

Kiedy dokument nie idzie do KSeF

Zanim zaczniesz odpytywać o status, spójrz na ksef.required. Gdy jest false, dokument do rejestru nie trafi nigdy i numer KSeF nie przyjdzie — nie ma czego pilnować, a odpytywanie takiej faktury to pętla bez końca.

Są trzy przypadki, nie dwa:

dokument ksef.required
proforma false — nie jest fakturą w rozumieniu ustawy o VAT
faktura z buyer.tax_id albo buyer.eu_vat_id true — obowiązkowo
faktura bez numeru nabywcy (konsument) zależy od ustawienia firmy

Faktury konsumenckie idą do KSeF tylko wtedy, gdy firma tak zdecydowała

Ministerstwo Finansów mówi, że wystawianie faktur konsumenckich w KSeF jest dobrowolne — nie zakazane i nie obowiązkowe. To wybór podatnika, więc OBIEQ nie podejmuje go za niego.

Rozstrzyga ptaszek w panelu: Ustawienia firmy → „Wysyłaj do KSeF także faktury bez NIP-u nabywcy". ⚠ Domyślnie jest WYŁĄCZONY (niewysyłanie jest zawsze zgodne z prawem, skoro wysyłanie jest dobrowolne).

⚠ Nie ustawisz tego żądaniem i nie ma na to pola w API. Decyzja należy do właściciela firmy i jej księgowości, a nie do systemu, który wystawia faktury — gdyby dało się ją przestawić z integracji, sprowadzałaby się do tego, kto pierwszy wyśle żądanie.

⚠ To jest częsty przypadek, a nie wyjątek. U firm sprzedających detalicznie faktur bez NIP-u bywa WIĘCEJ niż tych z NIP-em. Przy wyłączonym ustawieniu ksef.required będzie false na większości dokumentów — i to jest poprawne zachowanie, a nie awaria. Jeśli spodziewasz się, że wszystko idzie do KSeF, sprawdź ten ptaszek u klienta, zanim zaczniesz szukać błędu u siebie.

Odpytywanie o zmiany

GET /api/v1/faktury?updated_since=2026-09-04T08%3A00%3A00%2B02%3A00

Zwraca dokumenty zmienione od podanej chwili, posortowane rosnąco po czasie zmiany. Zapamiętaj updated_at ostatniego z odpowiedzi i podaj go przy następnym przebiegu — dzięki temu nie przechodzisz całej historii za każdym razem.

⚠ Datę zakoduj w adresie (urlencode). Znak + ze strefy czasowej jest w adresie odczytywany jako spacja i żądanie kończy się kodem 422.

⚠ PODAWAJ STREFĘ CZASOWĄ

Czas bez przesunięcia czytamy jako warszawski — w tej strefie pracuje cała aplikacja i baza.

co wysyłasz jak to czytamy
2026-09-04T08:00:00+02:00 ✅ jednoznacznie
2026-09-04T06:00:00Z ✅ jednoznacznie
2026-09-04T08:00:00 ⚠ jako 08:00 czasu warszawskiego

Co się stanie, gdy pominiesz strefę — zależy od tego, w czym liczy Twój system:

⚠ Ten drugi przypadek to dokładnie ta awaria, przed którą ma bronić external_id: dokument istnieje, a Twój system go nie widzi. Podanie przesunięcia zamyka sprawę raz na zawsze.

⚠ Nakładaj okno na zapas. Zamiast pytać dokładnie od ostatniego updated_at, cofnij się o minutę: powtórzony dokument nic nie kosztuje (rozpoznasz go po id), a pominięty kosztuje fakturę, o której nie wiesz.

Pozostałe filtry

parametr wartości
status draft, issued, cancelled
ksef_status jak w tabeli wyżej
type vat, proforma, correction
issued_from, issued_to RRRR-MM-DD
updated_since RRRR-MM-DDTHH:MM:SS+02:00 — co się zmieniło od Twojego ostatniego przebiegu
external_id Twój identyfikator dokumentu — patrz external_id
per_page do 100

⚠ platnosc_url nie jest filtrem — po adresie płatności się nie szuka. W odpowiedzi wraca, w zapytaniu zostanie zignorowany.

Wartość spoza listy kończy się kodem 422 — filtr, którego nie rozumiemy, nie może po cichu oddać wszystkiego.


Limity

operacja limit
POST /faktury 60 / minutę i 600 / godzinę
odczyt 300 / minutę

Limity są liczone dla klucza, nie dla adresu IP.

⚠ KSeF przyjmuje 180 faktur na godzinę dla jednego NIP-u. Powyżej tego tempa dokumenty oczekują w kolejce niezależnie od limitów API.

Odpowiedź 429 oznacza przekroczenie chwilowego limitu. Żądanie można ponowić z tym samym Idempotency-Key.


Stan usługi

GET /api/v1/stan

Nie wymaga klucza. Odpowiada {"stan":"ok"} albo kodem 503.


Kody odpowiedzi

kod znaczenie
201 faktura wystawiona
400 brak nagłówka Idempotency-Key
401 nieprawidłowy lub odwołany klucz
409 konflikt klucza idempotencji
422 nieprawidłowe dane żądania — w tym niezgodna suma kontrolna i cena podana w polu niepasującym do trybu cen
429 przekroczony limit zapytań
503 usługa niedostępna

Opis błędu znajduje się w polu error — w każdej odpowiedzi błędnej, również przy błędzie walidacji. Błąd walidacji niesie dodatkowo errors z rozbiciem na pola:

{
  "error": "Podaj `price_mode`: `net`, jeśli ceny w pozycjach są netto…",
  "errors": { "price_mode": ["Podaj `price_mode`…"] }
}

Czegoś tu brakuje albo coś nie działa tak, jak napisano? Napisz na kontakt@obieq.pl — dokumentacja jest częścią produktu, więc jej błąd traktujemy jak każdy inny.