POST /v1/convert
Convert an EN 16931-family e-invoice between formats — CII ↔ UBL, ZUGFeRD ↔ Factur-X, XRechnung CII ↔ UBL, Peppol BIS ↔ EN 16931 UBL. XML and hybrid PDF in and out.
Convert an e-invoice from one EN 16931-family format to another in a single call.
The engine routes the conversion through three primitives in order: extract embedded CII when the source is a hybrid PDF, convert between CII and UBL via Mustang (cii2ubl) or Helger en16931-ubl2cii when the syntaxes differ, rewrite the CustomizationID / guideline URN to match the target profile, and re-wrap into a PDF/A-3 hybrid when the target is ZUGFeRD or Factur-X.
When source and target carry semantics the target cannot represent, the engine fails closed with 422 CONVERSION_LOSSY_FAILCLOSED rather than silently dropping fields. The two known cases — France CTC Flux 2 BusinessProcess codes on UBL targets, and synthesising Peppol endpoints from a non-Peppol source — surface explicitly; callers can opt in to known-acceptable losses (e.g. dropping a PDF envelope when the target has no PDF spec) and read them back from the response headers.
Out of scope. FatturaPA, Facturae, KSeF, and SDI messaggio sources or targets are not supported (no published EN 16931-equivalent converter; conversion would silently lose national overlay fields). Use /v1/generate to produce these from structured JSON instead.
Quick copy examples
Convert CII → UBL:
curl -X POST "https://api.beliq.eu/v1/convert?targetFormat=ubl" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/xml" \
--data-binary @invoice.cii.xml \
--output invoice.ubl.xmlConvert ZUGFeRD PDF → Factur-X PDF (re-wrap with the new profile URN):
curl -X POST "https://api.beliq.eu/v1/convert?targetFormat=facturx&targetProfile=en16931" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/pdf" \
--data-binary @zugferd.pdf \
--output facturx.pdfConvert a Peppol BIS UBL → plain EN 16931 UBL (strips the cbc:EndpointID blocks; acceptable loss recorded):
curl -X POST "https://api.beliq.eu/v1/convert?targetFormat=ubl" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/xml" \
--data-binary @peppol.xml \
-D - \
--output en16931.xml
# Response headers include X-Lost-Elements-Count: 2 (one per stripped endpoint).Request
POST /v1/convert
Content-Type: application/xml | application/pdf
Authorization: Bearer <api-key>Headers
| Header | Required | Description |
|---|---|---|
Authorization |
required | Bearer <api-key> or use X-API-Key header |
Content-Type |
required | application/xml for XML input, application/pdf (or application/octet-stream) for a hybrid ZUGFeRD/Factur-X PDF |
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
sourceFormat |
string | optional | One of: auto (default), cii, ubl, zugferd, facturx, xrechnung, peppol-bis. With auto the engine inspects the XML root + CustomizationID URN. |
targetFormat |
string | required | One of: cii, ubl, zugferd, facturx, xrechnung, peppol-bis. |
targetProfile |
string | optional | ZUGFeRD / Factur-X profile when targetFormat ∈ {zugferd, facturx}: minimum, basicwl, basic, en16931 (default), extended, extended-ctc-fr. Ignored for other targets. |
dropFranceCtcOverlay |
boolean | optional | When true, the engine accepts the silent loss of a France CTC Flux 2 BusinessProcess code (S8/B8/M8) on UBL targets and records it in the response as an acceptable loss. Default: false (fail-closed with 422 CONVERSION_LOSSY_FAILCLOSED). |
Request body
The raw XML or hybrid PDF file content. Do not JSON-encode it.
Supported pairs
The engine guarantees a clean conversion path for every pair where both source and target are in the EN 16931 family. The table summarises the operational shape:
| Pair | What runs | Notes |
|---|---|---|
| CII ↔ UBL (any profile) | Mustang cii2ubl (CII → UBL) or Helger ubl2cii (UBL → CII) + URN swap |
Two JAR subprocesses end-to-end; the heavier path. |
| ZUGFeRD ↔ Factur-X (same syntax, different URN) | URN swap only (no JAR) | Profile change only; fastest path since no subprocess runs. |
| XRechnung-CII → XRechnung-UBL | Mustang cii2ubl |
Profile-preserving syntax conversion; XRechnung Schematron rules then re-validate at the target. |
| Peppol BIS UBL → plain EN 16931 UBL | URN swap + cbc:EndpointID strip |
Endpoints recorded as acceptable loss in X-Lost-Elements-Count. |
| Plain EN 16931 UBL → Peppol BIS UBL | Fail-closed (CONVERSION_LOSSY_FAILCLOSED) |
Peppol endpoints cannot be synthesised — use POST /v1/generate with an explicit peppol block on each party instead. |
| Hybrid PDF in + ZUGFeRD/Factur-X target | Extract embedded CII + URN swap + Mustang --action combine (PDF re-wrap) |
Single round-trip; the source PDF’s visual content is preserved as the envelope. |
| Hybrid PDF in + non-PDF target | Extract embedded CII + convert + drop envelope | Envelope drop is recorded as acceptable loss. |
| XML in + ZUGFeRD/Factur-X target | URN swap only — returns CII XML, not a PDF | Beliq does not wrap a standalone XML invoice into a hybrid PDF. To produce a ZUGFeRD/Factur-X hybrid PDF, call POST /v1/generate with output: "pdf" from structured JSON. |
| FatturaPA / Facturae / KSeF / SDI messaggio source or target | Fail-closed (CONVERSION_UNSUPPORTED_PAIR) |
Non-EN-16931 national formats — no published cross-format converter. |
Response
Success (200)
The body is raw bytes — application/xml or application/pdf depending on the envelope. The audit metadata is in the response headers:
| Response header | Description |
|---|---|
Content-Type |
application/xml or application/pdf. |
X-Source-Format |
Resolved source format (the engine’s auto-detect result if sourceFormat=auto was passed). |
X-Target-Format |
Echo of the requested target format. |
X-Profile-Detected |
Profile label inferred from the source’s CustomizationID / guideline URN (e.g. en16931, xrechnung, peppol-bis, facturx:basic). Empty when the source carries no profile URN. |
X-Lost-Elements-Count |
Number of acceptable-loss entries this conversion recorded (Peppol endpoint strips, dropped PDF envelopes, France CTC opt-in losses). Equals JSON.parse(X-Lost-Elements).length. |
X-Lost-Elements |
JSON-encoded array of LostElement entries — the full structured audit trail. Always present ("[]" when nothing was lost). Defensive 6KB cap: if the serialised list would exceed the cap, the header carries a single {"truncated": true, …} sentinel and X-Lost-Elements-Count remains the exact count. The cap should never fire in practice. |
X-Conversion-Tools |
Comma-separated engine-internal labels in execution order (e.g. pdf-extract,mustang,urn-swap, helger-ubl2cii,urn-swap, mustang-combine). Useful for audit; don’t switch caller logic on these. |
X-Output-Envelope |
xml or pdf — matches Content-Type. |
X-Version-Block |
Compact JSON object with the artifact versions in play for this request (e.g. schematronVersion, ciusVersion, facturxVersion, national XSD bundle versions). Omitted when empty. Same fields as the POST /v1/validate response body; Beliq-Ruleset selection fields are absent here because conversion runs no Schematron verdict selection. |
LostElement schema
Each entry in X-Lost-Elements has this shape:
| Field | Type | Description |
|---|---|---|
path |
string | XPath of the lost element in the source document. |
value |
string | null | Element text content where applicable; null for envelope-level losses (e.g. dropped PDF envelope). |
reason |
string | Human-readable explanation of why the loss happened. |
severity |
"info" | "warning" |
Today every entry is "info"; reserved for future warnings. |
acceptableLoss |
boolean | Always true on a 200 response (un-acceptable losses surface as 422 CONVERSION_LOSSY_FAILCLOSED instead). |
Worked example: Peppol BIS UBL → plain EN 16931 UBL
Converting a Peppol invoice carrying both endpoint blocks to plain EN 16931 strips both as an acceptable loss:
[
{
"path": "/Invoice/{urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2}AccountingSupplierParty/{urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2}Party/cbc:EndpointID",
"value": null,
"reason": "Peppol endpoint stripped (target is not peppol-bis)",
"severity": "info",
"acceptableLoss": true
},
{
"path": "/Invoice/{urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2}AccountingCustomerParty/{urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2}Party/cbc:EndpointID",
"value": null,
"reason": "Peppol endpoint stripped (target is not peppol-bis)",
"severity": "info",
"acceptableLoss": true
}
]The corresponding response carries X-Lost-Elements-Count: 2.
Lossy conversions — semantics
Beliq splits “lossy” into two buckets with very different behaviour:
-
Fail-closed losses (default): the source carries a field the target cannot represent and the caller has not opted in. Returns
422 CONVERSION_LOSSY_FAILCLOSEDwith the un-mappable XPath(s) inerror.details.unmappablePaths. No body is returned. Today this fires on two paths: France CTC Flux 2 BusinessProcess codes on a UBL-syntax target (opt-in:?dropFranceCtcOverlay=true), and synthesising a Peppol BIS endpoint pair on a non-Peppol source (no opt-in — use/v1/generateinstead). -
Acceptable losses: the loss is intrinsic to the requested conversion and the caller is implicitly asking for it. Returns
200 OKwith the count surfaced inX-Lost-Elements-Count. Today this fires on: stripping Peppolcbc:EndpointIDblocks when leavingpeppol-bis, dropping the PDF envelope when converting a hybrid PDF to a target with no PDF spec, and dropping a France CTC code whendropFranceCtcOverlay=truewas set.
Error responses
| HTTP Status | Error Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR |
Request body is empty or query parameters are invalid |
| 422 | CONVERSION_UNSUPPORTED_PAIR |
Source format is outside the EN 16931 family (FatturaPA / Facturae / KSeF / SDI messaggio source or target), or the source body is a PDF with no embedded invoice XML |
| 422 | CONVERSION_LOSSY_FAILCLOSED |
Source carries fields the target cannot represent; see the lossy-conversion semantics above |
| 500 | CONVERSION_TOOL_ERROR |
An underlying JAR subprocess (Mustang cii2ubl, Helger ubl2cii, Mustang --action combine) returned a non-zero exit |
| 503 | CONVERSION_TOOL_UNAVAILABLE |
A required JAR is not deployed on the engine (operational; should not happen in production) |
| 503 | ENGINE_UNAVAILABLE |
The engine is temporarily unreachable (generic 5xx path) |
422 CONVERSION_UNSUPPORTED_PAIR example
{
"success": false,
"error": {
"code": "CONVERSION_UNSUPPORTED_PAIR",
"message": "Source format 'facturae' is not in the EN 16931 family (supported: cii / ubl / zugferd / facturx / xrechnung / peppol-bis).",
"details": {
"sourceFormat": "facturae",
"targetFormat": "ubl"
}
}
}422 CONVERSION_LOSSY_FAILCLOSED example (France CTC)
{
"success": false,
"error": {
"code": "CONVERSION_LOSSY_FAILCLOSED",
"message": "Source CII carries France CTC Flux 2 BusinessProcess code 'B8'; UBL has no equivalent extension and Mustang cii2ubl would silently drop it. Pass options.dropFranceCtcOverlay=true (or the ?drop_france_ctc_overlay=true query param) to accept the loss explicitly.",
"details": {
"unmappablePaths": [
"/rsm:CrossIndustryInvoice/rsm:ExchangedDocumentContext/ram:BusinessProcessSpecifiedDocumentContextParameter/ram:ID"
]
}
}
}503 CONVERSION_TOOL_UNAVAILABLE example
{
"success": false,
"error": {
"code": "CONVERSION_TOOL_UNAVAILABLE",
"message": "Mustang JAR not available",
"details": {
"missingTool": "mustang"
}
}
}Notes
- Lost-elements audit lives in headers (not in the response body) so callers using
--output filecurl idioms get the converted bytes verbatim while still being able to inspect the audit trail. Browsers must read these viaAccess-Control-Expose-Headers(already configured onbeliq-api). - The engine resolves
sourceFormat=autoby inspecting the XML root + theCustomizationID/GuidelineSpecifiedDocumentContextParameter/IDURN. Plain CII or UBL without a profile URN resolves asciiorublrespectively. - For ZUGFeRD vs Factur-X disambiguation (both share the same CII URN family), the engine treats them as profile labels on a CII document. Source-format resolution returns
facturxby default; passsourceFormat=zugferdexplicitly when the caller’s intent is ZUGFeRD-specific. - See Cross-format conversion on the format overview page for the conceptual map.