← Back to all articles

De los datos al PDF sellado en una sola llamada API

SealDoc Team · · 6 min read

La pregunta que más oíamos a clientes con un stack de automatización ya en marcha era breve: “¿Por qué tengo que renderizar mi factura en otro sitio antes de poder entregársela?” Una pregunta justa. Un flujo de n8n o Make que quería convertir una fila de base de datos o un payload de webhook en un PDF de cumplimiento limpio se atascaba precisamente en ese paso. Usted tenía los datos. Quería un PDF/A-3 sellado. Entre ambos había una herramienta de terceros con su propia tarifa, su propio rate limit y su propio sistema de plantillas que se peleaba con nuestro validador de entrada cada vez que cambiaba la normativa.

Ese intermediario ha desaparecido. Desde los Sprints 49 y 50, puede enviar Markdown o una factura JSON directamente a SealDoc y recibir de vuelta un PDF/A-3 que cumple las mismas reglas que todo lo que pasa por nuestra pipeline de custody: formato de archivo, XML embebido cuando proceda, sello de tiempo RFC 3161 opcional, evidence pack opcional, soporte de Idempotency-Key. Una petición HTTP, una respuesta, listo para su bucket de retención.

Lo que es nuevo

Dos endpoints nuevos y una mejora de endurecimiento en los evidence packs.

POST /api/documents/generate

Envíe Markdown o HTML, reciba un PDF/A-3. El sanitizer HTML (véase ADR-0013) elimina en el servidor todos los scripts, iframes, imágenes externas y referencias CSS url(), porque un documento que usted archiva no debe poder cambiar silenciosamente su contenido más adelante a través de un activo externo. Máximo 100 KB de contenido por petición: lo bastante grande para un informe de cumplimiento típico, lo bastante pequeño para desincentivar el abuso. Idempotency-Key está soportado, lo que significa que un retry desde su flujo de n8n no produce un segundo PDF en su archivo.

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

Mismo principio, pero para facturas. Usted envía un payload JSON estructurado (vendedor, comprador, líneas, totales, fechas) y recibe un PDF/A-3 Factur-X 1.0 BASIC con el XML CII ya embebido. No hay un paso intermedio en el que tenga que renderizar primero un PDF visual que después tengamos que emparejar semánticamente.

Dos cosas que conviene saber antes de pasar esto a producción.

Validación de vendedor liderada por el IVA. El número de IVA en seller.vatNumber debe coincidir con el número de IVA de su propio tenant. Canonizamos ambos lados (mayúsculas, puntos y espacios eliminados) antes de comparar, así que nl 1234.56789.b01 casa perfectamente con NL123456789B01. Si no coinciden, recibe un 403 con un código de error claro. Esto no es burocracia: impide que un tenant emita facturas en nombre de otro. La deriva de CompanyName entre la petición y el tenant no se bloquea (los nombres comerciales cambian), pero se registra como advertencia de auditoría para que quede trazable.

El perfil de facturación debe estar completo. IVA, Dirección, Código Postal, Ciudad y País en su tenant forman juntos la identidad del vendedor en la factura. Si falta uno, recibe un 412 con la lista de campos que faltan. Eso lo arregla una vez en el portal y nunca más.

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

Para facturación recurrente la regla es simple: envíe siempre un Idempotency-Key explícito. No deduplicamos automáticamente por hash de payload, porque dos facturas mensuales idénticas en menos de 24 horas pueden ser perfectamente legítimas (una rectificación, una reemisión, un doble abono sobre una misma entidad jurídica). Idempotency-Key en manos de su flujo, garantía de no duplicados en las nuestras.

Manifest hash-chain v2.0 en los evidence packs

Un endurecimiento menos visible pero, para los auditores, importante. Cada evidence pack contiene ahora un MANIFEST.json con SHA256 y tamaño en bytes por archivo, ordenado alfabéticamente por nombre, en JSON canónico (camelCase, sin indentación). Junto a él hay un manifest.sha256: el SHA256 de esos bytes canónicos del manifiesto.

Por qué dos niveles. Los hashes por archivo detectan la manipulación del contenido de cualquier archivo individual. El hash del manifiesto detecta la manipulación del propio conjunto de archivos: la eliminación silenciosa de un adjunto del ZIP y la reescritura correspondiente del manifiesto. Esto último todavía era posible bajo v1.0; bajo v2.0 ya no lo es sin que un verificador lo detecte.

Importante: esto no es una firma criptográfica, y por eso no se llama manifest.signature. La nomenclatura refleja lo que es, una hash-chain. Un sprint futuro podrá añadir Cosign o una clave propia de SealDoc para no repudio; en ese momento, manifest.signature será el nombre correcto, y no antes.

Un ejemplo real: webhook de Stripe a Factur-X archivada

En concreto, así abren juntos los tres cambios una clase de flujos que antes no existía.

Supongamos que vende suscripciones SaaS a través de Stripe. En cada webhook invoice.payment_succeeded con éxito, quiere una factura Factur-X española en su propio archivo, separada de lo que Stripe genera (el PDF de Stripe no es PDF/A-3, no es Factur-X y no tiene sello de tiempo RFC 3161).

El flujo en n8n o Make:

  1. Stripe Trigger captura invoice.payment_succeeded.
  2. Function node mapea el objeto Stripe a un payload de factura SealDoc. customer_address se convierte en buyer, lines se construye a partir de lines.data, su propio número de IVA va estático en el bloque seller.
  3. HTTP Request node hace un POST /api/invoices/generate con Idempotency-Key: stripe-{{$json.id}}. Stripe puede reentregar tranquilamente el mismo webhook tres veces; su archivo recibe exactamente una factura.
  4. HTTP Response node recibe un stream de PDF/A-3 y una URL de evidence pack. Escriba el PDF en su bucket de retención, almacene la URL del evidence pack en su base de datos contable.

Eso es todo. Sin paso de renderizado aparte, sin motor de plantillas que mantener, sin XML que construir a mano. El XML Factur-X sale de nuestro lado del muro, conforme a 1.0 BASIC, y encaja en cualquier pipeline PEPPOL que un destinatario pueda solicitar.

Lo que resuelve

Los tres cambios abordan juntos un único problema, la distancia entre “tengo datos estructurados” y “tengo un documento que la Agencia Tributaria francesa o belga acepta como original”. Antes del Sprint 49, esa distancia era un tercero. Desde el Sprint 50, es una petición HTTP.

Si su flujo ya corre por n8n-nodes-sealdoc o por la aplicación de Make, los nuevos endpoints llegan en la próxima versión del node. Si habla directamente con la API REST, están en producción hoy.

Y ya que estábamos: facturación anual

Un cambio menor, pero suficientes clientes lo pidieron como para mencionarlo. En los ajustes de suscripción ahora puede alternar entre Mensual y Anual. Las tarifas anuales son PriceMonthly × 10, es decir, dos meses gratis, y Mollie carga cada doce meses en lugar de cada treinta días. Sin migración, sin cambio de contrato: active el toggle en su próximo momento de renovación.

La fontanería del cumplimiento está resuelta. Lo que haga con el tiempo recuperado depende de usted.


← Back to all articles