The awkward truth of e-invoice reception is that the sender picks the format. One customer's pipeline sees UN/CEFACT CII because their partners live in the ZUGFeRD world; the next sees UBL because everything arrives over Peppol; a German inbox sees XRechnung in either of its two syntaxes, plus credit notes, plus the occasional file nobody can place. Every one of those needs the same feature eventually: a human-readable PDF. POST /v1/render/xml/to/pdf exists so that feature is one route with no format switch in front of it: any supported invoice XML in, a clean PDF out, syntax detected from the file itself.
One route for a mixed inbox
The render family has four paths, and they are deliberately the same action:
| Route | When to prefer it |
|---|---|
POST /v1/render/xml/to/pdf | Mixed or unknown input; the pipeline route |
POST /v1/render/xrechnung/to/pdf | Explicitly an XRechnung flow; the path documents intent |
POST /v1/render/cii/to/pdf | Explicitly CII, e.g. XML extracted from ZUGFeRD/Factur-X hybrids |
POST /v1/render/ubl/to/pdf | Explicitly UBL, e.g. Peppol reception |
All four are permanent aliases with identical behaviour and auto-detection; none is deprecated. Detection is deterministic from the root element and namespace: CrossIndustryInvoice in the UN/CEFACT namespace is CII, Invoice or CreditNote in the OASIS UBL namespace is UBL. Because XRechnung, Peppol BIS 3.0, PINT, NLCIUS, and EHF are profiles of those two syntaxes, they all render without anything to declare. Credit notes are labelled as credit notes, so a credited amount never reads as a payable.
The endpoint
Multipart form upload, binary PDF response:
curl -X POST https://api.invoicexml.com/v1/render/xml/to/pdf \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "[email protected]" \
--output invoice-preview.pdf
Files up to 20 MB are accepted (invoice XML is typically a few kilobytes), processing is stateless (in memory for the duration of the request, discarded with the response), and the response is application/pdf with the original file name preserved with a .pdf extension.
What the PDF contains
The layout is generated fresh from the structured data; it is not a stylesheet over the XML, and it is not the visual layer of any hybrid the XML came from. Its design goal is completeness for the human in the loop: seller and buyer with their identifiers, line items with quantities, unit codes, and prices, allowances and charges, the VAT breakdown per category and rate, totals, payment details, and delivery and period information. On top of the standard body, an "Electronic invoice details" section surfaces what other renderers drop: the specification identifier, electronic addresses, legal registrations, line-level references and classifications, and any embedded attachments listed by file name. For reviewers, that section shows exactly what the machine-readable layer claims, which is what an ERP will actually book.
Options: language and logo
Two optional form fields shape the output. language (en, de, fr, default en) sets the field labels and date formatting; invoice content is never translated. logoUrl takes an absolute http(s) URL of a public PNG or JPEG (up to 2 MB) and prints it top-left, exactly as the create endpoints do, which matters when previews and generated invoices should look like siblings in the same product. Everything else is derived from the document.
Error behaviour you can build on
A pipeline route earns its keep on the bad files. Malformed XML, a root element that is neither CII nor UBL, or a file that is not XML at all each return a structured problem-details response with a machine-readable error code and a plain-language message, never a broken PDF. That makes the failure path wire-able: render on arrival, attach the PDF on success, and route the rare rejection to a human queue with the API's message attached. Note the boundary: rendering reads whatever business terms the file carries and lays them out; it does not run the EN 16931 or national rule sets, so a rendering success is not a compliance verdict. Validation stays its own call, covered below.
Integration snippets
The integration is the same three lines of intent everywhere: build a multipart request, send the file, save the PDF.
C# / .NET
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new("Bearer",
Environment.GetEnvironmentVariable("INVOICEXML_API_KEY"));
using var form = new MultipartFormDataContent();
form.Add(new ByteArrayContent(await File.ReadAllBytesAsync("invoice.xml")),
"file", "invoice.xml");
form.Add(new StringContent("de"), "language");
var response = await http.PostAsync(
"https://api.invoicexml.com/v1/render/xml/to/pdf", form);
response.EnsureSuccessStatusCode();
await File.WriteAllBytesAsync("invoice-preview.pdf",
await response.Content.ReadAsByteArrayAsync());
Node.js
import { readFile, writeFile } from "node:fs/promises";
const form = new FormData();
form.append("file", new Blob([await readFile("invoice.xml")]), "invoice.xml");
form.append("language", "de");
const response = await fetch("https://api.invoicexml.com/v1/render/xml/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("invoice-preview.pdf", Buffer.from(await response.arrayBuffer()));
Python
import os
import requests
with open("invoice.xml", "rb") as f:
response = requests.post(
"https://api.invoicexml.com/v1/render/xml/to/pdf",
headers={"Authorization": f"Bearer {os.environ['INVOICEXML_API_KEY']}"},
files={"file": ("invoice.xml", f, "application/xml")},
data={"language": "de"},
)
response.raise_for_status()
with open("invoice-preview.pdf", "wb") as out:
out.write(response.content)
PHP
$ch = curl_init("https://api.invoicexml.com/v1/render/xml/to/pdf");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("INVOICEXML_API_KEY")],
CURLOPT_POSTFIELDS => [
"file" => new CURLFile("invoice.xml", "application/xml", "invoice.xml"),
"language" => "de",
],
]);
$pdf = curl_exec($ch);
if (curl_getinfo($ch, CURLINFO_RESPONSE_CODE) !== 200) {
throw new RuntimeException($pdf);
}
curl_close($ch);
file_put_contents("invoice-preview.pdf", $pdf);
Every snippet works against any of the four routes; swap the path if the explicit format documents intent better in your codebase.
JSON instead of PDF: your own UI
A PDF is the right output when the destination is a document: the archive, an approval email, an attachment pane. When the destination is a screen you control (an AP workbench, a customer portal, a dashboard) render the data, not a document: POST /v1/extract/json reads the same CII and UBL files with the same parser and returns every EN 16931 business term as structured JSON with readable property names: parties, lines, price details, VAT breakdowns, totals, payment details, references, attachments. Your frontend lays it out natively, your search index covers it, and your mobile view stops fighting a fixed page size. Extraction is deterministic (parsed from the structured XML, no AI, no confidence scores), so what is on screen is what is in the file. The two calls pair naturally: JSON for the workflow and the screens, PDF for the humans outside the system, one upload each.
Attachments and validation
Two neighbours complete the reception picture. Embedded attachments: invoice XML can carry supporting documents base64-embedded in the file (BG-24: timesheets, delivery notes, a supplier's own PDF rendition). The rendered preview lists them; POST /v1/extract/attachments returns them as a ZIP, decoded verbatim, with externally referenced URIs deliberately not fetched. Validation: rendering shows what a file says, the validate endpoints judge whether it complies, per format, against the official XSD and Schematron rule sets. A reception pipeline typically validates at the boundary, renders for the archive, and extracts for the workflow, three calls on the same upload shape.
Endpoint reference
| Operation | Endpoint | Input | Output |
|---|---|---|---|
| Render any invoice XML | POST /v1/render/xml/to/pdf | CII / UBL XML (multipart) | PDF binary |
| Render, explicit routes | /v1/render/xrechnung/to/pdf, /v1/render/cii/to/pdf, /v1/render/ubl/to/pdf | Same | Same (permanent aliases) |
| Invoice data as JSON | POST /v1/extract/json | XML or hybrid PDF | { "invoice": ... } JSON |
| Embedded attachments | POST /v1/extract/attachments | XML or hybrid PDF | ZIP of embedded files |
| Compliance verdict | /v1/validate/xrechnung, /v1/validate/cii, /v1/validate/ubl | XML | Validation JSON findings |
Full OpenAPI 3.1 schema: api.invoicexml.com/v1/openapi | Interactive API explorer: api.invoicexml.com/v1/scalar
Get started
If an invoice XML is on disk right now, the curl call above is the whole integration; the online previews for XRechnung, CII, and UBL show the output without writing code. Create a free InvoiceXML account → and get 100 credits for free, no credit card required.
Related resources:
- Peppol invoice to PDF: making UBL invoices readable
- How to open an XRechnung file and read it as a PDF
- XRechnung visualization: KoSIT XSLT vs one API call
- Automating invoice conversions
- Invoice XML rendering API reference
- Full API documentation
InvoiceXML is a REST API for European e-invoice compliance covering CII, UBL, XRechnung, ZUGFeRD, Factur-X, and Peppol profiles. Stateless processing, GDPR compliant by architecture, and callable from any stack in minutes.