← Back to all articles

Od dat k zapečetěnému PDF v jednom API volání

SealDoc Team · · 5 min read

Otázka, kterou jsme od zákazníků s funkční automatizační stack slýchali nejčastěji, byla krátká: “Proč musím svou fakturu nejprve renderovat někde jinde, než ji mohu předat vám?” Oprávněná otázka. Tok n8n nebo Make, který chtěl z řádku databáze nebo z webhook payloadu udělat čisté compliance PDF, se zastavoval přesně na tomto kroku. Vy jste měli data. Chtěli jste zapečetěné PDF/A-3. Mezi tím seděl nástroj třetí strany s vlastním ceníkem, vlastním rate limitem a vlastním systémem šablon, který se s naším vstupním validátorem hádal při každé změně předpisů.

Tato mezistanice je pryč. Od sprintů 49 a 50 můžete posílat Markdown nebo JSON fakturu přímo do SealDoc a dostanete zpět PDF/A-3, které splňuje stejná pravidla jako vše, co prochází naší custody pipeline: archivní formát, vložené XML tam, kde to dává smysl, volitelné časové razítko RFC 3161, volitelný evidence pack, podpora Idempotency-Key. Jeden HTTP požadavek, jedna odpověď, hotovo pro váš retenční bucket.

Co je nového

Dva nové endpointy a jedno utužení v evidence packs.

POST /api/documents/generate

Pošlete Markdown nebo HTML, dostanete PDF/A-3. HTML sanitizer (viz ADR-0013) na straně serveru odstraní všechny scripty, iframe, externí obrázky a CSS odkazy url(), protože dokument, který archivujete, nesmí později tiše změnit svůj obsah skrz externí asset. Maximálně 100 KB obsahu na požadavek: dost velké pro typickou compliance zprávu, dost malé na to, aby to odrazovalo od zneužití. Idempotency-Key je podporován, což znamená, že retry z vašeho n8n flow nevytvoří v archivu druhé PDF.

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

Stejný princip, ale pro faktury. Pošlete strukturovaný JSON payload (prodávající, kupující, položky, součty, data) a dostanete PDF/A-3 Factur-X 1.0 BASIC s již vloženým CII XML. Žádný mezikrok, ve kterém byste museli nejdřív renderovat vizuální PDF a my ho potom sémanticky párovat.

Dvě věci, které byste měli vědět, než to nasadíte do produkce.

Validace prodávajícího vedená podle DPH. Číslo DPH v seller.vatNumber musí odpovídat číslu DPH vašeho vlastního tenanta. Obě strany před porovnáním kanonizujeme (velká písmena, tečky a mezery odstraněny), takže nl 1234.56789.b01 zcela odpovídá NL123456789B01. Pokud se neshodují, dostanete 403 s jasným chybovým kódem. Není to byrokracie: brání to tomu, aby jeden tenant vystavoval faktury jménem jiného. Drift CompanyName mezi požadavkem a tenantem neblokujeme (obchodní názvy se mění), ale logujeme ho jako audit varování, aby zůstal dohledatelný.

Fakturační profil musí být kompletní. DPH, Adresa, PSČ, Město a Země na vašem tenantovi tvoří dohromady identitu prodávajícího na faktuře. Pokud jeden chybí, dostanete 412 se seznamem chybějících polí. To opravíte jednou v portálu a pak už nikdy.

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
  }'

Pro opakované fakturace platí jednoduché pravidlo: vždy posílejte explicitní Idempotency-Key. Nededuplikujeme automaticky podle hashe payloadu, protože dvě identické měsíční faktury během 24 hodin mohou být zcela legitimní (oprava, znovuvystavení, dvojí předplatné na jedné právnické osobě). Idempotency-Key v rukou vašeho flow, garance bez duplikátů v rukou našich.

Manifest hash-chain v2.0 v evidence packs

Méně viditelné, ale pro auditory důležité utužení. Každý evidence pack nyní obsahuje MANIFEST.json s SHA256 a velikostí v bajtech pro každý soubor, abecedně seřazené podle názvu, v kanonickém JSON (camelCase, bez odsazení). Vedle leží manifest.sha256: SHA256 těchto kanonických bajtů manifestu.

Proč dvě úrovně. Hashe per-soubor odhalí manipulaci s obsahem jednoho souboru. Hash manifestu odhalí manipulaci se sadou souborů jako takovou: tiché odebrání přílohy ze ZIP a odpovídající přepsání manifestu. To druhé bylo ve v1.0 ještě možné; ve v2.0 už ne, aniž by si toho ověřovatel všiml.

Důležité: nejedná se o kryptografický podpis, a právě proto se soubor nejmenuje manifest.signature. Pojmenování odráží, čím je, tedy hash-chainem. Budoucí sprint možná přidá Cosign nebo klíč spravovaný SealDoc pro nepopiratelnost; v ten okamžik se manifest.signature stane správným názvem, a ne dříve.

Reálný příklad: Stripe webhook do archivované Factur-X

Konkrétně takto tyto tři změny dohromady otevírají třídu workflow, která dříve neexistovala.

Předpokládejme, že prodáváte SaaS předplatná přes Stripe. U každého úspěšného invoice.payment_succeeded webhooku chcete českou Factur-X fakturu ve vlastním archivu, oddělenou od toho, co generuje Stripe (PDF od Stripe není PDF/A-3, není Factur-X a nemá časové razítko RFC 3161).

Flow v n8n nebo Make:

  1. Stripe Trigger zachytí invoice.payment_succeeded.
  2. Function node namapuje Stripe objekt na SealDoc invoice payload. customer_address se stane buyer, lines se postaví z lines.data, vaše vlastní DIČ sedí staticky v seller bloku.
  3. HTTP Request node udělá POST /api/invoices/generate s Idempotency-Key: stripe-{{$json.id}}. Stripe může klidně doručit stejný webhook třikrát; váš archiv dostane přesně jednu fakturu.
  4. HTTP Response node přijme stream PDF/A-3 a URL evidence packu. PDF zapíšete do retenčního bucketu, URL evidence packu uložíte do své účetní databáze.

To je vše. Žádný samostatný rendering krok, žádný šablonovací engine, který si udržujete, žádné XML, které byste lepili rukama. Factur-X XML vychází z naší strany zdi, ve shodě s 1.0 BASIC, a zapadá do jakékoliv PEPPOL pipeline, kterou si příjemce vyžádá.

Co to řeší

Tři změny dohromady řeší jeden problém, vzdálenost mezi “mám strukturovaná data” a “mám dokument, který francouzský nebo belgický finanční úřad přijímá jako originál”. Před sprintem 49 byla touto vzdáleností třetí strana. Od sprintu 50 je to HTTP požadavek.

Pokud váš flow už běží přes n8n-nodes-sealdoc nebo aplikaci Make, nové endpointy přijdou v příštím vydání nodu. Pokud mluvíte přímo s REST API, jsou živé už dnes.

A když už jsme u toho: roční fakturace

Menší změna, ale poptávalo ji dost zákazníků na to, aby si zasloužila zmínku. V nastavení předplatného nyní můžete přepínat mezi Měsíčně a Ročně. Roční ceny jsou PriceMonthly × 10, tedy dva měsíce zdarma, a Mollie strhává každých dvanáct měsíců místo každých třiceti dnů. Žádná migrace, žádná změna smlouvy: přepněte přepínač u příštího okamžiku obnovy.

Compliance instalatérská práce je vyřešená. Co s ušetřeným časem uděláte, je na vás.


← Back to all articles