E-Invoicing (Factur-X)
PDFik generates hybrid e-invoices: a normal, human-readable PDF that also carries the complete machine-readable invoice data inside the same file, in the Factur-X / ZUGFeRD format used for French and German e-invoicing. Available on every plan, Free included — the output is the same clean PDF/A-3 everywhere.
No API key yet? Paste your invoice XML into the sandbox on pdfik.net — it returns a real, watermarked Factur-X PDF through the same pipeline, a few renders per day per visitor.
What Factur-X Is
A Factur-X document is one PDF file with two audiences:
- Humans see an ordinary invoice — the PDF is normalized to PDF/A-3, the archival PDF standard.
- Accounting software reads the UN/CEFACT Cross-Industry Invoice (CII) XML embedded in the file as
factur-x.xmland books the invoice without OCR or manual entry.
Factur-X (France) and ZUGFeRD 2.x (Germany) are the same standard under two names. You supply the XML — PDFik never generates or alters invoice data — and PDFik produces the hybrid PDF around it.
PDFik's Factur-X output is validated with veraPDF (PDF/A-3) and Mustangproject (Factur-X structure). Schema-valid does not mean tax-compliant — the invoice content is the caller's responsibility.
There are two ways to produce one:
| Mode | How | Visual half comes from |
|---|---|---|
| Dedicated endpoint | POST /einvoice-to-pdf — send only the XML | A block template — PDFik builds the invoice layout from your XML data |
einvoice option | Add an einvoice block to POST /url-to-pdf or POST /html-to-pdf | Your own rendered page |
Endpoint: POST /einvoice-to-pdf
POST https://api.pdfik.net/einvoice-to-pdf
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
xml | string | Yes | UN/CEFACT Cross-Industry-Invoice XML (the Factur-X payload), UTF-8, up to 1 MB. Validated against the official XSD of the declared profile before any quota is spent. |
profile | string | No | Factur-X conformance profile the XML declares: "minimum", "basicwl", "basic", "en16931" (default), or "extended". See Profiles. |
template_id | string | No | Id of a saved invoice template (Dashboard → E-Invoice). Mutually exclusive with template. |
template | object | No | Inline block-template JSON (the same format the dashboard editor produces). Mutually exclusive with template_id. |
webhook_url | string | No | Optional callback URL where the job outcome payload will be POSTed. Credentials must not be embedded in the URL (https://user:pass@host is rejected) — configure delivery headers in the dashboard instead. |
test | boolean | No | Run the job in test mode: full pipeline, no real render, a sample PDF is returned. Quotas are not debited. Defaults to false. |
Omitting both template_id and template uses your account's default template (the one marked Make default on the dashboard's E-Invoice page), or the built-in default (English labels, A4) if you have not set one. Sending both is rejected with 422 einvoice-options-conflict.
Request Headers
| Header | Required | Description |
|---|---|---|
X-API-Key | Yes | Your API key (sk_live_...). |
Idempotency-Key | No | 1-255 characters from A-Z a-z 0-9 _ - . :. Retrying a request with the same key returns the original job instead of creating (and billing) a duplicate. Recommended for retry-after-timeout logic. |
Example
The XML has to travel inside a JSON string, so build the request body from the file instead of quoting it inline:
# invoice.xml is your UN/CEFACT CII file (EN 16931 profile)
jq -n --rawfile xml invoice.xml '{xml: $xml, profile: "en16931"}' > request.json
curl -X POST https://api.pdfik.net/einvoice-to-pdf \
-H "X-API-Key: sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d @request.json
Response (202 Accepted):
{
"job_id": "8bfa5cc3-09ef-436f-80d4-a0eb867123aa",
"status": "queued",
"detail": "Job successfully sent to processing queue."
}
From here the flow is the same as every other conversion: poll GET /jobs/{job_id} until status is done, then fetch the file via GET /jobs/{job_id}/download — see the asynchronous flow.
The XML must declare what you claim
Two fields in the XML are checked against the request before the XSD runs:
<rsm:CrossIndustryInvoice ...>
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<!-- must match the "profile" you declare -->
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<!-- 380 = invoice, 381 = credit note -->
<ram:TypeCode>380</ram:TypeCode>
...
A mismatch between the XML's guideline URN and the declared profile is rejected with 422 einvoice-xml-invalid — never silently upgraded or downgraded to another compliance level.
Profiles
| Profile | Guideline URN the XML must declare | Complete legal invoice? |
|---|---|---|
minimum | urn:factur-x.eu:1p0:minimum | No — accompanying data only |
basicwl | urn:factur-x.eu:1p0:basicwl | No — accompanying data only |
basic | urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic | Yes |
en16931 (default) | urn:cen.eu:en16931:2017 | Yes |
extended | urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended | Yes |
The minimum and basicwl profiles do not carry a complete invoice: their embedded XML is accompanying data only and is NOT a legally sufficient e-invoice in Germany or France. Use basic, en16931 or extended for a full invoice. When you submit one of these profiles anyway, the 202 response repeats this warning in its detail field.
Germany states this explicitly: the Federal Ministry of Finance's FAQ E-Rechnung says XRechnung and ZUGFeRD from version 2.0.1 meet the VAT requirements for an e-invoice "mit Ausnahme der Profile MINIMUM und BASIC-WL" (read 7 September 2026).
Credit Notes
ExchangedDocument/TypeCode 380 (commercial invoice) and 381 (credit note) are supported. A credit note renders with the template's credit-note heading (the title_credit_note label, "CREDIT NOTE" in the default template) instead of the invoice title. Any other type code is rejected with 422 einvoice-xml-invalid.
Invoice Templates
The visual invoice for /einvoice-to-pdf is built from a block template: a fixed set of sections (heading, trading parties, line items, VAT breakdown, totals, payment instructions, notes, footer) whose labels, locale, accent color, logo, and page setup you control.
- Dashboard constructor: create and edit templates under Dashboard → E-Invoice, with a live preview of the composed invoice. Saved templates are referenced by
template_idin API calls. The Built-in default row offers language presets (English, Deutsch, Français, Italiano, Español, Nederlands, Polski, Português, Čeština, Українська — 14 locales): pick one and Save as my default to get an editable, localized template that becomes your account default. - Inline template: send the same JSON directly in the
templatefield, without saving it first. - Neither: your account's default template applies (dashboard → E-Invoice → Make default); without one, the built-in default is used (English labels, A4).
An inline template must list exactly the nine v1 block types in their fixed order — disable a block with "enabled": false instead of removing it:
{
"version": 1,
"locale": "de-DE",
"branding": { "accent_color": "#0f4c81", "font": "sans" },
"page": { "format": "A4", "margin": "10mm 15mm" },
"blocks": [
{ "type": "invoice-heading",
"labels": { "title": "RECHNUNG", "title_credit_note": "GUTSCHRIFT", "number": "Nr.", "issue_date": "Rechnungsdatum", "due_date": "Fällig am" } },
{ "type": "trading-parties", "labels": { "seller": "Verkäufer", "buyer": "Rechnungsempfänger" } },
{ "type": "line-items", "show_vat_column": true,
"labels": { "description": "Beschreibung", "qty": "Menge", "unit_price": "Einzelpreis", "vat": "USt.", "amount": "Betrag" } },
{ "type": "vat-breakdown", "labels": { "title": "USt.-Übersicht" } },
{ "type": "amounts-due", "labels": { "subtotal": "Zwischensumme (netto)", "tax": "USt. {rate}%", "total": "Gesamtbetrag" } },
{ "type": "payment-instructions", "text": "Zahlbar bis {due_date}." },
{ "type": "invoice-notes", "enabled": false },
{ "type": "additional-invoice-data" },
{ "type": "document-footer", "text": "Factur-X {profile} · {guideline}" }
]
}
Labels and text fields accept a whitelist of {placeholder} values substituted from the XML (such as {due_date}, {rate}, {profile}); an unknown placeholder or block field is rejected with a message naming the offending path. template_id and template are mutually exclusive — sending both returns 422 einvoice-options-conflict, and an unknown or foreign template_id returns 404 einvoice-template-not-found.
Using Your Own Design (the einvoice option)
If you already render your invoice with /url-to-pdf or /html-to-pdf, attach the XML to that render instead: the page you render becomes the human-readable half, the output is normalized to PDF/A-3, and the XML is embedded as factur-x.xml.
| Parameter | Type | Required | Description |
|---|---|---|---|
einvoice.format | string | No | E-invoice container format. Only "factur-x" (the default) in v1. |
einvoice.profile | string | No | Same profiles and default ("en16931") as above. |
einvoice.xml | string | Yes | The CII XML, UTF-8, up to 1 MB — validated exactly like on /einvoice-to-pdf. |
jq -n --rawfile xml invoice.xml '{
html: "<html><body><h1 style=\"color: #0f4c81;\">Invoice #90812</h1><p>...</p></body></html>",
einvoice: { format: "factur-x", profile: "en16931", xml: $xml }
}' > request.json
curl -X POST https://api.pdfik.net/html-to-pdf \
-H "X-API-Key: sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d @request.json
einvoice cannot be combined with options.user_password (PDF/A forbids encryption) or options.compression (re-saving breaks the PDF/A attributes). The combination is rejected with 422 einvoice-options-conflict on every plan.
Limits, Plans, and Errors
- XML size: up to 1 MB per invoice. Invoice XML of any size within this limit is accepted — the one exception is XML with embedded base64 file attachments, which can make the request too large: it is rejected with
413payload-too-large-for-queue before anything is charged. Submit the invoice without embedded attachments. - Plans: available on all plans, Free included, and does not require an upgrade. The output is the same clean PDF/A-3 on every plan.
- Rejected requests cost nothing: XML validation, template resolution, and the size check all run before any quota is spent.
- Job failures: if the render or the PDF/A-3 embedding fails after the job is queued, the job status and failure webhook carry
error_code:EINVOICE_FAILED.
| HTTP Status | Error Code | Meaning |
|---|---|---|
422 | EINVOICE_XML_INVALID | The XML failed parsing, the profile check, the type-code check, or the XSD. |
422 | EINVOICE_OPTIONS_CONFLICT | Mutually exclusive fields were combined. |
404 | EINVOICE_TEMPLATE_NOT_FOUND | template_id does not exist on this account. |
413 | PAYLOAD_TOO_LARGE_FOR_QUEUE | The job message does not fit the processing queue even compressed. |
Testing
Test mode ("test": true) works on /einvoice-to-pdf and with the einvoice option: the request is fully validated (XML, profile, template), the job runs through the real pipeline, and the download returns the bundled sample PDF — not a Factur-X render of your XML. Use it to verify integration plumbing; submit a real job to check the actual hybrid output.
The dashboard's E-Invoice page also shows a live preview of the composed invoice while you edit a template. The preview is not test mode either — it displays the invoice layout without rendering a PDF. See the note in Testing.