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ąć
- Zaloguj się na
app.obieq.pli wejdź w Klucze API. - 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). - Zaloguj się na
sandbox.obieq.pli tam w Klucze API wygeneruj klucz. Każdy klucz z tej instalacji zaczyna się odobq_test_i działa wyłącznie pod adresemhttps://sandbox.obieq.pl/api/v1. - Wystaw pierwszą fakturę na
sandbox.obieq.pl:POST /api/v1/faktury. - Po sprawdzeniu integracji wygeneruj klucz produkcyjny (
obq_…) w panelu naapp.obieq.pli przełącz się nahttps://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:
- klucz zaczyna się od
obq_test_— środowisko wynika z klucza, nie z treści żądania; - PDF ma napis „DOKUMENT TESTOWY" przez całą stronę;
- dokumenty nie trafiają do prawdziwego rejestru — idą do środowiska testowego Ministerstwa, a numer KSeF stamtąd nie ma mocy prawnej.
⚠ 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:
- dokumenty wystawione tym kluczem numerują się serią tego sklepu — bez
podawania
seriesw żądaniu; - lista dokumentów (
GET /faktury) pokazuje wyłącznie dokumenty tego sklepu; - lista zakupów (
GET /zakupy) pokazuje wyłącznie koszty przypisane do tego sklepu. ⚠ Zakup nieprzypisany to koszt ogólny firmy (czynsz, księgowość), a nie pozycja czekająca na przypisanie — nie widzi go żaden klucz sklepowy, więc suma kosztów ze wszystkich sklepów jest mniejsza niż koszty firmy. Przypisania dokonuje właściciel w panelu; KSeF pobiera zakupy per NIP i nie mówi, którego sklepu dotyczy koszt.
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.
Link do zapłaty
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" }
}
- Podmieniane są tylko pola podane i niepuste; pominięte zostają z proformy. Żeby poprawić nazwę, nie trzeba przepisywać adresu.
- ⚠ NIP-u nie da się zmienić.
buyer.tax_idjest opcjonalnym sprawdzeniem: jeśli go podasz, musi zgadzać się z NIP-em na proformie (myślniki, spacje i prefiksPLnie mają znaczenia). Inny NIP kończy się422— to inny podmiot i właściwa jest nowa proforma. - Numeru VAT UE (
eu_country,eu_vat_id) nie da się tu zmienić. - Proforma zostaje bez zmian — taka, jaką dostał nabywca.
⚠ 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
ZaliczkaCzesciowa— data otrzymania i kwota każdej zaliczki. ⚠ Data to dzień wpływu pieniędzy, a nie dzień wystawienia faktury zaliczkowej. Jeśli na zaliczce nie ma zapisanej wpłaty anipaid_on, żądanie kończy się422— nie podstawiamy daty dokumentu „bo zwykle się zgadza".FakturaZaliczkowa— numer każdej zaliczki: numer KSeF, gdy tam trafiła, albo numer własny ze znacznikiem „poza KSeF". Wymaga tego art. 106f ust. 3 — bez tego nabywca nie połączy wpłaty z rozliczeniem.
⚠ 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:
- dokument ma
"type": "proforma"i własny licznik (PF/1/09/2026), więc proforma nie zjada numeru faktury; - w odpowiedzi
ksef.requiredjestfalse, aksef.numberzostaje pusty — proforma nie jest fakturą w rozumieniu ustawy o VAT i do rejestru nie idzie. ⚠ Nie czekaj na numer KSeF, bo nigdy nie przyjdzie. ⚠ Proforma nie jest jedynym powodemrequired: false— patrzksef.required; - rozrachunki działa tak samo jak przy fakturze —
POST /faktury/{id}/platnosciprzyjmuje wpłaty na proformę, w całości i częściowo. To właśnie na proformie pilnuje się zapłaty, bo dopiero po niej wystawia się dokument sprzedaży.
⚠ 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:
- pozycja z
annex_15, kwota brutto powyżej 15 000 zł, nabywca z NIP-em i walutaPLN, asplit_paymentnie przyszło — to jest sytuacja, w której ustawa oznaczenia wymaga (art. 108a ust. 1a); split_payment: true, a firma nie ma w OBIEQ-u numeru rachunku — nabywca płaci MPP przelewem na rachunek sprzedawcy i nie ma innej drogi.
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:
has_content: falseznaczy „dokument jest w drodze", a nie „dostawca nie przysłał treści". Nie przestawaj odpytywać;updated_atrusza się drugi raz, gdy treść dojdzie — po to jestupdated_since.
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):
- pomyłka w cenie na fakturze pierwotnej — przyczyna istniała już przy
sprzedaży, więc
1(w okresie faktury pierwotnej); - przyczyna powstała po sprzedaży (np. klient zgodził się na wyższą stawkę
po wizycie) —
2, gdy powstała w miesiącu wystawienia korekty, albo3, gdy w innym.
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:
- liczysz w UTC (albo w strefie na zachód od Warszawy): okno zacznie się
wcześniej, niż chciałeś, więc dostaniesz kilka dokumentów za dużo.
Nieprzyjemne, ale bezpieczne — powtórkę rozpoznasz po
id; - liczysz w strefie na wschód od Warszawy (UTC+3 i dalej): okno zacznie się później — i cicho zgubisz dokumenty z tego przedziału. Nic się nie wywali, lista po prostu wróci krótsza.
⚠ 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.