← Back to all articles

How to generate a compliant Peppol invoice with n8n and SealDoc

SealDoc Team · · 6 min read

Generating a compliant Peppol invoice involves more moving parts than it looks. You need a valid CII XML document, the right EN 16931 profile, a PDF/A-3 container, an embedded XML attachment, and ideally an RFC 3161 timestamp before it goes into your archive. Doing all of that from scratch in a custom integration is weeks of work.

This tutorial shows how to wire the same result together in n8n in roughly 30 minutes, using the n8n-nodes-sealdoc community node. No custom code. The workflow covers invoice generation, job polling, and result storage.

Prerequisites

Before you start, you need:

  • An n8n instance running version 1.0 or later, either self-hosted or n8n Cloud. The community node works on both.
  • A SealDoc account on the Starter plan or above. The invoice generation and timestamping features are available from Starter.
  • A SealDoc API key. Generate one in the SealDoc dashboard under Settings, then API Keys. Copy it immediately; we show it once.

Install the community node on your n8n instance:

npm i n8n-nodes-sealdoc

Restart n8n after installation. You will now see the SealDoc node available in the node panel under the Community category.

What the workflow does

This workflow takes invoice data (seller, buyer, line items, amounts) and produces a fully compliant Factur-X PDF/A-3 with embedded CII XML. The steps are:

  1. A trigger provides the invoice data as JSON (from a webhook, a database query, a form submission, or any other n8n source).
  2. The SealDoc Invoice.Generate node sends the structured data to the SealDoc API and returns a job ID.
  3. The SealDoc Job.Get node polls until the job completes and returns a download URL for the generated document.
  4. A final node stores the result to your archive location of choice.

Step 1: set up the trigger

Create a new workflow. Add a Webhook trigger node. Set the HTTP method to POST. This gives you a URL you can call with invoice data from any system that can make an HTTP request.

For testing, you can also use an Execute Workflow Trigger or a Manual Trigger with static data. The important thing is that the trigger outputs a JSON object with your invoice fields. We will reference those fields in the next step.

A minimal example of the invoice data your trigger should provide:

{
  "invoiceNumber": "INV-2026-00042",
  "issueDate": "2026-05-06",
  "dueDate": "2026-06-05",
  "seller": {
    "name": "Acme BV",
    "vatNumber": "NL123456789B01",
    "address": "Herengracht 1, 1000 AA Amsterdam, NL",
    "iban": "NL91ABNA0417164300"
  },
  "buyer": {
    "name": "Widget GmbH",
    "vatNumber": "DE987654321",
    "address": "Hauptstrasse 10, 10115 Berlin, DE"
  },
  "lines": [
    {
      "description": "Consulting services May 2026",
      "quantity": 10,
      "unitPrice": 150.00,
      "vatRate": 21
    }
  ],
  "currency": "EUR"
}

Step 2: add the Invoice.Generate node

Add a SealDoc node to the canvas. Set:

  • Resource: Invoice
  • Operation: Generate
  • Profile: EN 16931 (use EXTENDED if your buyer or mandate requires additional fields)
  • Embed timestamp: ON (this attaches an RFC 3161 timestamp to the output document)
  • Input data: map from the trigger output using n8n expressions, e.g. {{ $json.invoiceNumber }} for the invoice number, {{ $json.seller.vatNumber }} for the seller VAT number, and so on for each field.

In the Credentials section, select or create a SealDoc API credential. Paste your API key into the Secret field.

Click Execute Node to test. If the API key is valid and the invoice data is complete, you will receive a response like:

{
  "jobId": "job_9kxQr2mPLv",
  "status": "pending",
  "estimatedSeconds": 4
}

The job has been queued. The SealDoc API processes it asynchronously so that a slow PDF rendering does not block your workflow execution.

Step 3: add the Job.Get node with polling

Add a second SealDoc node. Set:

  • Resource: Job
  • Operation: Get
  • Job ID: {{ $node["SealDoc Invoice Generate"].json.jobId }}

Wrap this node in n8n’s Wait node set to a 3-second interval, with a loop back that checks whether status equals completed or failed. Most invoices complete in under 5 seconds. Set the maximum iteration count to 10 to avoid an infinite loop on a genuinely stuck job.

When status is completed, the Job.Get response includes:

  • downloadUrl: a time-limited URL (valid for 10 minutes) pointing to the generated PDF/A-3 document.
  • evidencePackUrl: a ZIP archive containing the original inputs, the output PDF/A-3, the embedded Factur-X XML, the RFC 3161 timestamp token, and a manifest hash file. This is what you hand to an auditor.
  • facturXProfile: the profile actually used (confirms EN 16931 or EXTENDED).
  • timestampedAt: the UTC timestamp of the RFC 3161 token, which is the legally significant creation time.

If status is failed, check the failureReason field. Common values are validation_failed (a required field was missing from your input), unsupported_currency (SealDoc supports EUR, USD, GBP, CHF, PLN, CZK and a growing list), and quota_exceeded (your monthly job limit is reached).

Step 4: store the result

Add whichever storage node fits your stack. Common options:

  • HTTP Request node to PUT the file to your own document management system or MinIO bucket.
  • Google Drive or Dropbox node if you use cloud storage for your archive.
  • FTP/SFTP node for on-premise storage.
  • Move Binary Data plus Write Binary File if n8n is self-hosted and you want to write directly to a filesystem path.

Use the downloadUrl from the Job.Get response to fetch the binary file, then pass it to your storage node. Also store the evidencePackUrl separately if your retention policy requires it, which it should for invoices above a certain value or in regulated industries.

What you get in the output

The generated file is a PDF/A-3B document. Opening it in any PDF viewer shows a rendered invoice. The embedded attachment factur-x.xml contains the full CII XML, readable by accounting software that supports Factur-X or ZUGFeRD import. The RFC 3161 timestamp is embedded in the PDF’s document information dictionary and is independently verifiable.

If you run the output through a validator (our free Validator tool accepts the file directly), you will see a green result for PDF/A-3B conformance, EN 16931 schema validity, and Factur-X attachment presence.

Next steps

This tutorial covers the core generation flow. SealDoc’s API also supports:

  • Peppol lookup: before generating an invoice, check whether the buyer is reachable on Peppol via the Peppol resource in the SealDoc node.
  • Validation only: submit an existing Factur-X or XRechnung file for schema validation without generating a new document.
  • Batch generation: submit multiple invoice payloads in a single API call for high-volume scenarios.

Full API documentation is on our developers page, with OpenAPI spec and copy-pasteable curl examples for every endpoint.


← Back to all articles