E-invoicing
Issue Factur-X and ZUGFeRD PDFs or XRechnung XML from one JSON invoice. Pick a standard, send a minimal request, fix validation errors and test.
On this page
Send one structured invoice in the e_invoice field of POST /v1/pdf and CastPDF returns an EU e-invoice: a Factur-X or ZUGFeRD PDF (a PDF/A-3b with the invoice XML inside) or an XRechnung XML. The visible PDF and the XML are made from the same data and the same totals, so they cannot disagree.
This guide helps you choose a format, make your first valid request and fix what the API refuses. Every field of the invoice is listed in the e_invoice field reference.
Choose a standard and profile
| You invoice | Send | You get |
|---|---|---|
| Businesses in France, or across the EU | "standard": "factur-x" (profile en16931, the default) | A PDF with factur-x.xml embedded |
| Businesses in Germany | "standard": "zugferd" (profile en16931, the default) | The same kind of PDF: ZUGFeRD 2.1 and later and Factur-X use the same file name and the same XML |
| German public buyers, who ask for XRechnung | "standard": "xrechnung-cii" or "xrechnung-ubl" | The XRechnung 3.0 XML itself, in CII or UBL 2.1 syntax; no PDF is rendered |
| A receiver that accepts XRechnung inside a readable PDF | "standard": "zugferd", "profile": "xrechnung" | A PDF with the XRechnung CII embedded as xrechnung.xml |
The profile sets how much of the invoice goes into the XML. en16931 is the full European standard and the right choice for most invoices. extended is for receivers that ask for it. basic is a lighter EN 16931 profile. minimum and basic-wl describe the invoice as a whole and leave the line items out of the XML: use them only when your receiver asks for exactly that. For XRechnung there is one profile, xrechnung, which is also the default.
If your buyer names a format, use that one. If you are not sure which rules apply to you, the e-invoicing overview explains the formats and the German and French rules.
A minimal Factur-X or ZUGFeRD request
Without template_id or html, CastPDF's built-in invoice template renders the visible PDF. This is close to the smallest invoice the API accepts with VAT:
curl https://api.castpdf.com/v1/pdf \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"e_invoice": {
"standard": "factur-x",
"invoice": {
"number": "RE-2026-0815",
"issue_date": "2026-10-10",
"currency": "EUR",
"seller": {
"name": "Muster Software GmbH",
"vat_id": "DE123456789",
"address": { "line1": "Kastanienallee 12", "city": "Berlin", "postal_code": "10435", "country_code": "DE" }
},
"buyer": {
"name": "Acme Ltd",
"address": { "line1": "1 Market Street", "city": "Dublin", "postal_code": "D02 X285", "country_code": "IE" }
},
"payment": { "iban": "DE89370400440532013000", "due_date": "2026-11-09" },
"lines": [
{ "name": "Consulting", "quantity": 10, "unit_code": "HUR", "unit_price": "120.00", "vat": { "category": "S", "rate": 19 } }
]
}
}
}' \
--output RE-2026-0815.pdfimport { writeFile } from 'node:fs/promises';
const res = await fetch('https://api.castpdf.com/v1/pdf', {
method: 'POST',
headers: { Authorization: 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' },
body: JSON.stringify({
e_invoice: {
standard: 'factur-x',
invoice: {
number: 'RE-2026-0815',
issue_date: '2026-10-10',
currency: 'EUR',
seller: {
name: 'Muster Software GmbH',
vat_id: 'DE123456789',
address: { line1: 'Kastanienallee 12', city: 'Berlin', postal_code: '10435', country_code: 'DE' },
},
buyer: {
name: 'Acme Ltd',
address: { line1: '1 Market Street', city: 'Dublin', postal_code: 'D02 X285', country_code: 'IE' },
},
payment: { iban: 'DE89370400440532013000', due_date: '2026-11-09' },
lines: [{ name: 'Consulting', quantity: 10, unit_code: 'HUR', unit_price: '120.00', vat: { category: 'S', rate: 19 } }],
},
},
}),
});
if (!res.ok) {
const { error } = await res.json();
// invalid_e_invoice: error.details.issues lists every problem
throw new Error(`${error.code}: ${error.message}`);
}
console.log(res.headers.get('x-e-invoice')); // factur-x/en16931
await writeFile('RE-2026-0815.pdf', Buffer.from(await res.arrayBuffer()));import requests
invoice = {
"number": "RE-2026-0815",
"issue_date": "2026-10-10",
"currency": "EUR",
"seller": {
"name": "Muster Software GmbH",
"vat_id": "DE123456789",
"address": {"line1": "Kastanienallee 12", "city": "Berlin", "postal_code": "10435", "country_code": "DE"},
},
"buyer": {
"name": "Acme Ltd",
"address": {"line1": "1 Market Street", "city": "Dublin", "postal_code": "D02 X285", "country_code": "IE"},
},
"payment": {"iban": "DE89370400440532013000", "due_date": "2026-11-09"},
"lines": [
{"name": "Consulting", "quantity": 10, "unit_code": "HUR", "unit_price": "120.00", "vat": {"category": "S", "rate": 19}}
],
}
res = requests.post(
"https://api.castpdf.com/v1/pdf",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={"e_invoice": {"standard": "factur-x", "invoice": invoice}},
timeout=60,
)
if not res.ok:
error = res.json()["error"]
raise RuntimeError(f"{error['code']}: {error['message']}")
print(res.headers["x-e-invoice"]) # factur-x/en16931
with open("RE-2026-0815.pdf", "wb") as f:
f.write(res.content)HTTP/1.1 200 OK
content-type: application/pdf
content-disposition: attachment; filename="RE-2026-0815.pdf"
x-e-invoice: factur-x/en16931
x-document-id: 3f0c9a4e-6b1d-4c2a-9a57-0d8e4b7f21c3
x-pages: 1The totals are computed for you: 10 hours at 120.00 is 1,200.00 net, plus 228.00 VAT, so 1,428.00 is due. For ZUGFeRD, change "factur-x" to "zugferd"; nothing else changes. The file name defaults to the invoice number; send filename to choose your own.
What makes this the minimum: number, issue_date, currency, a seller and a buyer with a name and an address (at least country_code), and at least one line with name, quantity, unit_price and vat. Because the line has VAT, the seller needs a vat_id (or tax_number). Because an amount is due, payment needs a due_date or terms. A payment with an IBAN is a SEPA credit transfer by default.
A minimal XRechnung request
XRechnung adds the German rules (the KoSIT CIUS) on top of EN 16931, so it needs more: a buyer_reference (for public buyers the Leitweg-ID), a seller contact with name, phone and email, an electronic_address for the seller and the buyer, the city and postal code of both, and payment.
{
"response": "url",
"e_invoice": {
"standard": "xrechnung-cii",
"invoice": {
"number": "RE-2026-0816",
"issue_date": "2026-10-10",
"currency": "EUR",
"buyer_reference": "04011000-12345-67",
"seller": {
"name": "Muster Software GmbH",
"vat_id": "DE123456789",
"electronic_address": { "value": "[email protected]", "scheme": "EM" },
"address": { "line1": "Kastanienallee 12", "city": "Berlin", "postal_code": "10435", "country_code": "DE" },
"contact": { "name": "Accounts receivable", "phone": "+49 30 1234567", "email": "[email protected]" }
},
"buyer": {
"name": "Stadt Musterstadt",
"electronic_address": { "value": "04011000-12345-67", "scheme": "0204" },
"address": { "line1": "Rathausplatz 1", "city": "Musterstadt", "postal_code": "12345", "country_code": "DE" }
},
"payment": { "iban": "DE89370400440532013000", "terms": "Payable within 30 days.", "due_date": "2026-11-09" },
"lines": [
{ "name": "Consulting", "quantity": 10, "unit_code": "HUR", "unit_price": "120.00", "vat": { "category": "S", "rate": 19 } }
]
}
}
}For UBL, change "xrechnung-cii" to "xrechnung-ubl". Ask the buyer which syntax they accept; both carry the same invoice. Nothing is rendered, so template_id, html and the page options have no effect. With "response": "url" the answer is JSON with a signed link to the XML:
{
"id": "7c3e1a9b-2d4f-4b6a-8c0e-1f2a3b4c5d6e",
"status": "succeeded",
"url": "https://api.castpdf.com/v1/files/7c3e1a9b-2d4f-4b6a-8c0e-1f2a3b4c5d6e?exp=1791972902&sig=Rf5n…",
"filename": "RE-2026-0816.xml",
"e_invoice": { "standard": "xrechnung-cii", "profile": "xrechnung", "xml_filename": "xrechnung.xml" }
}The real answer has every field of a "response": "url" document (pages, bytes, expires_at, warnings and the others); the example shows the ones that matter here. With the default "response": "binary" you get the XML itself, as application/xml, named after the invoice number.
What comes back
| Standard | Body | File name | Extra |
|---|---|---|---|
factur-x, zugferd | A PDF/A-3b with the XML embedded as an associated file | The invoice number, .pdf | factur-x.xml inside (xrechnung.xml for the ZUGFeRD xrechnung profile) |
xrechnung-cii, xrechnung-ubl | The XML, application/xml | The invoice number, .xml | None |
A binary response carries x-e-invoice: standard/profile, such as zugferd/en16931. A "response": "url" answer adds "e_invoice": { "standard", "profile", "xml_filename" }. Each e-invoice request makes one document and is counted like any other PDF; a refused invoice is never counted.
Use your own template
The built-in template is a clean, neutral invoice. To use your own design, send template_id (or html) together with e_invoice. The template receives the invoice as template data, with every amount already computed exactly as in the XML, so the visible PDF matches the XML to the cent:
| Data | Contents |
|---|---|
invoice | number, title ("Invoice" or "Credit note"), issued, due, delivery_date, reference, buyer_reference, notes, terms |
seller, customer | name, address (as lines of text), email, phone, contact, tax_id, legal_registration_id, plus initials and tagline |
items | One per line: description, detail, quantity, unit, unit_price, vat_rate, vat_label, discount_amount, net_amount and formatted quantity_text and unit_price_text |
totals | subtotal, net, allowances, charges, tax_basis, vat, total, prepaid, due and more |
vat_breakdown | One row per VAT category and rate: label, rate, net, vat, note |
payment | account_name, iban (grouped in fours), bic, reference |
currency, locale | The invoice currency, and a locale from the seller's country for formatting |
e_invoice | standard, profile and a label such as "Factur-X EN 16931" |
<h1>{{ invoice.title }} {{ invoice.number }}</h1>
{% for item in items %}
<p>{{ item.description }}: {{ item.net_amount | money: currency }}</p>
{% endfor %}
<p>Total due: {{ totals.due | money: currency }}</p>
<p class="small">{{ e_invoice.label }}</p>Print totals from totals and vat_breakdown, never computed again in Liquid: that is what keeps the PDF and the XML equal. Send data only if you want to replace this data entirely. Your brand applies as with any template.
Check the files
The free e-invoice validator reads a Factur-X or ZUGFeRD PDF, or an XRechnung or other EN 16931 XML, and reports what it finds: the syntax and profile, missing required fields, wrong codes, VAT category rules, IBAN check digits, the XRechnung mandatory fields and the EN 16931 calculation rules on the stated amounts. For a PDF it also reads the embedded XML and the PDF's own PDF/A-3 and Factur-X identification. It is a quick check, not the official XSD and Schematron validation: for a definitive answer, run the KoSIT validator (and veraPDF for the PDF/A part).
Use it to check a file you received, or a file from another system before you switch to CastPDF. The files CastPDF makes are validated as described below.
Test mode
E-invoices work with test keys like everything else: a test request returns the same PDF with the diagonal test watermark and is not counted. It is checked by exactly the same rules, so an invoice that passes in test mode passes in live mode. The XML is never watermarked, so you can run a test XML through the validator or your buyer's test system. A watermarked PDF is not meant to be sent; switch to a live key for real invoices.
Fix validation errors
An invoice the API cannot issue is refused with 400 invalid_e_invoice before anything is rendered or counted. details.issues lists up to 50 problems as { "path", "message" }, and details.more_issues counts the rest. Fix them all and send again: the message names the field and usually the fix.
{
"error": {
"code": "invalid_e_invoice",
"message": "The e-invoice is not valid: e_invoice.invoice.payment.due_date: is required (or payment.terms) when an amount is due (EN 16931 BR-CO-25).",
"docs_url": "https://castpdf.com/docs/errors#invalid_e_invoice",
"details": {
"issues": [
{ "path": "e_invoice.invoice.payment.due_date", "message": "is required (or payment.terms) when an amount is due (EN 16931 BR-CO-25)." }
]
}
}
}The problems that come up most:
| Path and message | Fix |
|---|---|
payment.due_date: is required (or payment.terms) when an amount is due | Send due_date (YYYY-MM-DD) or terms text. |
seller.vat_id: is required (or seller.tax_number) | An invoice with VAT must show the seller's VAT number, with its country prefix (DE123456789), or the German tax_number. |
lines[0].vat.rate: is required for category S | Standard-rate lines need a rate in percent, such as 19. A 0 % line is category Z or E, not S with rate 0. |
buyer.vat_id: is required for reverse charge (category AE) | Reverse charge (AE) and intra-community supplies (K) need the buyer's VAT number. |
tax_exemptions.E: is required for exempt lines (category E) | Add "tax_exemptions": { "E": { "reason": "..." } } with the legal reason, or a VATEX code. |
payment.iban: is not a valid IBAN | The check digits do not match: copy the IBAN again. Spaces are fine. |
totals.tax_total: is 917.24, but it should be 917.23 | You sent totals, and one differs from ours. EN 16931 rounds each line to cents and the VAT once per rate, not per line: change your calculation, or leave totals out. |
buyer_reference, seller.contact.phone, buyer.electronic_address and more: is required for XRechnung | XRechnung needs the fields listed above. All missing ones are listed at once. |
profile: is not available for xrechnung-cii | XRechnung has only the profile xrechnung; Factur-X has no xrechnung profile (use zugferd). |
An invoice has 1 to 2,000 lines. Amounts, quantities, prices and rates can be JSON numbers or decimal strings such as "24.90"; strings avoid any doubt about decimals.
Limits of this version
delivercannot be combined with an XRechnung (invalid_request): download the XML with"response": "url"and send it yourself. Factur-X and ZUGFeRD PDFs can be emailed.- Batches do not accept
e_invoiceyet: send onePOST /v1/pdfper invoice (with"async": truefor many).
What was validated
In our test suite every generated XML passes the official XSD and Schematron rules (CEN EN 16931 1.3.16, Factur-X 1.09 and KoSIT XRechnung 3.0.2). On 2026-10-10, veraPDF 1.30.3 found 16 of 16 sample PDFs compliant with PDF/A-3b, and the KoSIT validator 1.6.3 accepted all 14 EN 16931 and XRechnung sample files. Your own data is checked by the API's rules on every request, but whether a receiver accepts an invoice also depends on their own requirements: ask them which standard, profile and syntax they want.
Next steps
- The e_invoice field reference: every invoice field, VAT category and code list.
- Factur-X, ZUGFeRD and XRechnung guide: a longer walkthrough with a full invoice.
- E-invoicing overview: the formats and the German and French rules.
- E-invoice validator: check any file for free.