Adres bazowy i uwierzytelnianie
Wszystkie żądania trafiają do https://api.getnotaly.com przez HTTPS, z treścią w JSON. Uwierzytelniasz się kluczem API w jednym z nagłówków:
Authorization: Bearer $NOTALY_API_KEY
# lub
X-API-Key: $NOTALY_API_KEY
Klucz dostajesz przy rejestracji; w panelu tworzysz, rotujesz i unieważniasz klucze. Pełny klucz jest pokazywany raz, a przechowujemy tylko jego skrót. Limit to 60 żądań na minutę na klucz; powyżej API odpowiada 429 z Retry-After: 60.
Utwórz fakturę
POST/invoices
Waliduje dane, generuje XML FA(3) i wysyła go do KSeF. Zwraca 201 z obiektem faktury. Wyślij nagłówek Idempotency-Key, aby ponowienia były bezpieczne: ten sam klucz zwraca pierwotną fakturę zamiast tworzyć nową.
| Pole | Typ | Uwagi |
|---|---|---|
number | string | Twój numer faktury, np. FV/2026/10/001 |
issueDate | string | YYYY-MM-DD |
currency | string | ISO 4217, np. PLN |
seller, buyer | object | nip (10 cyfr, poprawna suma kontrolna), name, address.line1, opcjonalnie address.line2, address.country (ISO 3166 alpha-2) |
items[] | array | 1–1000 pozycji: name, opcjonalnie unit, quantity, unitNetPrice, vatRate (23, 8, 5 lub 0) |
{
"id": "inv_8f2c41d0a9b34e7c9d1e2f3a4b5c6d7e",
"status": "submitted",
"number": "FV/2026/10/001",
"ksefReferenceNumber": "20261012-EE-2A41B3C7D9-1F",
"ksefNumber": null,
"upo": null,
"pdfUrl": "/invoices/inv_8f2c…/pdf",
"error": null,
"history": [ { "status": "draft", "at": "…", "source": "api" },
{ "status": "submitted", "at": "…", "source": "ksef" } ],
"createdAt": "2026-10-12T07:23:07.000Z",
"updatedAt": "2026-10-12T07:23:08.000Z"
}
Gdy KSeF jest niedostępny, faktura zostaje zapisana jako draft z "error": "ksef_unavailable" i żadne dane nie giną.
Pobierz fakturę
GET/invoices/{id}
Zwraca ten sam obiekt faktury. Gdy faktura ma status submitted, wywołanie sprawdza też KSeF i zapisuje decyzję, więc odpytywanie jest poprawną alternatywą dla webhooków.
| Status | Znaczenie |
|---|---|
| draft | XML wygenerowany, jeszcze nie wysłany do KSeF |
| submitted | Wysłana do KSeF; pozostaje tu, dopóki KSeF ją przetwarza |
| accepted | Przyjęta: ksefNumber i upo są ustawione |
| rejected | Odrzucona: error zawiera stały kod, np. ksef_rejected:<code> |
Pobieranie XML i PDF
GET/invoices/{id}/xml
Dokładny XML FA(3) wysłany do KSeF (application/xml). Jego strukturę opisuje poradnik XML FA(3).
GET/invoices/{id}/pdf
Wizualizacja PDF (application/pdf) z kodem QR do weryfikacji w KSeF (KOD I). Fakturą w sensie prawnym jest XML; PDF jest dla ludzi.
Webhooki
Dodaj endpoint HTTPS w panelu, w sekcji Webhooki. Sekret do podpisu jest pokazywany raz. Notaly wysyła POST, gdy faktura zmienia stan:
| Zdarzenie | Kiedy |
|---|---|
invoice.accepted | KSeF przyjął fakturę; zawiera ksefNumber, upo i bezwzględny pdfUrl |
invoice.rejected | KSeF odrzucił fakturę; error zawiera kod |
invoice.error | Nie udało się przetworzyć faktury |
{
"id": "evt_inv_8f2c…_accepted",
"type": "invoice.accepted",
"createdAt": "2026-10-12T07:24:02.000Z",
"data": { "invoiceId": "inv_8f2c…", "status": "accepted",
"ksefReferenceNumber": "20261012-EE-…", "ksefNumber": "2745013933-20261012-…",
"upo": "…", "error": null, "pdfUrl": "https://api.getnotaly.com/invoices/inv_8f2c…/pdf" }
}
Weryfikacja podpisu
Każda dostawa zawiera X-Webhook-Id, X-Webhook-Timestamp i X-Webhook-Signature: t=<unix>,v1=<hex>, gdzie v1 to HMAC-SHA256 z "<t>.<surowe body>" z sekretem endpointu jako kluczem.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret: string, rawBody: string, header: string): boolean {
const p = Object.fromEntries(header.split(",").map((x) => x.split("=", 2)));
const t = Number(p.t);
if (!p.v1 || Math.abs(Date.now() / 1000 - t) > 300) return false; // okno 5 minut
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const got = Buffer.from(p.v1, "hex");
return got.length === expected.length && timingSafeEqual(got, expected);
}
Ponowienia i dostarczanie
- Potwierdź dowolnym kodem
2xx. Każdy inny status, przekierowanie lub timeout powoduje ponowienie. - Do 8 prób z wykładniczym odstępem (30 s, 60 s, 2 min… maksymalnie 1 godzina, z losowym rozrzutem). Treść i id zdarzenia są identyczne przy każdej próbie.
- Dostarczanie „co najmniej raz”: deduplikuj po
X-Webhook-Id. - Endpointy muszą być publicznymi adresami HTTPS; adresy prywatne i wewnętrzne są odrzucane.
Błędy
Błędy to JSON ze stałym kodem error. Surowe komunikaty KSeF nigdy nie są zwracane.
| HTTP | error | Co zrobić |
|---|---|---|
| 401 | unauthorized | Brak klucza API lub klucz nieprawidłowy |
| 402 | quota_exceeded | Sandbox wykorzystał 3 faktury testowe; przejdź na Starter lub Scale |
| 404 | not_found | Nieznane id lub faktura utworzona innym kluczem API |
| 422 | validation_failed | Popraw pola wymienione w details |
| 422 | seller_nip_not_authorized | W sandboxie seller.nip musi mieć wartość 2745013933 |
| 429 | rate_limited | Odczekaj Retry-After sekund |
Kody na poziomie faktury w polu error: ksef_unavailable (KSeF nie odpowiedział; faktura pozostaje draft), ksef_rejected:<code> (odrzucona przez KSeF) i ksef_error.
Sandbox i plany
| Plan | Cena | Środowisko KSeF | Faktury |
|---|---|---|---|
| Sandbox | Darmowy | TEST, NIP sprzedawcy 2745013933 | 3 łącznie |
| Starter | 49 €/mc | Twoje połączenie z KSeF | Do 200 miesięcznie |
| Scale | 199 €/mc | Twoje połączenie z KSeF | Do 1 000 miesięcznie |