Dai dati al PDF sigillato in una sola chiamata API
La domanda che ci sentivamo rivolgere più spesso da clienti con uno stack di automazione già funzionante era breve: “Perché devo prima rendere la mia fattura altrove prima di potervela consegnare?” Una domanda legittima. Un flusso n8n o Make che voleva trasformare una riga di database o un payload di webhook in un PDF di compliance pulito si bloccava esattamente su quel passaggio. Lei aveva i dati. Voleva un PDF/A-3 sigillato. Tra i due si trovava uno strumento di terze parti con il proprio listino, il proprio rate limit e il proprio sistema di template che litigava con il nostro validatore di ingresso a ogni cambio di normativa.
Quella stazione intermedia non c’è più. Dagli Sprint 49 e 50 può inviare Markdown o una fattura JSON direttamente a SealDoc e ricevere indietro un PDF/A-3 che rispetta le stesse regole di tutto ciò che passa nella nostra pipeline di custody: formato d’archivio, XML embedded dove pertinente, marca temporale RFC 3161 opzionale, evidence pack opzionale, supporto Idempotency-Key. Una richiesta HTTP, una risposta, pronto per il suo bucket di retention.
Cosa c’è di nuovo
Due nuovi endpoint e un irrobustimento sugli evidence pack.
POST /api/documents/generate
Invia Markdown o HTML, riceve un PDF/A-3. Il sanitizer HTML (vedi ADR-0013) rimuove lato server tutti gli script, gli iframe, le immagini esterne e i riferimenti CSS url(), perché un documento che lei archivia non deve poter cambiare silenziosamente il proprio contenuto in seguito tramite un asset esterno. Massimo 100 KB di contenuto per richiesta: abbastanza grande per un tipico report di compliance, abbastanza piccolo da scoraggiare gli abusi. L’Idempotency-Key è supportato, il che significa che un retry dal suo flusso n8n non produce un secondo PDF nel suo archivio.
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
Stesso principio, ma per le fatture. Invia un payload JSON strutturato (venditore, acquirente, righe, totali, date) e riceve un PDF/A-3 Factur-X 1.0 BASIC con l’XML CII già embedded. Niente passaggio intermedio in cui deve prima rendere un PDF visivo che noi dobbiamo poi accoppiare semanticamente.
Due cose da sapere prima di mettere in produzione.
Validazione venditore guidata dalla partita IVA. Il numero di partita IVA in seller.vatNumber deve corrispondere alla partita IVA del suo tenant. Canonizziamo entrambi i lati (maiuscole, punti e spazi rimossi) prima del confronto, quindi nl 1234.56789.b01 corrisponde benissimo a NL123456789B01. Se non corrispondono, riceve un 403 con un codice di errore chiaro. Non è burocrazia: impedisce a un tenant di emettere fatture a nome di un altro. La deriva del CompanyName tra richiesta e tenant non viene bloccata (i nomi commerciali cambiano), ma viene registrata come avviso di audit per restare tracciabile.
Il profilo di fatturazione deve essere completo. IVA, Indirizzo, CAP, Città e Paese sul tenant compongono insieme l’identità del venditore in fattura. Se ne manca uno, riceve un 412 con la lista dei campi mancanti. Lo si sistema una volta nel portale e mai più dopo.
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
}'
Per la fatturazione ricorrente la regola è semplice: invii sempre un Idempotency-Key esplicito. Non deduplichiamo automaticamente sull’hash del payload, perché due fatture mensili identiche entro 24 ore possono essere del tutto legittime (una nota di rettifica, una riemissione, un doppio abbonamento su una stessa entità giuridica). Idempotency-Key nelle mani del suo flusso, garanzia di non-duplicazione nelle nostre.
Manifest hash-chain v2.0 negli evidence pack
Un irrobustimento meno visibile ma, per gli auditor, importante. Ogni evidence pack contiene ora un MANIFEST.json con SHA256 e dimensione in byte per ciascun file, ordinato alfabeticamente per nome, in JSON canonico (camelCase, senza indentazione). Accanto si trova un manifest.sha256: lo SHA256 di quei byte canonici del manifest.
Perché due livelli. Gli hash per file rilevano la manomissione del contenuto di un singolo file. L’hash del manifest rileva la manomissione dell’insieme di file stesso: la rimozione silenziosa di un allegato dallo ZIP e la riscrittura coerente del manifest. Quest’ultima era ancora possibile sotto v1.0; sotto v2.0 non lo è più senza che un verificatore se ne accorga.
Importante: non è una firma crittografica, ed è proprio per questo che il file non si chiama manifest.signature. La denominazione riflette ciò che è, una hash-chain. Uno sprint futuro potrà aggiungere Cosign o una chiave gestita da SealDoc per il non ripudio; a quel punto manifest.signature diventerà il nome corretto, e non prima.
Un esempio reale: webhook Stripe verso Factur-X archiviata
Concretamente, ecco come i tre cambiamenti aprono insieme una classe di flussi che prima non esisteva.
Supponiamo che lei venda abbonamenti SaaS tramite Stripe. A ogni webhook invoice.payment_succeeded riuscito vuole una fattura Factur-X italiana nel proprio archivio, separata da ciò che Stripe genera (il PDF di Stripe non è PDF/A-3, non è Factur-X e non ha marca temporale RFC 3161).
Il flusso in n8n o Make:
- Stripe Trigger intercetta
invoice.payment_succeeded. - Function node mappa l’oggetto Stripe su un payload di fattura SealDoc.
customer_addressdiventabuyer,linesviene costruito dalines.data, la sua partita IVA resta statica nel blocco seller. - HTTP Request node esegue un
POST /api/invoices/generateconIdempotency-Key: stripe-{{$json.id}}. Stripe può tranquillamente riconsegnare lo stesso webhook tre volte; il suo archivio riceve esattamente una fattura. - HTTP Response node riceve uno stream PDF/A-3 e un URL di evidence pack. Scrive il PDF nel suo bucket di retention, salva l’URL dell’evidence pack nel suo database contabile.
Tutto qui. Nessun passaggio di rendering separato, nessun motore di template da mantenere, nessuna XML da costruire a mano. L’XML Factur-X esce dalla nostra parte del muro, conforme a 1.0 BASIC, e si inserisce in qualsiasi pipeline PEPPOL che un destinatario possa richiedere.
Cosa risolve
I tre cambiamenti affrontano insieme un unico problema, la distanza tra “ho dei dati strutturati” e “ho un documento che l’Agenzia delle Entrate francese o belga accetta come originale”. Prima dello Sprint 49 quella distanza era una terza parte. Dallo Sprint 50 è una richiesta HTTP.
Se il suo flusso passa già per n8n-nodes-sealdoc o per l’app Make, i nuovi endpoint arrivano nella prossima versione del node. Se parla direttamente con l’API REST, sono live oggi.
E già che c’eravamo: fatturazione annuale
Un cambiamento più piccolo, ma abbastanza clienti l’hanno chiesto da meritare una menzione. Nelle impostazioni dell’abbonamento può ora alternare tra Mensile e Annuale. I prezzi annuali sono PriceMonthly × 10, ovvero due mesi gratis, e Mollie addebita ogni dodici mesi invece che ogni trenta giorni. Nessuna migrazione, nessuna modifica contrattuale: sposti il toggle al prossimo momento di rinnovo.
La plumbing della compliance è risolta. Cosa fare con il tempo recuperato dipende da Lei.