mojaKsięgowa.ai · Publiczne APIReferencja endpointów (Redoc)openapi.json

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.

  1. W aplikacji: Ustawienia → Integracje API → Utwórz klucz. Wybierz środowisko piaskownica (klucz mk_test_…), zakresy faktury: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.
  2. Sprawdź tożsamość klucza:
http
GET /api/public/v1/ja HTTP/1.1
Authorization: Bearer mk_test_…
json
{
  "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}
}
  1. Wystaw fakturę:
http
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).

  1. 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

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ę:

json
{
  "blad": {
    "kod": "WALIDACJA",
    "komunikat": "Nieprawidłowe dane żądania.",
    "pola": [{"pole": "pozycje[0].stawka_vat", "kod": "ENUM", "komunikat": "Input should be '23', '8', …"}]
  }
}

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ę:

json
{"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:

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ć:

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:

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

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.

http
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}]
}
json
{"przyczyna": "Uszkodzone opakowanie", "powod": "reklamacja",
 "pozycje": [{"lp": 2, "cena_jednostkowa_brutto_po": "20.00"}]}

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.

json
{
  "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"}]
}

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}:

json
{
  "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.

python
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)
php
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

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:

9. Limity planu

10. Dobre praktyki integracji

  1. Jedno zamówienie = jeden zewnetrzny_id faktury i jeden Idempotency-Key; retry zawsze z tym samym kluczem.
  2. Nie traktuj 201 z POST /faktury jako „faktura w KSeF" — dopiero ksef.status = przyjeta (webhook faktura.ksef.przyjeta) kończy sprawę; odrzucona wymaga poprawki po stronie właściciela w aplikacji.
  3. Weryfikuj podpis webhooka, deduplikuj po id zdarzenia, odpowiadaj szybko, przetwarzaj asynchronicznie.
  4. Po wyłączeniu webhooka albo dłuższej awarii dociągnij stan z GET /faktury z filtrami dat i statusu.
  5. Honoruj Retry-After i X-RateLimit-*; masowe synchronizacje rozłóż w czasie.
  6. Przechowuj sekret klucza i sekret webhooka jak hasła; rotuj je z aplikacji, nie twórz nowych kluczy „na zapas".
  7. 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.