Peppol UBL e-invoicing with n8n: create, validate and preview e-invoices via REST API

If you run workflows for European clients and are dealing with invoices, Peppol UBL invoicing is probably already on your radar. Belgium made it mandatory for all B2B transactions from January 2026, every VAT-registered business must send and receive structured UBL invoices via the Peppol network simultaneously, no phase-in by company size. Netherlands, Norway, Denmark, and the rest of the Peppol cluster are in the same position.

I run InvoiceXML, a REST API for European e-invoice compliance, and I’ve been putting together n8n workflows around it. This post covers three practical UBL workflows you can build with the HTTP Request node today: generating a Peppol-ready UBL invoice, validating an incoming UBL file before ERP import, and rendering a UBL XML document as a human-readable PDF for approval workflows.

The compliance challenge is that UBL validation requires two separate Schematron layers, the base EN 16931 rules plus the Peppol BIS 3.0 overlay, which OpenPeppol updates quarterly. Running both in a self-hosted setup requires an XSLT 2.0 processor. Easier to call an API.

No custom nodes needed. Everything runs through the native HTTP Request node.


Workflow 1: Generate a Peppol BIS 3.0 UBL invoice

Use case: Your n8n workflow receives invoice data from a CRM, database webhook, or form submission and needs to produce a compliant UBL XML file for Peppol submission.

API docs: Create UBL Invoice using REST-API | InvoiceXML

HTTP Request node configuration:

Method:  POST
URL:     https://api.invoicexml.com/v1/create/ubl
Headers:
  Authorization: Bearer {{ $env.INVOICEXML_API_KEY }}
Body:    Form Data (multipart/form-data)

Body fields:

Field Value
InvoiceNumber {{ $json.invoiceNumber }}
IssueDate {{ $json.issueDate }}
PaymentDueDate {{ $json.dueDate }}
SellerName {{ $json.sellerName }}
SellerTaxId {{ $json.sellerVat }}
SellerStreet {{ $json.sellerStreet }}
SellerPostcode {{ $json.sellerPostcode }}
SellerCity {{ $json.sellerCity }}
SellerCountry BE
BuyerName {{ $json.buyerName }}
BuyerCountry {{ $json.buyerCountry }}
Currency EUR
TaxBasisTotal {{ $json.netAmount }}
TaxTotalAmount {{ $json.vatAmount }}
GrandTotalAmount {{ $json.grossAmount }}
PaymentMeansCode 58
IBAN {{ $json.iban }}
profile peppol-bis-3
Lines[0][description] {{ $json.lines[0].description }}
Lines[0][quantity] {{ $json.lines[0].qty }}
Lines[0][unitPrice] {{ $json.lines[0].unitPrice }}
Lines[0][lineTotal] {{ $json.lines[0].lineTotal }}
Lines[0][taxPercentage] 21
Lines[0][taxCategoryCode] S

The profile parameter controls which CustomizationID is embedded in the output. Supported values:

  • peppol-bis-3 - Belgium, Denmark, Sweden, Croatia, cross-border EU

  • nlcius - Netherlands public sector

  • ehf - Norway

  • xrechnung - Germany B2G (adds Leitweg-ID requirement)

  • en16931 - plain EN 16931, no CIUS overlay

    Response: UBL 2.1 XML as application/xml. Pass it to your Peppol access point’s API or save it to disk/S3/Google Drive for transmission.

Full workflow example:

Webhook trigger
  → Set node (map your data to invoice fields)
  → HTTP Request (create UBL)
  → IF node (check HTTP status = 200)
      → Write Binary File / Google Drive upload
      → HTTP Request to Peppol access point
  → Error branch: Slack/email notification

Workflow 2: Validate an incoming UBL invoice

Use case: Your workflow receives UBL invoices from suppliers via email attachment, SFTP, or a Peppol access point inbox. You want to screen them before they hit your ERP or accounting system.

The validation endpoint runs two Schematron layers, EN 16931 base rules plus the Peppol BIS 3.0 overlay declared in the document’s CustomizationID. Profile is detected automatically from the submitted XML.

API docs: Validate UBL Invoice using REST-API | InvoiceXML

HTTP Request node:

Method:  POST
URL:     https://api.invoicexml.com/v1/validate/ubl
Headers:
  Authorization: Bearer {{ $env.INVOICEXML_API_KEY }}
Body:    Form Data (multipart/form-data)
  file:  [binary input from previous node]

Response structure:

{
  "valid": false,
  "detail": "Validation failed with 2 error(s)",
  "data": {
    "detectedProfile": "Peppol BIS Billing 3.0",
    "documentType": "Invoice",
    "customizationId": "urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0"
  },
  "errors": {
    "friendly": [
      {
        "rule": "BR-CO-14",
        "layer": "en16931",
        "message": "Invoice total VAT amount does not match the sum of VAT breakdown amounts."
      },
      {
        "rule": "PEPPOL-EN16931-R004",
        "layer": "peppol-bis",
        "message": "Invoice type code should be 380 for a standard invoice."
      }
    ]
  }
}

Using the response in your workflow:

HTTP Request (validate)
  → IF node: {{ $json.valid }} === true
      → True branch: pass to ERP import node
      → False branch:
          → Code node: extract error messages
            {{ $json.errors.friendly.map(e => e.message).join('\n') }}
          → Slack node: notify AP team with plain-language errors
          → Move file to /rejected/ folder

The layer field on each error tells you whether it is a fundamental EN 16931 data problem (en16931) or a Peppol network-specific issue (peppol-bis), useful for routing: data errors go back to the supplier, Peppol config errors go to your access point provider.

Both valid and invalid invoices return HTTP 200, branch on valid, not on HTTP status code. This means you do not need error handling nodes for the expected invalid case.


Workflow 3: Render UBL as PDF for human approval

Use case: Your Peppol access point delivers UBL XML invoices to your AP inbox. Finance needs to approve invoices before payment but cannot read XML. This workflow renders each incoming UBL file as a clean PDF and sends it for approval before posting to your accounting system.

Method:  POST
URL:     https://api.invoicexml.com/v1/render/ubl/to/pdf
Headers:
  Authorization: Bearer {{ $env.INVOICEXML_API_KEY }}
Body:    Form Data (multipart/form-data)
  file:  [binary UBL XML from previous node]

Response is a binary PDF. Wire it into an approval workflow:

HTTP Request (render to PDF)
  → Send Email node
      To: finance@yourcompany.com
      Subject: Invoice approval required — {{ $json.invoiceNumber }}
      Attachment: [binary PDF response]
  → Wait node (wait for approval reply or webhook)
  → IF approved:
      → Post to accounting system
  → IF rejected:
      → Notify supplier

The rendered PDF shows the detected Peppol profile in the header, all line items, VAT breakdown, payment details, and IBAN, everything the approver needs without them ever opening an XML file.


Storing your API key safely

Store the InvoiceXML API key as an n8n credential, do not hardcode it in nodes:

Settings → Credentials → New → Generic Credential Type → HTTP Header Auth
Name:         InvoiceXML
Header Name:  Authorization
Header Value: Bearer YOUR_API_KEY

Then reference it in every HTTP Request node via the credential selector rather than the Headers field directly.


Try these tools online for free:

I am happy to answer questions about specific workflow configurations or field mappings for different ERP/CRM sources.