← Powrót do wszystkich artykułów

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

SealDoc Team · · 5 min czytania

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:

  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.

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.

Zwaliduj własną fakturę, bez konta →
← Powrót do wszystkich artykułów