← Back to all articles

Od danych do zapieczętowanego PDF w jednym wywołaniu API

SealDoc Team · · 5 min read

Pytanie, które słyszeliśmy najczęściej od klientów z działającym stosem automatyzacji, było krótkie: “Dlaczego muszę najpierw renderować fakturę gdzieś indziej, zanim mogę przekazać ją Państwu?” Słuszne pytanie. Przepływ n8n lub Make, który chciał z wiersza w bazie danych albo z payloadu webhooka zrobić czysty PDF zgodności, zatrzymywał się dokładnie na tym kroku. Mieli Państwo dane. Chcieli Państwo zapieczętowanego PDF/A-3. Pomiędzy nimi siedziało narzędzie trzecie z własnym cennikiem, własnym rate limitem i własnym systemem szablonów, który przy każdej zmianie przepisów kłócił się z naszym walidatorem wejścia.

Tego pośrednika już nie ma. Od Sprintów 49 i 50 mogą Państwo wysłać Markdown albo fakturę JSON wprost do SealDoc i otrzymać PDF/A-3, który spełnia te same reguły co wszystko, co przechodzi przez naszą pipeline custody: format archiwalny, osadzony XML tam, gdzie ma to zastosowanie, opcjonalny znacznik czasu RFC 3161, opcjonalny evidence pack, obsługa Idempotency-Key. Jedno żądanie HTTP, jedna odpowiedź, gotowe do bucketa retencji.

Co nowego

Dwa nowe endpointy i jeden krok utwardzający w evidence packs.

POST /api/documents/generate

Wysyłają Państwo Markdown lub HTML, dostają PDF/A-3. Sanitizer HTML (patrz ADR-0013) usuwa po stronie serwera wszystkie skrypty, iframe’y, obrazy zewnętrzne i odwołania CSS url(), ponieważ dokument, który Państwo archiwizują, nie może później po cichu zmienić swojej treści przez zewnętrzny zasób. Maksymalnie 100 KB treści na żądanie: dość duży na typowy raport zgodności, dość mały, by zniechęcić do nadużyć. Idempotency-Key jest wspierany, co oznacza, że retry z Państwa flow w n8n nie utworzy drugiej PDF w archiwum.

curl -X POST https://api.sealdoc.eu/api/documents/generate \
  -H "X-Api-Key: $KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: report-2026-q1-acme-001" \
  -d '{
    "format": "markdown",
    "content": "# Q1 2026 Compliance Report\n\nTenant: Acme BV\n\nAll 142 invoices processed without exceptions.",
    "title": "Q1 2026 Compliance Report",
    "timestampRfc3161": true,
    "generateEvidencePack": true
  }'

POST /api/invoices/generate

Ta sama zasada, tylko dla faktur. Wysyłają Państwo ustrukturyzowany payload JSON (sprzedawca, nabywca, pozycje, sumy, daty) i otrzymują PDF/A-3 Factur-X 1.0 BASIC z osadzonym już XML CII. Bez kroku pośredniego, w którym musieliby Państwo najpierw wyrenderować wizualny PDF, a my później dopasowywać go semantycznie.

Dwie rzeczy warto wiedzieć przed wdrożeniem na produkcji.

Walidacja sprzedawcy prowadzona po VAT. Numer VAT w seller.vatNumber musi zgadzać się z numerem VAT Państwa własnego tenanta. Kanonizujemy obie strony (wielkie litery, kropki i spacje usunięte) przed porównaniem, więc nl 1234.56789.b01 pasuje bez problemu do NL123456789B01. Jeśli nie zgadzają się, otrzymują Państwo 403 z czytelnym kodem błędu. To nie biurokracja: zapobiega temu, by jeden tenant wystawiał faktury w imieniu innego. Drift w CompanyName między żądaniem a tenantem nie jest blokowany (nazwy handlowe zmieniają się), ale trafia do logu jako ostrzeżenie audytowe, by pozostał śladowalny.

Profil rozliczeniowy musi być kompletny. PTU/VAT, Adres, Kod pocztowy, Miasto i Kraj na Państwa tenancie razem tworzą tożsamość sprzedawcy na fakturze. Jeśli brakuje jednego, otrzymują Państwo 412 z listą brakujących pól. Naprawiają Państwo to raz w portalu i nigdy więcej.

curl -X POST https://api.sealdoc.eu/api/invoices/generate \
  -H "X-Api-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "seller": {"name":"Acme BV","vatNumber":"NL123456789B01","address":"Singel 1","postalCode":"1011AB","city":"Amsterdam","country":"NL"},
    "buyer": {"name":"Klant BV","vatNumber":"BE0123456789","address":"Avenue 5","postalCode":"1000","city":"Brussels","country":"BE"},
    "invoiceNumber":"2026-001","invoiceDate":"2026-05-07","currency":"EUR","vatRate":0.21,"vatLabel":"21% BTW",
    "lines":[{"description":"Consulting","quantity":10,"unitPriceExclVat":150,"unit":"hour"}],
    "timestampRfc3161":true,"generateEvidencePack":true
  }'

Dla rozliczeń cyklicznych zasada jest prosta: zawsze proszę wysyłać jawny Idempotency-Key. Nie deduplikujemy automatycznie po hashu payloadu, ponieważ dwie identyczne faktury miesięczne w ciągu 24 godzin mogą być całkowicie uzasadnione (korekta, ponowna emisja, podwójny abonament na jednej osobie prawnej). Idempotency-Key w rękach Państwa flow, gwarancja braku duplikatów w naszych.

Manifest hash-chain v2.0 w evidence packs

Mniej widoczne, ale dla audytorów istotne utwardzenie. Każdy evidence pack zawiera teraz MANIFEST.json z SHA256 i rozmiarem w bajtach dla każdego pliku, posortowany alfabetycznie po nazwie, w kanonicznym JSON (camelCase, bez wcięć). Obok znajduje się manifest.sha256: SHA256 tych kanonicznych bajtów manifestu.

Dlaczego dwa poziomy. Hashe per-plik wykrywają manipulację treścią pojedynczego pliku. Hash manifestu wykrywa manipulację samym zestawem plików: po cichu usunięcie załącznika z ZIP-a i odpowiednie przepisanie manifestu. Tamto było jeszcze możliwe w v1.0; w v2.0 nie jest już możliwe bez tego, by weryfikator to zauważył.

Ważne: to nie jest kryptograficzny podpis i właśnie dlatego plik nie nazywa się manifest.signature. Nazewnictwo odzwierciedla to, czym jest, czyli hash-chainem. Przyszły sprint może dodać Cosign albo klucz zarządzany przez SealDoc dla niezaprzeczalności; wtedy manifest.signature stanie się właściwą nazwą, a nie wcześniej.

Realny przykład: webhook Stripe do zarchiwizowanego Factur-X

Konkretnie, oto jak te trzy zmiany razem otwierają klasę flow, która wcześniej nie istniała.

Załóżmy, że sprzedają Państwo subskrypcje SaaS przez Stripe. Przy każdym udanym webhooku invoice.payment_succeeded chcą Państwo polskiej faktury Factur-X we własnym archiwum, niezależnie od tego, co generuje Stripe (PDF Stripe’a nie jest PDF/A-3, nie jest Factur-X i nie ma znacznika czasu RFC 3161).

Flow w n8n lub Make:

  1. Stripe Trigger łapie invoice.payment_succeeded.
  2. Function node mapuje obiekt Stripe na payload faktury SealDoc. customer_address staje się buyer, lines budowane jest z lines.data, Państwa własny numer VAT siedzi statycznie w bloku seller.
  3. HTTP Request node wykonuje POST /api/invoices/generate z Idempotency-Key: stripe-{{$json.id}}. Stripe może spokojnie wysłać ten sam webhook trzy razy; archiwum dostanie dokładnie jedną fakturę.
  4. HTTP Response node otrzymuje strumień PDF/A-3 i URL evidence packa. Zapisują Państwo PDF do bucketa retencji, URL evidence packa zapisują w bazie księgowej.

To wszystko. Bez osobnego kroku renderowania, bez silnika szablonów do utrzymania, bez XML budowanego ręcznie. XML Factur-X wychodzi z naszej strony muru, zgodny z 1.0 BASIC, i pasuje do każdej pipeline PEPPOL, której odbiorca może zażądać.

Co to rozwiązuje

Trzy zmiany razem dotykają jednego problemu, czyli odległości między “mam ustrukturyzowane dane” a “mam dokument, który francuski lub belgijski urząd skarbowy akceptuje jako oryginał”. Przed Sprintem 49 ta odległość była stroną trzecią. Od Sprintu 50 jest żądaniem HTTP.

Jeśli Państwa flow chodzi już przez n8n-nodes-sealdoc albo aplikację Make, nowe endpointy trafią do następnego wydania noda. Jeśli rozmawiają Państwo bezpośrednio z REST API, są one live już dzisiaj.

A przy okazji: rozliczenia roczne

Mniejsza zmiana, ale dość klientów o nią prosiło, by zasłużyła na wzmiankę. W ustawieniach subskrypcji mogą Państwo teraz przełączać między Miesięczną a Roczną. Ceny roczne to PriceMonthly × 10, czyli dwa miesiące gratis, a Mollie pobiera płatność co dwanaście miesięcy zamiast co trzydzieści dni. Bez migracji, bez zmiany umowy: proszę przełączyć przełącznik przy najbliższym odnowieniu.

Hydraulika zgodności jest rozwiązana. Co Państwo zrobią z odzyskanym czasem, zależy od Państwa.


← Back to all articles