← Back to all articles

Van data naar verzegelde PDF in één API-call

SealDoc Team · · 5 min read

De vraag die we het vaakst kregen van klanten met een werkende automation-stack was kort: “Waarom moet ik mijn factuur eerst ergens anders renderen voordat ik hem aan jullie kan geven?” Een redelijke vraag. Een n8n- of Make-flow die uit een database of webhook-payload een nette compliance-PDF wil maken, liep tot vorige sprint vast op exact die stap. U had data. U wilde een verzegelde PDF/A-3. Daartussen zat een derde partij met een eigen prijskaartje, een eigen rate limit, en een eigen template-systeem dat bij elke regelwijziging weer ruzie had met onze intake-validator.

Dat tussenstation is weg. Sinds Sprint 49 en 50 kunt u Markdown of een JSON-factuur rechtstreeks bij SealDoc inschieten en eruit komt een PDF/A-3 die voldoet aan dezelfde regels als alles wat door onze custody-pipeline gaat: archiefformaat, embedded XML waar van toepassing, optionele RFC 3161-tijdstempel, optioneel evidence pack, idempotency-key support. Eén HTTP-request, één antwoord, klaar voor uw retentie-bucket.

Wat er nieuw is

Twee nieuwe endpoints en een verhardende stap in de evidence packs.

POST /api/documents/generate

Stuur Markdown of HTML, krijg een PDF/A-3 terug. De HTML-sanitizer (zie ADR-0013) strijkt server-side alle scripts, iframes, externe images en CSS-url()-verwijzingen weg, omdat een document dat u archiveert niet later via een externe asset alsnog van inhoud mag veranderen. Maximum 100KB content per request: groot genoeg voor een typisch compliance-rapport, klein genoeg om misbruik te voorkomen. Idempotency-Key wordt ondersteund, wat betekent dat een retry uit n8n geen tweede PDF in uw archief oplevert.

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\nAlle 142 facturen verwerkt zonder afwijkingen.",
    "title": "Q1 2026 Compliance Report",
    "timestampRfc3161": true,
    "generateEvidencePack": true
  }'

POST /api/invoices/generate

Hetzelfde principe, maar voor facturen. U stuurt een gestructureerde JSON-payload (verkoper, koper, regels, totalen, datums) en u krijgt een Factur-X 1.0 BASIC PDF/A-3 terug met de CII-XML al ingesloten. Geen tussenstap waarin u zelf nog een visuele PDF moet maken die we daarna semantisch moeten matchen.

Twee dingen die u moet weten voordat u dit produceert.

VAT-leading verkoper-validatie. Het BTW-nummer in seller.vatNumber moet overeenkomen met het BTW-nummer van uw eigen tenant. We canonicaliseren beide kanten (hoofdletters, punten, spaties weg) voor de vergelijking, dus nl 1234.56789.b01 matcht prima met NL123456789B01. Komt het niet overeen, dan krijgt u een 403 met een duidelijke foutcode. Dit is geen bureaucratie: het voorkomt dat één tenant facturen kan uitschrijven op naam van een andere. CompanyName-drift tussen request en tenant blokkeren we niet (handelsnamen veranderen), maar we loggen het wel als audit-warning zodat het traceerbaar is.

Billing profile moet compleet zijn. VAT, Address, PostalCode, City en Country op uw tenant zijn samen de identiteit van de verkoper op de factuur. Mist er één, dan krijgt u 412 met de lijst ontbrekende velden. Dat repareert u één keer in het portaal en daarna nooit meer.

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

Voor terugkerende facturatie geldt: gebruik altijd een expliciete Idempotency-Key. We doen geen automatische deduplicatie op payload-hash, want twee identieke maandfacturen binnen 24 uur kunnen volstrekt legitiem zijn (correctie, herfacturatie, dubbel abonnement op één entiteit). Idempotency-Key in handen van uw flow, garantie van geen duplicaten in onze hand.

Manifest hash-chain v2.0 in evidence packs

Een minder zichtbare, maar voor auditors belangrijke verharding. Elk evidence pack bevat nu een MANIFEST.json met per bestand de SHA256 en de bytes-grootte, alfabetisch gesorteerd op naam, in canonical JSON (camelCase, geen indentatie). Daarnaast staat er een manifest.sha256: de SHA256 van die canonical-manifestbytes.

Waarom twee niveaus. Per-file hashes detecteren content-tampering van een individueel bestand. De manifest-hash detecteert tampering van de bestandsset zelf: het stilletjes weghalen van een bijlage uit de ZIP en het manifest navenant herschrijven. Dat tweede was met v1.0 nog mogelijk; met v2.0 niet meer zonder dat een verifier het ziet.

Belangrijk: dit is geen cryptografische handtekening, en daarom heet het ook geen manifest.signature. De naamgeving reflecteert wat het is, een hash-chain. Een latere sprint voegt mogelijk Cosign of een door SealDoc beheerde sleutel toe voor non-repudiation; dán wordt manifest.signature de juiste naam, en niet eerder.

Een echt voorbeeld: Stripe-webhook naar gearchiveerde Factur-X

Concreet hoe deze drie wijzigingen samen een hele klasse van workflows mogelijk maken die voorheen niet bestonden.

Stel: u verkoopt SaaS-abonnementen via Stripe. Bij elke succesvolle invoice.payment_succeeded webhook wilt u een Nederlandse Factur-X-factuur in uw eigen archief, los van wat Stripe genereert (Stripe’s PDF is geen PDF/A-3, geen Factur-X, en heeft geen RFC 3161-tijdstempel).

De flow in n8n of Make:

  1. Stripe Trigger vangt invoice.payment_succeeded.
  2. Function-node mapt het Stripe-object naar een SealDoc invoice payload. customer_address wordt buyer, lines wordt opgebouwd uit lines.data, het BTW-nummer van uw eigen onderneming staat statisch in de seller-blok.
  3. HTTP Request-node doet een POST /api/invoices/generate met Idempotency-Key: stripe-{{$json.id}}. Stripe levert dezelfde webhook gerust drie keer; uw archief krijgt één factuur.
  4. HTTP Response-node krijgt direct een PDF/A-3-stream plus een evidence pack URL. Schrijf de PDF naar uw retentie-bucket, sla de evidence pack URL op in uw boekhoud-database.

Dat is het. Geen aparte rendering-step, geen template-engine die u zelf moet onderhouden, geen XML die u handmatig moet construeren. De Factur-X-XML rolt uit onze kant van de muur, conform 1.0 BASIC, en past in elk PEPPOL-traject dat een ontvanger kan vragen.

Wat het oplost

De drie veranderingen pakken samen één probleem aan, en dat is de afstand tussen “ik heb gestructureerde data” en “ik heb een document dat een Belgische of Franse fiscus accepteert als origineel”. Voor Sprint 49 was die afstand een derde partij. Sinds Sprint 50 is het een HTTP-request.

Voor wie zijn flow al draait via n8n-nodes-sealdoc of de Make-app: de nieuwe endpoints zitten in de volgende node-versie. Voor wie direct tegen de REST-API praat: ze zijn vandaag al live.

En terwijl we toch bezig waren: jaarbetaling

Een kleinere wijziging, maar verzocht door genoeg klanten om te noemen. In de abonnement-instellingen kunt u nu wisselen tussen Maandelijks en Jaarlijks. De jaartarieven zijn PriceMonthly × 10, oftewel twee maanden gratis, en Mollie incasseert om de twaalf maanden in plaats van om de dertig dagen. Geen migratie nodig, geen contractwijziging: zet de toggle om bij uw volgende verlengmoment.

De compliance-plumbing is opgelost. Wat u doet met de tijd die u terugwint, is aan u.


← Back to all articles