eliDocs
API Reference

POST /v1/convert

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:

convert-cii-to-ubl.sh
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.xml

Convert ZUGFeRD PDF → Factur-X PDF (re-wrap with the new profile URN):

convert-zugferd-to-facturx.sh
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.pdf

Convert a Peppol BIS UBL → plain EN 16931 UBL (strips the cbc:EndpointID blocks; acceptable loss recorded):

convert-peppol-to-en16931.sh
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 bytesapplication/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_FAILCLOSED with the un-mappable XPath(s) in error.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/generate instead).

  • Acceptable losses: the loss is intrinsic to the requested conversion and the caller is implicitly asking for it. Returns 200 OK with the count surfaced in X-Lost-Elements-Count. Today this fires on: stripping Peppol cbc:EndpointID blocks when leaving peppol-bis, dropping the PDF envelope when converting a hybrid PDF to a target with no PDF spec, and dropping a France CTC code when dropFranceCtcOverlay=true was 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 file curl idioms get the converted bytes verbatim while still being able to inspect the audit trail. Browsers must read these via Access-Control-Expose-Headers (already configured on beliq-api).
  • The engine resolves sourceFormat=auto by inspecting the XML root + the CustomizationID / GuidelineSpecifiedDocumentContextParameter/ID URN. Plain CII or UBL without a profile URN resolves as cii or ubl respectively.
  • 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 facturx by default; pass sourceFormat=zugferd explicitly when the caller’s intent is ZUGFeRD-specific.
  • See Cross-format conversion on the format overview page for the conceptual map.