Od danych do zapieczętowanego PDF w jednym wywołaniu API
Każdy, kto ma działający stos automatyzacji, trafia na to samo krótkie pytanie: “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ł.
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:
- Stripe Trigger łapie
invoice.payment_succeeded. - Function node mapuje obiekt Stripe na payload faktury SealDoc.
customer_addressstaje siębuyer,linesbudowane jest zlines.data, Państwa własny numer VAT siedzi statycznie w bloku seller. - HTTP Request node wykonuje
POST /api/invoices/generatezIdempotency-Key: stripe-{{$json.id}}. Stripe może spokojnie wysłać ten sam webhook trzy razy; archiwum dostanie dokładnie jedną fakturę. - 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.
Co to rozwiązuje
Trzy zmiany razem dotykają jednego problemu, czyli odległości między “mam ustrukturyzowane dane” a “mam zwalidowaną e-fakturę gotową do archiwizacji”. 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 zasługuje 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.