Des données au PDF scellé en un seul appel API
La question que nous entendions le plus souvent de la part des clients ayant une stack d’automatisation déjà fonctionnelle était brève : “Pourquoi dois-je rendre ma facture ailleurs avant de pouvoir vous la confier ?” Question légitime. Un flux n8n ou Make qui voulait transformer une ligne de base de données ou un payload de webhook en un PDF de conformité propre se bloquait précisément sur cette étape. Vous aviez les données. Vous vouliez un PDF/A-3 scellé. Entre les deux se trouvait un outil tiers, avec son propre tarif, sa propre limite de débit et son propre système de templates qui se disputait avec notre validateur d’entrée à chaque changement de réglementation.
Cet intermédiaire a disparu. Depuis les Sprints 49 et 50, vous pouvez envoyer du Markdown ou une facture JSON directement à SealDoc et recevoir en retour un PDF/A-3 qui répond aux mêmes règles que tout ce qui passe par notre pipeline de custody : format d’archive, XML embarqué le cas échéant, horodatage RFC 3161 optionnel, evidence pack optionnel, prise en charge de l’Idempotency-Key. Une requête HTTP, une réponse, prêt pour votre bucket de rétention.
Ce qui est nouveau
Deux nouveaux endpoints et un durcissement dans les evidence packs.
POST /api/documents/generate
Envoyez du Markdown ou du HTML, recevez un PDF/A-3. Le sanitizer HTML (voir ADR-0013) supprime côté serveur tous les scripts, iframes, images externes et références CSS url(), parce qu’un document que vous archivez ne doit pas pouvoir voir son contenu changer silencieusement plus tard via un asset externe. Maximum 100 Ko de contenu par requête : assez grand pour un rapport de conformité classique, assez petit pour décourager l’abus. L’Idempotency-Key est pris en charge, ce qui signifie qu’un retry depuis votre flux n8n ne produit pas un second PDF dans votre archive.
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
Même principe, mais pour les factures. Vous envoyez un payload JSON structuré (vendeur, acheteur, lignes, totaux, dates) et vous recevez un PDF/A-3 Factur-X 1.0 BASIC avec le XML CII déjà embarqué. Plus d’étape intermédiaire dans laquelle vous devez d’abord rendre un PDF visuel que nous devrions ensuite faire correspondre sémantiquement.
Deux choses à savoir avant de mettre en production.
Validation vendeur orientée TVA. Le numéro de TVA dans seller.vatNumber doit correspondre au numéro de TVA de votre propre tenant. Nous canonisons les deux côtés (majuscules, points et espaces retirés) avant la comparaison, donc nl 1234.56789.b01 correspond très bien à NL123456789B01. S’ils ne correspondent pas, vous obtenez un 403 avec un code d’erreur explicite. Ce n’est pas de la bureaucratie : cela empêche un tenant d’émettre des factures au nom d’un autre. La dérive du CompanyName entre la requête et le tenant n’est pas bloquée (les noms commerciaux changent), mais elle est journalisée comme avertissement d’audit afin de rester traçable.
Le profil de facturation doit être complet. TVA, Adresse, Code postal, Ville et Pays sur votre tenant forment ensemble l’identité du vendeur sur la facture. S’il en manque un, vous obtenez un 412 avec la liste des champs manquants. Vous corrigez cela une fois dans le portail et plus jamais ensuite.
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
}'
Pour la facturation récurrente, la règle est simple : envoyez toujours un Idempotency-Key explicite. Nous ne dédupliquons pas automatiquement sur le hash du payload, parce que deux factures mensuelles identiques en moins de 24 heures peuvent être parfaitement légitimes (correction, réémission, double abonnement sur une même entité juridique). Idempotency-Key entre les mains de votre flux, garantie de non-duplication entre les nôtres.
Manifest hash-chain v2.0 dans les evidence packs
Un durcissement moins visible, mais important pour les auditeurs. Chaque evidence pack contient désormais un MANIFEST.json avec, pour chaque fichier, le SHA256 et la taille en octets, trié alphabétiquement par nom, en JSON canonique (camelCase, sans indentation). À côté se trouve un manifest.sha256 : le SHA256 de ces octets de manifeste canoniques.
Pourquoi deux niveaux. Les hashes par fichier détectent l’altération du contenu d’un fichier individuel. Le hash du manifeste détecte l’altération de l’ensemble de fichiers lui-même : le retrait silencieux d’une pièce jointe du ZIP et la réécriture du manifeste en conséquence. Cette dernière manipulation était encore possible sous v1.0 ; sous v2.0 elle ne l’est plus sans qu’un vérificateur le remarque.
Important : il ne s’agit pas d’une signature cryptographique, et c’est pour cela que le fichier ne s’appelle pas manifest.signature. Le nommage reflète ce que c’est, une hash-chain. Un sprint futur ajoutera peut-être Cosign ou une clé gérée par SealDoc pour la non-répudiation ; à ce moment-là, manifest.signature deviendra le bon nom, et pas avant.
Un exemple réel : webhook Stripe vers Factur-X archivée
Concrètement, voici comment les trois changements ouvrent ensemble une catégorie de workflows qui n’existait pas auparavant.
Imaginons que vous vendez des abonnements SaaS via Stripe. À chaque webhook invoice.payment_succeeded réussi, vous voulez une facture Factur-X française dans votre propre archive, distincte de ce que Stripe génère (le PDF de Stripe n’est pas un PDF/A-3, n’est pas Factur-X et n’a pas d’horodatage RFC 3161).
Le flux dans n8n ou Make :
- Stripe Trigger capte
invoice.payment_succeeded. - Function node mappe l’objet Stripe vers un payload de facture SealDoc.
customer_addressdevientbuyer,linesest construit à partir delines.data, votre propre numéro de TVA reste statique dans le bloc seller. - HTTP Request node effectue un
POST /api/invoices/generateavecIdempotency-Key: stripe-{{$json.id}}. Stripe peut très bien retransmettre le même webhook trois fois ; votre archive reçoit exactement une facture. - HTTP Response node reçoit un flux PDF/A-3 et une URL d’evidence pack. Écrivez le PDF dans votre bucket de rétention, stockez l’URL de l’evidence pack dans votre base comptable.
Voilà. Pas d’étape de rendu séparée, pas de moteur de templates à maintenir, pas de XML à construire à la main. Le XML Factur-X sort de notre côté du mur, conforme à 1.0 BASIC, et s’intègre dans n’importe quel pipeline PEPPOL qu’un destinataire peut demander.
Ce que cela résout
Les trois changements traitent ensemble un seul problème, l’écart entre “j’ai des données structurées” et “j’ai un document que l’administration fiscale française ou belge accepte comme original”. Avant le Sprint 49, cet écart était un tiers. Depuis le Sprint 50, c’est une requête HTTP.
Si votre flux passe déjà par n8n-nodes-sealdoc ou par l’application Make, les nouveaux endpoints arrivent dans la prochaine version du node. Si vous parlez directement à l’API REST, ils sont en ligne aujourd’hui.
Et tant qu’on y était : la facturation annuelle
Un changement plus modeste, mais suffisamment de clients l’ont demandé pour qu’il mérite d’être mentionné. Dans les paramètres d’abonnement, vous pouvez désormais basculer entre Mensuel et Annuel. Les tarifs annuels sont PriceMonthly × 10, soit deux mois gratuits, et Mollie prélève tous les douze mois au lieu de tous les trente jours. Pas de migration, pas de modification de contrat : actionnez le toggle à votre prochain renouvellement.
La plomberie de la conformité est résolue. Ce que vous faites du temps regagné vous appartient.