Skip to content
LiveZUGFeRD / Factur-X

Your layout, our XML

You already have your invoice PDF — from your ERP, your print shop, your own template. This endpoint takes it as the carrier, generates the CII XML from your invoice data and returns that same PDF with the XML inside it as factur-x.xml.

POST/api/v1/invoice/{countryCode}/{format}/embed
POST/api/v1/invoice/{countryCode}/{format}/embed/upload

The difference from /generate

Both endpoints produce the same e-invoice from the same data and run on the same entitlement. The only difference is the page the recipient sees.

  • • With /generate the visual page comes from our template — then PDF/A-3 conformance is our problem.
  • • With /embed the visual page comes from you and stays as it is; we only place the XML inside and set the markers.
  • • Same invoice data, same validation chain, same failure modes (422 with the same rule codes).

There is no template or templateId on this route. The body is strict and rejects them with a 400 rather than ignoring them silently — the layout is yours.

To the Creator API (/generate)

Hybrid formats only

A hybrid document needs a visual page a carrier PDF can stand in for. XML-only formats such as xrechnung or ubl have none, and plain pdf carries no XML — both return 400.

Path parameters

FieldTypeRequiredDescription
countryCodestringCountry of the hybrid format — de, ch, fr or be.
formatstringThe hybrid format: zugferd for DE and CH, facturx for FR and BE.

Request

FieldTypeRequiredDescription
pdfstring (base64)Your carrier PDF, base64-encoded. It becomes the visual page of the hybrid document.
invoiceobjectThe invoice data the XML is generated from — the same shape as /generate, with the same mandatory fields.
validateboolean-Also runs the XSD and Schematron check and returns the verdict in the validation block. Costs a second unit, exactly as on /generate.
enforcePendingboolean-Rejects rule findings that are still in their warning phase. When a newly enforced rule of a pinned rulebook (EN 16931, Peppol BIS, a national CIUS) would reject a document that was accepted before, we announce it first: for at least 28 days the call still returns 200 and the finding comes as a SPEC_RULE_PENDING warning with rule (or pendingKey when the finding has no rule identifier), enforceFrom and specId. true switches that phase off for this call — every finding then answers 422, as it will from enforceFrom anyway. The document is not conformant in either case.
formatOptions.profilestring-ZUGFeRD / Factur-X profile, EN16931 by default.
formatOptions.versionstring-Format version, 2.4 for ZUGFeRD and 1.08 for Factur-X by default.

PDF/A-3 is declared, not converted

We set the marker on the carrier you sent. We do not rewrite your PDF. On an arbitrary PDF that declaration can therefore be wrong.

  • • Most commonly when your PDF uses fonts without an embedded font file — which PDF/A-3 requires.
  • • What we actually checked is listed in carrier.checks; what we found, in carrier.warnings.
  • • A full PDF/A-3b proof (veraPDF) does not take place here. Nothing is rejected because of it: the finding is ours, the decision is yours.

If your recipient enforces a full proof: run your carrier through a PDF/A converter once before it reaches us — or use /generate, where the PDF is ours.

Example

bash
1curl -X POST https://service.invoice-api.xhub.io/api/v1/invoice/de/zugferd/embed \
2 -H "Authorization: Bearer sk_live_abc123..." \
3 -H "Content-Type: application/json" \
4 -d '{
5 "pdf": "JVBERi0xLjQKJeLjz9MKMyAwIG9iago8PC9UeXBlL1BhZ2UvTWVkaWFCb3...",
6 "validate": true,
7 "invoice": {
8 "invoiceNumber": "RE-2025-001",
9 "type": "invoice",
10 "issueDate": "2025-01-15",
11 "dueDate": "2025-02-15",
12 "currency": "EUR",
13 "seller": {
14 "name": "Meine Firma GmbH",
15 "street": "Musterstraße 1",
16 "city": "Berlin",
17 "postalCode": "10115",
18 "countryCode": "DE",
19 "vatId": "DE123456789",
20 "bankAccount": {
21 "iban": "DE89370400440532013000",
22 "bic": "COBADEFFXXX"
23 }
24 },
25 "buyer": {
26 "name": "Kunde AG",
27 "street": "Kundenweg 42",
28 "city": "München",
29 "postalCode": "80331",
30 "countryCode": "DE",
31 "vatId": "DE987654321"
32 },
33 "countrySpecific": {
34 "countryCode": "DE",
35 "buyerReference": "BUYER-REF-001"
36 },
37 "items": [
38 {
39 "position": 1,
40 "description": "Beratungsleistung",
41 "quantity": 10,
42 "unit": "HUR",
43 "unitPrice": 150.00,
44 "taxRate": 19,
45 "taxCategoryCode": "S",
46 "netAmount": 1500.00,
47 "taxAmount": 285.00,
48 "grossAmount": 1785.00
49 }
50 ],
51 "subtotal": 1500.00,
52 "total": 1785.00,
53 "taxSummary": [
54 {
55 "taxRate": 19,
56 "netAmount": 1500.00,
57 "taxAmount": 285.00
58 }
59 ]
60 }
61 }'

Large carriers: multipart instead of base64

The same thing as the JSON route, except the carrier arrives as a file part of a multipart/form-data request instead of base64 inside JSON — no base64 overhead on your side or ours. Same checks, same entitlement, same billing and the same JSON response.

The parts of the request

FieldTypeRequiredDescription
pdffileThe carrier as a file part with content type application/pdf — your layout, binary instead of base64.
invoiceJSON-TextThe invoice data as JSON text (content type application/json) — the same object as at the JSON route, checked by the same rules.
validatestring-true runs the same XSD and Schematron check as /validate and counts as two units. Omitted or false: no check, one unit.
enforcePendingstring-true rejects findings in their warning phase with a 422 — the same switch as at the JSON route.
formatOptionsJSON-Text-JSON text with profile and/or version — the same strict object as at the JSON route. template and templateId are rejected.
bash
1curl -X POST \
2 https://service.invoice-api.xhub.io/api/v1/invoice/de/zugferd/embed/upload \
3 -H "Authorization: Bearer sk_live_abc123..." \
4 -H "Accept: application/pdf" \
5 -F "pdf=@rechnung-RE-2025-001.pdf;type=application/pdf" \
6 -F "invoice=@invoice.json;type=application/json" \
7 -F "validate=true" \
8 -o rechnung-zugferd.pdf \
9 -D headers.txt
10 
11# X-Result-Rechecked: false heisst, das Ergebnis wurde nicht erneut geoeffnet
12# (Traeger > 15 MiB) — eine Aussage ueber den Weg, nicht ueber das Dokument.
13grep -i "^X-" headers.txt

The binary response

Send the header Accept: application/pdf and you get the hybrid PDF itself as the response body — but only when there is nothing to report: the check passed or was not requested, warnings is empty and carrier.warnings is empty. Otherwise the JSON body comes back, so a finding is never lost.

The binary response carries four headers: X-Validation-Status (passed or skipped), X-Result-Rechecked (true or false), X-Hash (SHA-256 of the returned bytes) and X-Payload-Hash (the same payloadHash as in JSON).

What happens with large carriers

From 5 MiB on, a separate service handles the carrier and appends the XML to your file — your bytes stay unchanged. Above 15 MiB the result is not opened again: warnings carries PDF_RESULT_NOT_RECHECKED, carrier.checks stays empty, and the binary response reports X-Result-Rechecked: false. That describes the way, not the document, and it does not hold up the binary answer. Where the fonts could not be checked, CARRIER_FONTS_NOT_CHECKED appears in carrier.warnings.

The limits

The request body may be up to 96 MiB. The carrier may be up to 64 MiB when the embedding service is available on the server; otherwise the same limit as at the JSON route applies, 15 MiB by default. The 413 tells the two apart: PAYLOAD_TOO_LARGE for the body, CARRIER_TOO_LARGE for the carrier, each with limit in bytes.

Response

200 OK
json
1{
2 "success": true,
3 "format": "zugferd",
4 "filename": "RE-2025-001.pdf",
5 "mimeType": "application/pdf",
6 "hash": "9f2c4e1b7a0d5836f4b2c9e18a7d3045c6b1e8f29a4d70b3c5e81f6a2d9c4b70",
7 "payloadHash": "3a7f1e9c2b8d4056a1c7e3f9b2d84e6015c9a7f3e1b8d2460c5a9f7e3b1d8460",
8 "data": "JVBERi0xLjQKJeLjz9MKMyAwIG9iago8PC9UeXBlL1BhZ2Uv...",
9 "embeddedXml": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><rsm:CrossIndustryInvoice ...",
10 "carrier": {
11 "pdfA3Declared": true,
12 "checks": [
13 { "code": "PDF-001", "severity": "error", "passed": true },
14 { "code": "XMP-003", "severity": "warning", "passed": true }
15 ],
16 "warnings": [
17 {
18 "code": "CARRIER_FONTS_NOT_EMBEDDED",
19 "message": "The carrier uses 2 fonts without an embedded font file."
20 }
21 ]
22 },
23 "validation": {
24 "valid": true,
25 "status": "passed",
26 "results": [
27 { "format": "zugferd-2.4-en16931", "valid": true, "errors": [] }
28 ]
29 },
30 "appliedFormatOptions": {
31 "version": "2.4",
32 "profile": "EN16931"
33 }
34}
FieldTypeDescription
successbooleanWhether the hybrid document was produced.
formatstringThe output format used.
filenamestringSuggested filename.
mimeTypestringMIME type of the returned document.
hashstringSHA-256 of the returned bytes, hex-encoded. Not stable across calls, because the PDF carries a creation timestamp — use payloadHash for idempotent names.
payloadHashstringSHA-256 of the canonical JSON of your invoice data — the stable key for idempotent calls. hash changes on every call because the PDF carries a timestamp.
datastring (base64)The hybrid document, base64-encoded: your layout with the generated XML inside.
embeddedXmlstringThe embedded CII XML in plain text, for convenient inspection.
carrierobjectWhat we measured on the document we produced — a finding, not a claim.
validationobjectThe conformance verdict — present only when the request set validate: true.
appliedFormatOptionsobjectThe version and profile the document was actually written in: yours, or the defaults.

The carrier block

FieldTypeDescription
pdfA3Declaredbooleantrue means we set the PDF/A-3 marker on your carrier (XMP pdfaid:part=3, conformance B, sRGB output intent). It does not mean your carrier is conformant.
checksarrayThe individual checks, each with code, severity (warning, error, fatal), passed and, where there is one, message.
warningsarrayFindings about the carrier you sent that we report but do not reject on — for example CARRIER_FONTS_NOT_EMBEDDED.

The validation block

Present only if you sent validate: true. status says what happened: passed — the rulebooks ran and found nothing; failed — they found something, valid is then false and errors names it; not_run — they did not run completely, valid is null and reason says why (missing, load-failed, transform-failed or not-bound). A not_run is not a pass: it proves nothing about the document.

Billing

The call runs on the format's existing create entitlement and costs one unit — two if you set validate: true. No new entitlement, no surcharge.

Errors

StatusCodeDescription
400Bad RequestThe country/format pair is not a hybrid format — an XML-only format or pdf has no visual page.
400Bad Requesttemplate or templateId was sent. Neither exists on this route.
400INVALID_PDFThe carrier is not a readable PDF.
401UNAUTHORIZEDAPI key missing or invalid.
403FORBIDDENThe key does not hold the format's create entitlement.
413PAYLOAD_TOO_LARGEThe carrier PDF is larger than the configured limit.
422UNPROCESSABLEThe invoice data cannot be turned into a conforming e-invoice — the same body as /generate, with errors and complianceErrors and without data.
429QUOTA_EXCEEDEDThe format's quota is exhausted.
503EMBED_UNAVAILABLEEMBED_UNAVAILABLE — the embedding service is not available and the carrier is larger than 15 MiB. Retry-After: 60, and nothing is billed. Only at the upload route.