Przejdź do głównej zawartości

Kontrakt API

Sancho rozpoznaje dokumenty przez wymienny backend OCR. Domyślnie jest to Parseur, ale wdrożenie self-hosted może wskazać własną usługę — wystarczy, że wystawi dwa endpointy HTTP opisane poniżej.

Jeśli dopiero zaczynasz implementację, zacznij od przewodnika Jak zbudować backend OCR, a tutaj wracaj po szczegóły.

Zmienne środowiskowe API:

Zmienna Opis
OCR_PROVIDER parseur (domyślnie) albo custom.
OCR_CUSTOM_BASE_URL Adres bazowy usługi, np. https://ocr.firma.local. Wymagany dla custom.
OCR_CUSTOM_SUBMIT_PATH Ścieżka endpointu przyjmującego dokument. Domyślnie /ocr/documents.
OCR_CUSTOM_STATUS_PATH Ścieżka endpointu statusu; identyfikator zlecenia doklejany jest na końcu. Domyślnie /ocr/documents.
OCR_CUSTOM_API_KEY Token wysyłany w nagłówku Authorization. Wymagany przy bearer i raw, nieużywany przy none i jwt_hs256.
OCR_CUSTOM_AUTH_SCHEME none, bearer (domyślnie), raw albo jwt_hs256. Patrz Uwierzytelnianie.
OCR_CUSTOM_JWT_SECRET Sekret współdzielony z usługą OCR, minimum 24 znaki. Wymagany dla jwt_hs256.
OCR_CUSTOM_JWT_ISSUER Wartość iss tokenu. Domyślnie adres API tego wdrożenia.
OCR_CUSTOM_JWT_AUDIENCE Wartość aud tokenu. Domyślnie host z OCR_CUSTOM_BASE_URL.
OCR_CUSTOM_JWT_TTL_S Czas życia tokenu w sekundach. Domyślnie 300, zakres 180–3600 — token musi przeżyć całą wysyłkę, którą uwierzytelnia.
OCR_POLL_INTERVAL_MS Odstęp między odpytaniami. Domyślnie 3000.
OCR_POLL_MAX_ATTEMPTS Maksymalna liczba odpytań. Domyślnie 200 (czyli 10 minut przy domyślnym odstępie).

Adres jest sklejany dosłownie: OCR_CUSTOM_BASE_URL + ścieżka. Baza może więc zawierać własny prefiks (https://ocr.firma.local/api) i nie zostanie on utracony.

Niepoprawna konfiguracja zatrzymuje start API: custom bez OCR_CUSTOM_BASE_URL, parseur bez PARSEUR_API_KEY, bearer albo raw bez OCR_CUSTOM_API_KEY, jwt_hs256 bez sekretu, a także budżet odpytywania (OCR_POLL_INTERVAL_MS × OCR_POLL_MAX_ATTEMPTS) dłuższy niż 10 minut.

OCR_CUSTOM_AUTH_SCHEME wybiera jeden z czterech trybów:

Tryb Nagłówek Authorization
none brak nagłówka — dla usługi, która niczego nie sprawdza
bearer (domyślny) Bearer <OCR_CUSTOM_API_KEY>
raw <OCR_CUSTOM_API_KEY> — dla bram dopasowujących nagłówek dosłownie, jak Header Auth w n8n
jwt_hs256 Bearer <token> — token HS256 podpisywany od nowa przy każdym żądaniu

Wybór jest walidowany przy starcie: bearer i raw wymagają OCR_CUSTOM_API_KEY, a jwt_hs256 — sekretu (wystawca i odbiorca mają wartości domyślne). Usługę bez uwierzytelniania trzeba zadeklarować jawnie przez none; brak tokenu nie jest traktowany jak jego wyłączenie.

Nagłówek trafia do obu endpointów: przyjęcia dokumentu i statusu.

Sancho podpisuje token sekretem współdzielonym z usługą OCR (HMAC-SHA256), a usługa weryfikuje go tym samym sekretem. Ładunek:

{
"iss": "https://app.firma.local",
"aud": "ocr.firma.local",
"sub": "sancho-ocr-client",
"iat": 1774694400,
"exp": 1774694700,
"jti": "b8f3c1de-9d4e-4a51-8b0c-1f2e3d4a5b6c"
}

iss i aud są wypełniane automatycznie — adresem API tego wdrożenia i hostem usługi OCR — dopóki nie ustawisz ich jawnie. Nagłówek JOSE to {"alg":"HS256","typ":"JWT"}. Token żyje OCR_CUSTOM_JWT_TTL_S sekund (domyślnie 300) i nie jest cache’owany — każde żądanie, łącznie z każdym odpytaniem o status, dostaje świeży.

Sekret jest symetryczny: obie strony mogą wystawić ważny token. Krótki czas życia chroni przed powtórzeniem przechwyconego żądania, ale nie przed wyciekiem samego sekretu — traktuj go jak hasło. Minimum 24 znaki — krótszy zatrzymuje start API. Zalecane 32 (openssl rand -hex 16). Sama długość niczego nie gwarantuje: sekret musi być losowy, bo zgadywalny daje się złamać offline z jednego przechwyconego tokenu.

W węźle Webhook ustaw Authentication: JWT Auth i utwórz poświadczenie:

  • Key Type — Passphrase
  • Secret — ten sam sekret co OCR_CUSTOM_JWT_SECRET
  • Algorithm — HS256

Poświadczenie podepnij do obu węzłów Webhook — n8n weryfikuje uwierzytelnienie osobno w każdym z nich.

n8n sprawdza wyłącznie podpis oraz exp. Pola iss, aud, sub i jti są wysyłane, ale nie są weryfikowane; jeśli mają obowiązywać, dołóż jawne sprawdzenie w samym przepływie.

Tryb raw jest niezgodny z JWT Auth: n8n czyta token jako drugi człon nagłówka (authHeader.split(" ")[1]), więc token bez prefiksu Bearer kończy się błędem 401.

Błąd uwierzytelnienia zwracaj jako 401 (brak tokenu) albo 403 (token niepoprawny) — dokładnie tak zachowuje się wbudowana weryfikacja n8n. Nie zwracaj 5xx: Sancho ponawia odpytania po 5xx, więc dokument zużyłby cały budżet, zanim zgłosi błąd.

Poświadczenie n8n przechowuje dokładnie jeden sekret, więc nie da się przyjmować starego i nowego równocześnie. Wymiana jest jednoczesna: w oknie serwisowym podmień sekret w n8n i OCR_CUSTOM_JWT_SECRET w Sancho. Dokumenty wysłane w trakcie wymiany skończą się błędem i trzeba je powtórzyć.

POST {OCR_CUSTOM_BASE_URL}{OCR_CUSTOM_SUBMIT_PATH}

Żądanie multipart/form-data:

Pole Opis
file Zawartość dokumentu (PDF lub obraz); nazwa pliku i typ MIME są zachowane.
documentType purchase_order albo goods_received_source.

Odpowiedź 202 (dowolny kod 2xx jest akceptowany):

{ "id": "3f1c8b12-9d4e-4a51-8b0c-1f2e3d4a5b6c", "status": "pending" }

id to dowolny identyfikator unikalny w obrębie usługi — najlepiej UUID. Musi być nieprzezroczysty i bezpieczny w URL: trafia do ścieżki endpointu statusu, więc znaki takie jak / czy spacja zostaną zakodowane procentowo (%2F, %20), a wiele serwerów odrzuca %2F w segmencie ścieżki. Nie używaj numerów dokumentów.

status przyjmuje wartości pending, processing, queued, ready lub error; pominięcie pola oznacza pending. Zwrócenie ready już tutaj jest dozwolone — Sancho i tak wykona jedno odpytanie, bo odpowiedź na przyjęcie nie zawiera pola data.

Sancho nigdy nie ponawia tego żądania automatycznie — powtórka oznaczałaby drugie zlecenie dla tego samego dokumentu.

GET {OCR_CUSTOM_BASE_URL}{OCR_CUSTOM_STATUS_PATH}/{id}

W trakcie przetwarzania:

{ "id": "3f1c8b12-9d4e-4a51-8b0c-1f2e3d4a5b6c", "status": "processing" }

Po zakończeniu:

{
"id": "3f1c8b12-9d4e-4a51-8b0c-1f2e3d4a5b6c",
"status": "ready",
"data": { "documentNumber": "ZAM/1/2026", "items": [] }
}

Przy błędzie:

{ "id": "3f1c8b12-9d4e-4a51-8b0c-1f2e3d4a5b6c", "status": "error", "message": "Nie udało się odczytać dokumentu" }

Odpowiedź spoza zakresu 2xx (w tym 404 dla nieznanego id) jest traktowana jak awaria połączenia: Sancho ponawia odpytanie w ramach pozostałego budżetu, a gdy ten się wyczerpie — oznacza dokument jako błędny.

Pole data musi być zgodne ze schematem JSON odpowiadającym przesłanemu documentType:

  • purchase-order.schema.json — documentType: purchase_order
  • goods-received-source.schema.json — documentType: goods_received_source — dowolny dokument źródłowy przyjęcia: WZ, faktura zakupu, dowód dostawy. Oprócz nagłówka i pozycji schemat zawiera klasyfikację kind (invoice / goodsIssuedNote / other), ceny pozycji (priceNet, valueNet, vatRate) oraz deliveryCostNet — koszt transportu, zwracany zamiast pozycji usługowej w items

Zasady wspólne dla obu schematów:

  • Wszystkie pola są opcjonalne — pomiń to, czego parser nie odczytał.
  • Pominięta wartość skalarna jest traktowana jak null, pominięte items jak [].
  • Nieznane klucze są ignorowane.
  • Daty to napisy w formacie YYYY-MM-DD.
  • Liczby to liczby JSON, nie napisy.
  • NIP-y najlepiej podawać jako same cyfry — Sancho i tak usuwa spacje oraz myślniki.

Dokument, którego wynik nie przejdzie walidacji, otrzymuje status błędu (SchemaValidationError) i nie trafia do dalszego przetwarzania.