Automation MCP Server Features Blog Pricing Contact
Automation UBL Format

Peppol Invoice to PDF: Making UBL Invoices Readable for the Humans in the Loop

The Peppol network moves invoices as pure UBL XML, and that is exactly right until a person needs to look at one: an approver checking line items, a controller resolving a dispute, an auditor sampling the archive. This guide shows how to give every received invoice and credit note a readable PDF twin with one API call, where that call sits in a reception pipeline, and when JSON extraction into your own screens is the better answer.

Peppol solved invoice delivery and deliberately did not solve invoice reading. What lands at your access point is a UBL XML document: precise, validated, bookable by machines, and unreadable by everyone else in the building. Most days that is fine, because most invoices flow straight through. The problem surfaces on the exceptions, which is exactly where humans enter: an approver wants to see what line 7 actually says, a supplier calls about a dispute, an auditor samples last year's archive. This post is about giving those humans what they need without weakening the machine pipeline: a readable PDF twin for every received UBL invoice, generated on demand or on arrival, plus the JSON route for teams that would rather render the data in their own screens.

Complete UBL Toolkit

Everything you need to create, convert, validate, and preview UBL invoices, via REST API or online.

The network delivers data, not documents

An invoice received over Peppol is a UBL 2.1 document conforming to a profile, typically Peppol BIS Billing 3.0, or a national flavour such as NLCIUS in the Netherlands, EHF in Norway, or PINT internationally. It expresses the EN 16931 semantic model in UBL's cac:/cbc: element grammar, and a simple three-line invoice easily runs to several hundred lines of XML. There is no visual layer anywhere in the exchange: no PDF rides along, and the network neither requires nor standardizes one.

That design is a feature. Structured data books itself; PDFs need OCR and hope. But it means the receiving side inherits a small, permanent obligation: whenever a person must look at an invoice, something in your stack has to turn data into a document, or into a screen. The Peppol network overview covers the delivery half; this post is the reading half.


Who hits this problem

Accounts payable teams whose ERP books UBL automatically but shows approvers a wall of XML, or nothing, when a case needs eyes. Controllers and auditors who sample archived invoices and need a readable rendition of what was actually received, not a supplier's marketing PDF. Access point and service providers whose customers keep asking "can I see the invoice?" and who want a preview feature without building a rendering engine. Developers testing UBL generation, because visually scanning a rendered invoice catches a wrong total or a missing payment reference far faster than reading raw XML. If several of those sound familiar, the fix below is the same one call for all of them.


One call: UBL in, PDF out

The endpoint is POST /v1/render/ubl/to/pdf: multipart upload of the XML file, PDF back as the response body.

curl -X POST https://api.invoicexml.com/v1/render/ubl/to/pdf \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "[email protected]" \
  --output received-invoice.pdf

The same call from a Node.js reception handler, using nothing beyond standard fetch:

import { readFile, writeFile } from "node:fs/promises";

const form = new FormData();
form.append("file", new Blob([await readFile("received-invoice.xml")]), "received-invoice.xml");

const response = await fetch("https://api.invoicexml.com/v1/render/ubl/to/pdf", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.INVOICEXML_API_KEY}` },
  body: form
});
if (!response.ok) throw new Error(await response.text());

await writeFile("received-invoice.pdf", Buffer.from(await response.arrayBuffer()));

The PDF is generated fresh from the structured data and lays out every business term the document carries: parties with their identifiers, line items with quantities and prices, the VAT breakdown per category and rate, totals, payment instructions, and an "Electronic invoice details" section for the e-invoicing-specific fields (endpoint IDs, references, listed attachments). An optional language form field (en, de, fr) localizes the field labels; the invoice content itself is never translated.


Credit notes, profiles, and syntax quirks

Three details that matter in real Peppol traffic:

Credit notes are first-class. UBL uses a separate CreditNote root element, and a meaningful share of network traffic is credits. The renderer detects the root and labels the output as a credit note, so a credited amount never reads as a payable.

The profile is detected, not declared. BIS Billing 3.0, PINT, NLCIUS, EHF, plain EN 16931 UBL: the renderer reads the CustomizationID and handles the document accordingly. A reception pipeline does not need to branch by trading partner.

Mixed-syntax reality is covered. If your inbox also receives German XRechnung files (UBL or CII syntax) or standalone CII documents from ZUGFeRD-centric partners, the sibling endpoints render those, and the generic POST /v1/render/xml/to/pdf accepts any supported syntax and detects it from the root namespace. One route for everything is the simplest wiring; the any-XML rendering guide covers that pattern in depth.


Where rendering sits in a reception pipeline

The pattern that works in production is render on arrival, store beside the original. When the access point hands your system a document, the pipeline validates it, books or routes it, and in the same breath posts it to the render endpoint and stores the returned PDF next to the XML in the archive or DMS. Cost: one HTTP call per invoice, a second or two of latency off the critical path. Payoff: every screen in your organisation that can show a PDF (the ERP attachment pane, the approval email, the DMS preview) now shows the invoice, with no per-viewer tooling and no "ask IT to open this file" tickets.

The lighter alternative is render on demand: store only the XML and call the endpoint when a person actually clicks. This suits high-volume, low-touch flows where under one percent of invoices are ever looked at; rendering is stateless and deterministic, so the PDF a person sees in three years is generated from the same data either way. Both patterns coexist fine with validation: rendering does not check compliance, so keep the UBL validation endpoint at the boundary where documents enter, and treat the renderer purely as the presentation layer.


Attachments travelling inside the invoice

UBL invoices can carry supporting documents embedded in the XML itself, base64-encoded (BG-24 in EN 16931): timesheets, delivery confirmations, sometimes the supplier's own PDF rendition. The rendered preview lists them so reviewers see they exist; to actually open them, POST /v1/extract/attachments returns every embedded file as a ZIP, decoded exactly as the sender packed it. Externally referenced documents (a URL in the XML) are deliberately not fetched, so nothing in your pipeline silently calls out to third-party servers. The attachment extraction reference has the details.


Your own AP screens instead of a PDF

If you are building the surface people look at (an AP workbench, a customer-facing portal at an access point, an archive search UI) generating PDFs just to re-display them is a detour. POST /v1/extract/json reads the same UBL file into structured JSON, every business term under readable property names:

{
  "invoice": {
    "invoiceNumber": "PEP-2026-09182",
    "issueDate": "2026-09-10",
    "currency": "EUR",
    "seller": { "name": "Nordic Supply AB", "electronicAddress": { "identifier": "7300010000001", "schemeId": "0088" } },
    "buyer": { "name": "Beispiel Einkauf GmbH" },
    "totals": { "taxBasisTotalAmount": 6720.00, "taxTotalAmount": 1680.00, "duePayableAmount": 8400.00 },
    "vatBreakdowns": [ { "categoryCode": "S", "rate": 25, "taxableAmount": 6720.00, "taxAmount": 1680.00 } ],
    "lines": [ { "lineId": "1", "quantity": 40, "unitCode": "HUR", "lineNetAmount": 6720.00 } ]
  }
}

From there, your approval screen renders the fields in your own layout, your search index covers supplier names and amounts, and your portal shows the invoice natively on mobile, none of which a PDF does well. Extraction is deterministic parsing of the structured XML, no AI and no confidence scores, so the amounts on screen are the amounts in the document. Most mature integrations end up using both routes: JSON for the screens and the workflow, PDF for email, archive, and everyone outside the system.


The XML stays the original

Whatever you render or extract, the received UBL document remains the invoice. Legal retention attaches to the XML; the PDF is a derived copy with no standing of its own, and it says so in its role. Archive the original bytes as delivered by the network, keep derived artifacts clearly secondary, and regenerate them at will; because rendering is stateless, the same XML always yields the same reading.


Get started

Drop a received UBL file on the online preview to see the output on your own data, then create a free InvoiceXML account → and get 100 credits for free, no credit card required; the curl call above is the whole integration.

Related resources:


InvoiceXML is a REST API for European e-invoice compliance covering Peppol UBL, XRechnung, ZUGFeRD, Factur-X, and CII. Stateless processing, GDPR compliant by architecture, and callable from any stack in minutes.

Start free today

Ready to automate your invoices?

Validate, convert and embed compliant e-invoices through one API. Start your 30-day free trial. No credit card required.

GDPR Compliant No credit card required Setup in minutes
Peppol UBL
Factur-X
EN 16931
142 / 142 passed
Compliant
PDF/A-3 embedded