Publiczne API mojaKsięgowa.ai — przewodnik dla integratorów
Wersja API: v1 · Adres: https://api.mojaksiegowa.ai/api/public/v1 · Referencja (Redoc): /api/public/v1/docs
API służy do wystawiania faktur sprzedażowych z systemów e-commerce (sklep, platforma, ERP) i odbierania zdarzeń o ich losie: numer KSeF, odrzucenie, zapłata. Wszystko, co robisz przez API, dzieje się w organizacji właściciela klucza dokładnie tak, jakby zrobił to w aplikacji: faktura dostaje numer z Twojej numeracji, trafia do KSeF, księguje się i wchodzi do JPK.
Ten przewodnik opisuje przebiegi i zasady. Pełną listę pól każdego żądania i odpowiedzi znajdziesz w Redoc.
1. Pierwsze wywołanie w 5 minut
Nie masz jeszcze konta? Nie musisz konfigurować całej firmy (KSeF, dokumenty z ZUS, ewidencje), żeby
testować API. Zarejestruj się w aplikacji i dodaj firmę, a na pierwszym ekranie jej konfiguracji kliknij
„Jestem integratorem — przetestuj API" (dolny pasek; na telefonie „Testuj API"). Po dwóch pytaniach
(status VAT, forma opodatkowania) dostaniesz od razu sandbox i klucz testowy mk_test_… ze wszystkimi
zakresami — przejdź wtedy do kroku 2. Jeśli firma nie ma adresu siedziby, uzupełnij go w Ustawienia →
Dane organizacji; bez niego faktury testowe dostaną 422 ORGANIZACJA_BEZ_ADRESU. Klucz produkcyjny
utworzysz po dokończeniu konfiguracji firmy.
- W aplikacji: Ustawienia → Integracje API → Utwórz klucz. Wybierz środowisko
piaskownica(kluczmk_test_…), zakresyfaktury:zapis,faktury:odczyt,klienci:zapis,webhooki:zarzadzanie. Sekret zobaczysz tylko raz — skopiuj go od razu. Klucz produkcyjny (mk_live_…) jest dostępny dopiero po ukończeniu konfiguracji firmy w aplikacji (409 ONBOARDING_NIEUKONCZONY); klucz testowy — od razu. - Sprawdź tożsamość klucza:
GET /api/public/v1/ja HTTP/1.1
Authorization: Bearer mk_test_…
{
"organizacja": {"id": "…", "nazwa": "Kwiaciarnia Róża (sandbox)", "nip": "5261040828"},
"srodowisko": "piaskownica",
"zakresy": ["faktury:zapis", "faktury:odczyt", "klienci:zapis", "webhooki:zarzadzanie"],
"klucz": {"nazwa": "Sklep — test", "prefiks": "mk_test_a1b2c3d4", "wygasa": null},
"ksef_polaczony": true,
"limity": {"odczyt_na_minute": 300, "zapis_na_minute": 120, "faktury_api_w_miesiacu": 0, "max_faktur_api_miesiac": 1000}
}
- Wystaw fakturę:
POST /api/public/v1/faktury HTTP/1.1
Authorization: Bearer mk_test_…
Content-Type: application/json
Idempotency-Key: zamowienie-10245-faktura
{
"zewnetrzny_id": "10245",
"numer_zamowienia": "ZAM/2026/10245",
"nabywca": {"nazwa": "Hurtownia Kwiatów sp. z o.o.", "nip": "5261040828", "ulica": "Polna", "numer_domu": "1", "kod_pocztowy": "00-001", "miasto": "Warszawa"},
"pozycje": [
{"nazwa": "Doniczka ceramiczna 20 cm", "ilosc": 2, "cena_jednostkowa_brutto": "30.00", "stawka_vat": "23"}
]
}
Odpowiedź 201 zawiera id, numer (z numeracji organizacji) i blok ksef ze statusem oczekuje.
Wysyłka do KSeF jest asynchroniczna — śledź ją przez GET /faktury/{id} albo webhook (rozdział 7).
- Przejdź na produkcję: utwórz klucz
produkcja(mk_live_…) i podmień sekret w konfiguracji. Kontrakt jest identyczny; różni się tylko to, że faktury naprawdę trafiają do KSeF.
2. Uwierzytelnianie i klucze
- Nagłówek
Authorization: Bearer <sekret>. Sekret w adresie (?api_key=…) jest odrzucany z401 SEKRET_W_ADRESIE— tokeny w URL lądują w logach każdego proxy po drodze. - Klucz należy do organizacji, nie do użytkownika. Ma zakresy (rozdział 2.1) i opcjonalną datę wygaśnięcia. Organizacja może mieć do 20 aktywnych kluczy.
- Rotacja (w aplikacji): nowy sekret obowiązuje od razu, stary działa jeszcze 24 h, żeby podmienić konfigurację bez przerwy. Odwołanie jest natychmiastowe i gasi oba sekrety.
- Błędy klucza:
401 KLUCZ_NIEPRAWIDLOWY,401 KLUCZ_ODWOLANY,401 KLUCZ_WYGASL,401 BRAK_KLUCZA. - Zapisy (
POST,PUT,PATCH,DELETE) wymagają aktywnej subskrypcji organizacji (403 SUBSKRYPCJA_NIEAKTYWNA) oraz — dla klucza produkcyjnego — modułu e-commerce (403 MODUL_NIEAKTYWNY; w planie Biznes moduł jest w cenie, w pozostałych planach to dodatek). Odczyty działają zawsze. Klucz testowy nie wymaga modułu.
2.1. Zakresy
| Zakres | Co obejmuje |
|---|---|
faktury:odczyt |
GET /faktury*, PDF, XML, UPO |
faktury:zapis |
wystawianie, szkice, płatności, e-mail, usuwanie szkicu |
klienci:odczyt / klienci:zapis |
kartoteka klientów |
produkty:odczyt / produkty:zapis |
katalog produktów |
webhooki:zarzadzanie |
webhooki i log doręczeń |
Brak zakresu → 403 BRAK_ZAKRESU z listą brakujących zakresów w blad.brakujace.
3. Koperta odpowiedzi i błędy
Sukces zwraca obiekt zasobu albo stronę listy (rozdział 4). Każdy błąd ma tę samą kopertę:
{
"blad": {
"kod": "WALIDACJA",
"komunikat": "Nieprawidłowe dane żądania.",
"pola": [{"pole": "pozycje[0].stawka_vat", "kod": "ENUM", "komunikat": "Input should be '23', '8', …"}]
}
}
kodjest stabilnym identyfikatorem — steruj logiką po nim, nie po treścikomunikat.polapojawia się przy błędach walidacji (ścieżka pola jak w JSON żądania).- Dodatkowe szczegóły lądują bezpośrednio w
blad, np.blad.idistniejącej faktury przy409 DUPLIKAT_ZEWNETRZNY_IDalboblad.retry_after_sprzy429. - Nieznane pola w żądaniu są odrzucane (
422 WALIDACJA) — literówka w nazwie pola nie przejdzie cicho.
Najczęstsze kody:
| Status | Kod | Kiedy |
|---|---|---|
| 401 | BRAK_KLUCZA, KLUCZ_NIEPRAWIDLOWY, KLUCZ_ODWOLANY, KLUCZ_WYGASL, SEKRET_W_ADRESIE |
uwierzytelnianie |
| 403 | BRAK_ZAKRESU, SUBSKRYPCJA_NIEAKTYWNA, MODUL_NIEAKTYWNY, LIMIT_FAKTUR_API |
uprawnienia i plan |
| 404 | NIE_ZNALEZIONO |
zasób nie istnieje albo należy do innej organizacji |
| 409 | DUPLIKAT_ZEWNETRZNY_ID, JUZ_WYSTAWIONA, NIE_MOZNA_USUNAC, FAKTURA_NIEWYSTAWIONA, FAKTURA_ANULOWANA, KOREKTA_W_TOKU, LIMIT_WEBHOOKOW, DOSTARCZENIE_W_TOKU |
konflikt stanu |
| 409 | IDEMPOTENCY_KEY_W_TOKU |
to samo żądanie właśnie się wykonuje — powtórz po Retry-After |
| 409 | ONBOARDING_NIEUKONCZONY |
zapis kluczem produkcyjnym, zanim firma ukończyła konfigurację w aplikacji — odczyty działają |
| 422 | WALIDACJA, KWOTA_ZA_DUZA, STAWKA_NIEZGODNA_Z_TYPEM, OSS_NIEOBSLUGIWANE, DATA_WSTECZNA, DATA_PRZYSZLA, WALUTA_NIEOBSLUGIWANA, KSEF_NIESKONFIGUROWANY, ORGANIZACJA_BEZ_ADRESU, IDEMPOTENCY_KEY_KONFLIKT, LIMIT_SANDBOX, NIC_DO_KOREKTY, KORYGUJ_FAKTURE_PIERWOTNA, KOREKTA_NIEDOSTEPNA, KOREKTA_BEZ_PLATNOSCI, KOREKTA_W_APLIKACJI, ODBIORCA_NIEPRAWIDLOWY |
dane albo stan organizacji |
| 429 | LIMIT_ZAPYTAN |
limit zapytań (rozdział 3.1) |
| 502 | NBP_NIEDOSTEPNY |
kurs waluty chwilowo niedostępny — powtórz |
| 500 | BLAD_SERWERA |
nasz błąd — powtórz z tym samym Idempotency-Key |
| 503 | KONFLIKT_WSPOLBIEZNOSCI |
chwilowy konflikt przy zapisie — powtórz po Retry-After z tym samym Idempotency-Key |
Część reguł walidacji ma własny kod pola zamiast kodu koperty: odpowiedź to 422 z blad.kod = WALIDACJA,
a szczegół siedzi w blad.pola[*].kod — NIEPRAWIDLOWY_NIP i KONSUMENT_Z_NIP (pole nip) oraz
URL_NIEDOZWOLONY (pole url webhooka), POZYCJA_NIE_ISTNIEJE, ZWROT_PONAD_ILOSC, CENA_PONAD_OBECNA,
CENA_BRUTTO_WYMAGANA i BRAK_ZMIANY (pozycje korekty).
Rozpoznawaj je po pola[*].kod, nie po blad.kod.
3.1. Limity zapytań
Limity liczone per klucz w oknie 60 s, osobno dla odczytów (GET) i zapisów: domyślnie 300 i 120 na minutę
(aktualne wartości w GET /ja). Każda odpowiedź niesie X-RateLimit-Limit, X-RateLimit-Remaining
i X-RateLimit-Reset (sekundy do końca okna); 429 dodaje Retry-After. Synchronizując wiele zamówień, wysyłaj
je sekwencyjnie albo z małą współbieżnością i honoruj Retry-After — nie jest to limit dzienny, po minucie okno się
odnawia.
4. Listy i paginacja
Listy (/klienci, /produkty, /faktury, /webhooki/{id}/dostarczenia) zwracają stronę:
{"dane": [{"id": "…"}, {"id": "…"}], "nastepny_kursor": "eyJ1…"}
Parametry: limit (1–100, domyślnie 50) i kursor — podaj nastepny_kursor z poprzedniej odpowiedzi; null oznacza
ostatnią stronę. Kolejność jest stabilna (po dacie utworzenia i id), więc iteracja nie gubi ani nie dubluje
rekordów dodanych w trakcie. Nie ma pola total — sklep i tak iteruje do końca. Uszkodzony kursor →
422 KURSOR_NIEPRAWIDLOWY.
Filtry GET /faktury: data_od, data_do (data wystawienia), status, status_ksef, status_platnosci, rodzaj,
klient_id, zewnetrzny_id, numer_zamowienia. Filtry GET /klienci: nip, zewnetrzny_id, szukaj,
tylko_aktywni.
5. Klienci i produkty
Możesz wystawiać faktury bez kartoteki (dane nabywcy wprost w fakturze), ale kartoteka daje stałe id klienta
i historię. Kluczem powiązania z Twoim systemem jest zewnetrzny_id — unikalny w organizacji:
PUT /klienci/zewnetrzne/{zewnetrzny_id}iPUT /produkty/zewnetrzne/{zewnetrzny_id}to upsert:201gdy rekord powstał,200gdy został zaktualizowany. Bezpieczny do wielokrotnego wywołania przy każdej synchronizacji.POSTz zajętymzewnetrzny_id→409 DUPLIKAT_ZEWNETRZNY_IDzblad.idistniejącego rekordu.- Klient:
rodzajfirma|konsument(domyślnie firma, gdy podanonipalbonumer_vat_ue). Polski NIP jest sprawdzany sumą kontrolną, separatory i prefiksPLsą ignorowane;numer_vat_uetylko dla kraju ≠ PL. Konsument z NIP-em →422 WALIDACJAzpola[*].kod = KONSUMENT_Z_NIP, błędny NIP →NIEPRAWIDLOWY_NIP. - Produkt:
rodzajtowar|usluga|material(wpływa na typ transakcji zagranicznej),stawka_vat(23,8,5,0,zw,np,oo),cena_netto, opcjonalniekod_produktu(SKU widoczny na fakturze),kod_gtu,kod_pkwiu,kod_cn. DELETEdezaktywuje rekord (zostaje w historii faktur); pozycja ze zdezaktywowanym produktem →422 PRODUKT_NIEAKTYWNY.
6. Faktury
6.1. Wystawienie
POST /faktury działa synchronicznie do momentu nadania numeru i zapisu (201 z pełną fakturą), a wysyłka do
KSeF jest asynchroniczna. Co trzeba wiedzieć:
- Nabywca:
{"klient_id": "…"}lub{"zewnetrzny_id": "…"}(z kartoteki) albo dane wprost (nazwa,nip, adres;krajw v1 tylkoPLdla nabywcy wprost — zagraniczny nabywca musi być w kartotece). - Odbiorcy (opcjonalnie,
odbiorcy[]): odbiorca inny niż nabywca — np. szkoła, gdy nabywcą jest gmina, albo oddział firmy nabywcy. Szczegóły w rozdziale 6.7. - Pozycje: z katalogu (
produkt_id/produkt_zewnetrzny_id, ilość, opcjonalnie inna cena) albo wprost (nazwa,ilosc,cena_jednostkowa_nettolubcena_jednostkowa_brutto,stawka_vat). Cena brutto jest domyślnie przeliczana na netto z 4 miejscami, a sumy faktury liczone od netto, tak jak w aplikacji — suma może różnić się od kwoty zamówienia o grosz. - Faktura liczona od brutto (
"liczona_od_brutto": true, opcjonalnie): pozycja zcena_jednostkowa_bruttojest liczona od brutto (art. 106e ust. 7–8) — wartość brutto pozycji to dokładnie ilość × cena brutto (po rabacie), VAT jest kwotą zawartą w brutto, a netto różnicą, więc suma faktury zgadza się z kwotą zamówienia co do grosza. W KSeF taka pozycja ma cenę i wartość brutto (P_9B, P_11A); w odpowiedzi ma wypełnionecena_jednostkowa_brutto(acena_jednostkowa_nettowyliczoną z brutto). - Stawka
zw: faktura z pozycją zwolnioną z VAT musi wskazać podstawę prawną zwolnienia (podstawa_zwolnienia_vat, np. „art. 43 ust. 1 pkt 19 ustawy o VAT”) — trafia na fakturę i do KSeF. Sprzedawcy zwolnionemu ze względu na limit sprzedaży (ustawienie w aplikacji) wpisujemy ją sami: „art. 113 ust. 1 ustawy o VAT”, w roku rozpoczęcia działalności — ust. 9. W pozostałych przypadkach brak →422 BRAK_PODSTAWY_ZWOLNIENIAprzed zapisem (szkicwystaw: falsezapisze się, alePOST /faktury/{id}/wystawodpowie tym samym kodem). Korekta przejmuje podstawę z faktury korygowanej. - Daty:
data_wystawieniadomyślnie dziś (czas polski); data wsteczna →422 DATA_WSTECZNA.termin_platnoscidomyślnie z terminu klienta (14 dni). - Waluta: ISO 4217; obca waluta dostaje kurs NBP z dnia poprzedzającego (
502 NBP_NIEDOSTEPNY= powtórz). - Mechanizm podzielonej płatności: pozycja może nieść
zalacznik_15— towar lub usługa z załącznika nr 15 do ustawy o VAT (stal, elektronika, części samochodowe, roboty budowlane…). Pominięte ustalamy zpkwiupozycji albo produktu, ale tylko wtedy, gdy symbol jednoznacznie należy do załącznika; grupy oznaczone w ustawie „ex” i pozycje bez symbolu PKWiU (np. paliwa, złoto inwestycyjne) wskaż sam. Faktura dla firmy powyżej 15 000 zł brutto (w walucie obcej — po przeliczeniu kursem faktury) z taką pozycją w stawce z VAT dostaje oznaczenie „mechanizm podzielonej płatności” (w odpowiedzimechanizm_podzielonej_platnosci: true), a pozycje — znacznik w KSeF. - Typ transakcji (krajowa / WDT / eksport) ustalamy z kraju nabywcy i rodzaju pozycji. Stawka niezgodna z typem
(np. 23 % dla firmy z DE) →
422 STAWKA_NIEZGODNA_Z_TYPEM. Sprzedaż osobie prywatnej z innego kraju UE do progu 42 000 zł rocznie ma polski VAT; po przekroczeniu progu (procedura OSS) →422 OSS_NIEOBSLUGIWANE. Zwykle przed zapisem faktury — jeśli próg przekroczy inna faktura wystawiona w tej samej chwili, błąd przyjdzie przy wystawieniu, a faktura zostanie szkicem. wystaw: falsezostawia szkic bez numeru i bez KSeF; wystawisz go później przezPOST /faktury/{id}/wystaw. Szkic można też usunąć (DELETE), wystawionej faktury nie (409 NIE_MOZNA_USUNAC).rodzaj: "proforma"wystawia proformę (numeracjaPRO/…, bez KSeF, blokksef=null).ksef:auto(domyślnie) iwyslijznaczą to samo — dokument idzie do KSeF.pominjest na razie niedostępne (422): w v1 każda faktura, także dla konsumenta, trafia do KSeF.- Zanim cokolwiek zapiszemy, sprawdzamy gotowość organizacji: brak adresu firmy →
422 ORGANIZACJA_BEZ_ADRESU, brak połączenia z KSeF →422 KSEF_NIESKONFIGUROWANY(poleksef_polaczonywGET /ja). Właściciel naprawia to w aplikacji, integracja nie ma jak.
6.2. Cykl życia i blok ksef
status faktury opisuje dokument w księgowości: szkic → wystawiona → wyslana → zaksiegowana → zadeklarowana,
plus anulowana i wymaga_decyzji (wstrzymana do decyzji właściciela). Dla integracji ważniejszy jest blok
ksef:
ksef.status |
Znaczenie | Co dalej |
|---|---|---|
szkic |
dokument nie został jeszcze wystawiony | POST /faktury/{id}/wystaw |
oczekuje |
w kolejce do KSeF albo po błędzie tymczasowym (nastepna_proba) |
czekaj; automat ponawia |
przyjeta |
KSeF nadał numer: numer, link_weryfikacyjny |
zamknij zamówienie, pobierz PDF |
odrzucona |
błąd permanentny (bledy), dokument wrócił do szkicu |
popraw dane w aplikacji, wystaw ponownie — numer zostaje |
anulowana |
faktura anulowana w aplikacji | — |
Błędy tymczasowe (KSeF niedostępny) ponawiamy automatycznie — nie ponawiaj POST /faktury samodzielnie.
Ponowne POST /faktury/{id}/wystaw na dokumencie w kolejce → 409 JUZ_WYSTAWIONA.
6.3. Idempotencja
Każde żądanie zapisu może nieść nagłówek Idempotency-Key (do 255 znaków, np. zamowienie-10245-faktura).
Semantyka jak w Stripe, w oknie 24 h:
- ta sama treść → odtworzona odpowiedź z nagłówkiem
Idempotent-Replayed: true, bez ponownego wykonania, - inna treść pod tym samym kluczem →
422 IDEMPOTENCY_KEY_KONFLIKT, - żądanie jeszcze w toku →
409 IDEMPOTENCY_KEY_W_TOKUzRetry-After(także przez chwilę po zapisie, dopóki pierwotne żądanie nie skończy budować odpowiedzi), - błąd
5xxpo drodze (także503 KONFLIKT_WSPOLBIEZNOSCI— odczekajRetry-After) → powtórz z tym samym kluczem; jeśli faktura zdążyła powstać, dostaniesz ją, nie duplikat, a jeśli nie zdążyła zostać wystawiona, powtórzenie dokończy wystawienie. Gdy wystawienia nie da się teraz dokończyć (np. organizacja straciła połączenie z KSeF), powtórzenie oddaje fakturę jako szkic (status: szkic) — wystaw ją przezPOST /faktury/{id}/wystaw, który zwróci właściwy błąd.
Dodatkowo zewnetrzny_id faktury jest unikalny w organizacji (409 DUPLIKAT_ZEWNETRZNY_ID z blad.id), więc nawet
bez Idempotency-Key zamówienie nie dostanie dwóch faktur. Zalecamy oba mechanizmy: nagłówek chroni retry,
zewnetrzny_id chroni przed logiką sklepu.
6.4. Płatności
POST /faktury/{id}/platnosci z opcjonalnymi kwota (domyślnie cała reszta) i data_zaplaty (domyślnie dziś).
Częściowa wpłata → status_platnosci: czesciowo_oplacone, pełna → oplacone i zdarzenie faktura.oplacona.
Endpoint honoruje Idempotency-Key — retry nie doliczy wpłaty drugi raz. Faktura może być też opłacona w aplikacji
(np. dopasowanie z wyciągu bankowego) — wtedy dowiesz się o tym z webhooka.
6.5. PDF, XML, UPO, e-mail
GET /faktury/{id}/pdf— PDF z kodem QR KSeF. Faktura (krajowa i zagraniczna) dostaje PDF dopiero po przyjęciu przez KSeF (409 FAKTURA_NIEWYSTAWIONAwcześniej — stan pokazujeksef.status); proforma — zawsze, także szkic. Anulowanej faktury ani proformy nie da się pobrać ani wysłać (409 FAKTURA_ANULOWANA).GET /faktury/{id}/xml— dokładnie ten XML FA(3), który poszedł do KSeF (409 BRAK_XMLprzed wysyłką);GET /faktury/{id}/upo— urzędowe poświadczenie odbioru (409 BRAK_UPOzanim jest).POST /faktury/{id}/wyslij-email— wysyła PDF naemail(domyślnie e-mail nabywcy z kartoteki,422 BRAK_ADRESU_EMAILgdy go nie ma), opcjonalniejezyk. Odpowiedź nabywcy trafia do firmy — na adres ustawiony w aplikacji (domyślnie e-mail właściciela). W sandboxie i na koncie w trybie testowym nic nie wychodzi (200,message_idzaczyna się odsandbox-).
6.6. Korekty (zwroty, reklamacje, rabaty)
POST /faktury/{id}/korekty wystawia fakturę korygującą do faktury przyjętej w KSeF i wysyła ją do KSeF tak samo jak
fakturę. Korekta z API to zawsze korekta „nowe okoliczności” — ujmowana w okresie jej wystawienia, nie wstecz.
Korekty błędów pierwotnych (np. zła stawka na fakturze) wystawia właściciel w aplikacji.
POST /api/public/v1/faktury/{id}/korekty HTTP/1.1
Authorization: Bearer mk_test_…
Content-Type: application/json
Idempotency-Key: rma-311-korekta
{
"zewnetrzny_id": "RMA-311",
"przyczyna": "Zwrot towaru przez klienta",
"powod": "zwrot_towaru",
"pozycje": [{"lp": 1, "ilosc_zwrocona": 1}]
}
pozycjewskazują pozycje faktury polp(jak wpozycje[].lpodpowiedzi) i zmianę:ilosc_zwrocona(zwrot), nową cenę jednostkowącena_jednostkowa_brutto_poalbocena_jednostkowa_netto_po(reklamacja, rabat) albo oba naraz. Cena brutto jest przeliczana na netto z 4 miejscami, jak przy wystawianiu faktury; pozycja faktury liczonej od brutto (liczona_od_brutto) przyjmuje tylkocena_jednostkowa_brutto_po— wprost (cena_jednostkowa_netto_po→CENA_BRUTTO_WYMAGANA). Cena to cena przed rabatem procentowym pozycji. Korekta z API może cenę tylko obniżyć (CENA_PONAD_OBECNA); cena bez zmiany dajeBRAK_ZMIANY. Pozycje niewymienione zostają bez zmian."pelna": truezamiastpozycjekoryguje całą fakturę do zera.
{"przyczyna": "Uszkodzone opakowanie", "powod": "reklamacja",
"pozycje": [{"lp": 2, "cena_jednostkowa_brutto_po": "20.00"}]}
powod:zwrot_towaru(domyślnie),reklamacja,rabat,inny;przyczynatrafia na dokument i do KSeF.- Kolejne korekty liczą się od stanu po poprzednich (ilości i ceny): po zwrocie 1 z 2 sztuk można zwrócić jeszcze najwyżej jedną
(
422zpola[*].kod = ZWROT_PONAD_ILOSC). Podawaj zawszeidfaktury pierwotnej —idkorekty daje422 KORYGUJ_FAKTURE_PIERWOTNAzblad.idfaktury pierwotnej. - Odpowiedź
201to dokument zrodzaj: "korekta",faktura_korygowana_id,numer_faktury_korygowanejiprzyczyna_korekty. Pozycje i kwoty korekty to różnice: zwrot daje ujemną ilość i ujemne wartości, acena_jednostkowa_nettoto cena po korekcie. Ujemnepozostalo_do_zaplatyto kwota do zwrotu nabywcy. - Faktura bez numeru KSeF →
409 FAKTURA_NIEWYSTAWIONA(poczekaj naksef.status = przyjeta). Korekta odrzucona przez KSeF zostaje szkicem i blokuje kolejną (409 KOREKTA_W_TOKU) — właściciel poprawia ją w aplikacji. - Korekta nie przyjmuje wpłat (
422 KOREKTA_BEZ_PLATNOSCI): zwrot pieniędzy rozliczasz w swoim systemie płatności. - Korekta przejmuje odbiorców (
odbiorcy[], rozdział 6.7) z faktury korygowanej — szkoła z faktury dla gminy dostaje w KSeF także korektę. - Webhooki korekty to te same zdarzenia co faktury (
faktura.wystawiona,faktura.ksef.przyjeta…) zidkorekty irodzaj: "korekta"wdane. Listę korekt dajeGET /faktury?rodzaj=korekta.
6.7. Odbiorca inny niż nabywca (szkoła, gmina, oddział)
Nabywca to strona transakcji, która rozlicza VAT; odbiorca to podmiot, dla którego faktycznie jest dostawa albo usługa, ale który sam nabywcą nie jest. Typowy przypadek: samorządy rozliczają VAT centralnie, więc przy zakupie dla szkoły nabywcą jest gmina (jej NIP), a szkoła jest odbiorcą — z własną nazwą, adresem i NIP-em.
{
"nabywca": {"nazwa": "Gmina Przykładowo", "nip": "5261040828", "ulica": "Rynek", "numer_domu": "1",
"kod_pocztowy": "00-950", "miasto": "Przykładowo"},
"odbiorcy": [{"rola": "jst_odbiorca", "nazwa": "Szkoła Podstawowa nr 1 w Przykładowie",
"id_wew": "5261040828-00001", "ulica": "Szkolna", "numer_domu": "5",
"kod_pocztowy": "00-951", "miasto": "Przykładowo"}],
"pozycje": [{"nazwa": "Tablica interaktywna", "ilosc": 1, "cena_jednostkowa_netto": "4999.00", "stawka_vat": "23"}]
}
rola:jst_odbiorca— jednostka podrzędna samorządu (szkoła, przedszkole, ośrodek pomocy społecznej); nabywcą jest gmina, powiat albo województwo,gv_odbiorca— członek grupy VAT, gdy nabywcą jest grupa VAT,odbiorca— oddział albo jednostka wewnętrzna nabywcy, która sama nie jest nabywcą.
- Odbiorca z rolą
jst_odbiorca/gv_odbiorcapotrzebuje NIP alboid_wew(identyfikatora wewnętrznego z KSeF w formacieNIP-xxxxx— NIP nabywcy i 5 cyfr nadanych jednostce, gdy nie ma ona własnego NIP), a nabywca — polskiego NIP. Faktura dostaje wtedy w KSeF oznaczenie JST albo grupy VAT u nabywcy, a odbiorca z NIP /id_wewwidzi ją w KSeF. NIP iid_wewnaraz →422 WALIDACJA. id_wewweź od nabywcy — nadaje go swojej jednostce w KSeF. KSeF przyjmuje fakturę bez sprawdzania, czy taki identyfikator istnieje, więc pomyłka nie da błędu: faktura zostanie przyjęta, ale jednostka jej nie zobaczy.- Najwyżej 10 odbiorców; jednostka JST i członek grupy VAT nie mogą wystąpić na jednej fakturze. Odbiorcy niezgodni
z nabywcą (rola JST bez polskiego NIP nabywcy,
id_wewspoza NIP nabywcy) →422 ODBIORCA_NIEPRAWIDLOWY. - Odbiorcy wracają w odpowiedzi jako
odbiorcy[](adres w jednej linii) i są na PDF faktury.
7. Webhooki
Zamiast odpytywać GET /faktury/{id}, zarejestruj webhook: POST /webhooki z url (https, publiczny host — adresy
prywatne i lokalne są odrzucane 422 WALIDACJA z pola[*].kod = URL_NIEDOZWOLONY) i listą zdarzenia. Odpowiedź zawiera sekret
(whsec_…) — widoczny tylko raz, służy do weryfikacji podpisu. Do 10 webhooków na organizację.
7.1. Zdarzenia i payload
| Zdarzenie | Kiedy |
|---|---|
faktura.wystawiona |
dokument dostał numer (także wystawiony w aplikacji) |
faktura.ksef.przyjeta |
KSeF nadał numer |
faktura.ksef.odrzucona |
błąd permanentny, dokument wrócił do szkicu |
faktura.oplacona |
faktura w pełni opłacona (API, aplikacja, wyciąg bankowy) |
faktura.anulowana |
anulowana w aplikacji |
ping |
tylko z POST /webhooki/{id}/test |
Payload jest celowo cienki i bez danych osobowych — po szczegóły sięgnij GET /faktury/{faktura_id}:
{
"id": "1f0c…",
"zdarzenie": "faktura.ksef.przyjeta",
"utworzono": "2026-09-24T10:15:03Z",
"dane": {
"faktura_id": "…", "zewnetrzny_id": "10245", "numer": "FV/12/09/2026", "rodzaj": "faktura",
"status": "wyslana", "status_platnosci": "do_zaplaty", "waluta": "PLN",
"kwota_brutto": "60.00", "kwota_zaplacona": "0.00", "pozostalo_do_zaplaty": "60.00",
"numer_zamowienia": "ZAM/2026/10245",
"ksef": {"status": "przyjeta", "numer": "5261040828-20260924-…", "bledy": [], "nastepna_proba": null, "link_weryfikacyjny": "https://…"}
}
}
id to id zdarzenia — to samo dla wszystkich webhooków organizacji i dla każdej próby doręczenia. Deduplikuj po
nim: przy ponowieniach to samo zdarzenie może przyjść więcej niż raz. Nagłówki: X-MK-Event (nazwa zdarzenia),
X-MK-Event-Id, X-MK-Delivery (id doręczenia), X-MK-Signature.
7.2. Weryfikacja podpisu
X-MK-Signature: t=<unix>,v1=<hex> gdzie v1 = HMAC-SHA256(sekret, "<t>." + surowe_ciało). Odrzuć żądanie, gdy
|teraz − t| > 300 s albo podpis się nie zgadza. Porównuj w stałym czasie. Nagłówek może zawierać kilka v1
(okno łaski po rotacji sekretu) — wystarczy, że pasuje jeden.
import hmac, hashlib, time
def zweryfikuj(sekret: str, naglowek: str, cialo: bytes, tolerancja_s: int = 300) -> bool:
# Nagłówek przychodzi z zewnątrz — zniekształcony ma dać False, nie wyjątek (i 500 u Ciebie).
t, podpisy = None, []
for element in naglowek.split(","):
klucz, _, wartosc = element.strip().partition("=")
if klucz == "t":
t = wartosc
elif klucz == "v1":
podpisy.append(wartosc)
try:
znacznik = int(t)
except (TypeError, ValueError):
return False
if not podpisy or abs(time.time() - znacznik) > tolerancja_s:
return False
oczekiwany = hmac.new(sekret.encode(), f"{znacznik}.".encode() + cialo, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(oczekiwany, p) for p in podpisy)
function zweryfikuj(string $sekret, string $naglowek, string $cialo, int $tolerancja = 300): bool {
$t = null; $podpisy = [];
foreach (explode(',', $naglowek) as $p) {
[$k, $v] = array_pad(explode('=', trim($p), 2), 2, '');
if ($k === 't' && ctype_digit($v)) { $t = (int) $v; } elseif ($k === 'v1') { $podpisy[] = $v; }
}
if ($t === null || !$podpisy || abs(time() - $t) > $tolerancja) { return false; }
$oczekiwany = hash_hmac('sha256', $t . '.' . $cialo, $sekret);
foreach ($podpisy as $p) { if (hash_equals($oczekiwany, $p)) { return true; } }
return false;
}
Podpisuj surowe ciało żądania (bajty), nie ciało po ponownej serializacji JSON.
7.3. Doręczenia, ponowienia, wyłączenie
- Odpowiedz
2xxw ciągu 10 s. Przetwarzanie zrób asynchronicznie — najpierw zapisz zdarzenie, potem odpowiedz. - Odpowiedź inna niż
2xx, timeout, błąd DNS albo przekierowanie liczą się jako porażka. Ponawiamy do 6 prób z odstępami 1 min, 5 min, 30 min, 2 h, 12 h. Odpowiedzi4xx(poza408,425,429) nie są ponawiane — to błąd po stronie odbiorcy (zły adres, odrzucony podpis), retry nic by nie zmienił. - Po 50 porażkach z rzędu webhook wyłącza się automatycznie (
wylaczony_automatycznie), właściciel dostaje powiadomienie w aplikacji i mailem. Włącz go ponownie przezPATCH /webhooki/{id}z{"aktywny": true}— licznik zeruje się, ale zdarzenia z okresu wyłączenia nie są wysyłane; dociągnij je zGET /faktury?data_od=…. GET /webhooki/{id}/dostarczenia— log ostatnich 14 dni: status (oczekuje/dostarczono/nieudane), numer próby, kod HTTP, błąd,nastepna_probai dokładnypayload.POST …/dostarczenia/{id}/ponowuruchamia nowy cykl prób z tym samym payloadem (409 DOSTARCZENIE_W_TOKU, gdy doręczenie jeszcze trwa).POST /webhooki/{id}/testwysyłapingsynchronicznie i zwraca wynik — sprawdź nim podpis przed produkcją.POST /webhooki/{id}/sekretrotuje sekret natychmiast; doręczenie rozpoczęte przed rotacją może być jeszcze podpisane starym sekretem — przez chwilę akceptuj oba.- Sekrety w adresie webhooka (
?token=…) są dozwolone, ale nie polegaj na nich — podpis HMAC jest źródłem prawdy.
8. Sandbox
Klucz mk_test_… wskazuje na osobną organizację piaskownicową („Nazwa firmy (sandbox)", ten sam NIP), którą
właściciel włącza w aplikacji albo która powstaje przy pierwszym kluczu testowym. Zasady:
- Kontrakt i walidacje są identyczne z produkcją. Faktury dostają numery i przechodzą pełną walidację FA(3)
(schemat XSD), ale nic nie wychodzi do KSeF: poprawny dokument dostaje numer KSeF
DEMO-…i statusprzyjeta, błędny —odrzuconaz powodem. Dzięki temu w sandboxie widać te same błędy, które zablokowałyby produkcję. - Bez e-maili, bez powiadomień, bez księgowania w produkcyjnej organizacji.
- Twardy limit 1000 faktur na miesiąc (
422 LIMIT_SANDBOX). - Klucze testowe i webhooki są niezależne od produkcyjnych — webhook zarejestrowany kluczem testowym dostaje tylko zdarzenia z sandboxu.
- Reset (w aplikacji) kasuje faktury, klientów, produkty i księgi testowe; klucze API i webhooki zostają.
- Sandbox nie wymaga modułu e-commerce ani karty, ani ukończonej konfiguracji firmy — zintegruj się przed zakupem. Założony przed konfiguracją firmy ma ustawienia wskazane przez Ciebie (status VAT, forma opodatkowania); po jej ukończeniu przejmuje ustawienia firmy, a Twoje dane testowe zostają.
- Nie wprowadzaj do sandboxu prawdziwych danych osobowych.
9. Limity planu
- Faktury sprzedażowe: bez limitu w każdym planie (
max_faktur_api_miesiac: -1wGET /ja; sandbox: 1000). - Dostęp do API z klucza produkcyjnego to moduł e-commerce: w planie Biznes w cenie, w Standardzie i Pro dodatek
płatny miesięcznie (właściciel włącza go w Ustawienia → Subskrypcja). Bez modułu zapisy z klucza produkcyjnego
kończą się
403 MODUL_NIEAKTYWNY; odczyty, odwołanie klucza i usunięcie webhooka działają zawsze. - Zapytania: 300 odczytów i 120 zapisów na minutę per klucz (rozdział 3.1).
10. Dobre praktyki integracji
- Jedno zamówienie = jeden
zewnetrzny_idfaktury i jedenIdempotency-Key; retry zawsze z tym samym kluczem. - Nie traktuj
201zPOST /fakturyjako „faktura w KSeF" — dopieroksef.status = przyjeta(webhookfaktura.ksef.przyjeta) kończy sprawę;odrzuconawymaga poprawki po stronie właściciela w aplikacji. - Weryfikuj podpis webhooka, deduplikuj po
idzdarzenia, odpowiadaj szybko, przetwarzaj asynchronicznie. - Po wyłączeniu webhooka albo dłuższej awarii dociągnij stan z
GET /fakturyz filtrami dat i statusu. - Honoruj
Retry-AfteriX-RateLimit-*; masowe synchronizacje rozłóż w czasie. - Przechowuj sekret klucza i sekret webhooka jak hasła; rotuj je z aplikacji, nie twórz nowych kluczy „na zapas".
- Zacznij w sandboxie, przełącz na
mk_live_dopiero po pełnym przebiegu: faktura →przyjeta→ PDF → płatność.
11. Wersjonowanie
Ścieżka /api/public/v1 jest stała. Dodajemy pola i zdarzenia bez zmiany wersji (ignoruj nieznane pola
w odpowiedziach i nieznane zdarzenia w webhookach); zmiany łamiące wejdą jako v2 z okresem równoległym.