Skip to content

XRechnung API

Convert CII, UBL and Peppol invoices to XRechnung and back with one REST call. Charged only when the file validates. No document archive is kept.

On this page

The Klarbon XRechnung API converts CII, UBL and Peppol BIS invoices to XRechnung, and XRechnung back to plain UBL or CII, with one REST call. Every file it writes is validated against the rule set selected for the output format before you get it, and a conversion that does not validate costs nothing. Files are processed for the request, without a document archive.

What the API does

  • POST /api/v1/convert with target=xrechnung writes XRechnung 3.0 in the syntax of your source, UBL or CII.
  • The answer carries the file and the full validation report, so you see the rule set and every finding.
  • target=ubl or target=cii turns an XRechnung into the plain EN 16931 syntax.
  • losses reports fields changed, omitted or assumed when writing the target. It does not include source fields the reader could not carry into the invoice model; compare the result with the original.

What it does not do

XRechnung asks for data some invoices lack, such as a buyer reference. A German public-sector recipient may give you a Leitweg-ID for this field. The API reports what is missing; it never invents it. Klarbon does not send invoices to ZRE, OZG-RE or any other portal. Checking an XRechnung on its own is in beta, see the e-invoice validation API.

Quick start

Each snippet is the same call. The end-to-end test runs these snippets against a local Klarbon server and the real validation engine.

Create a key on your account page and put it in KLARBON_API_KEY. Never put it in the address.

POST /api/v1/convert

curl + jq
curl -sS https://klarbon.app/api/v1/convert \
  -H "Authorization: Bearer $KLARBON_API_KEY" \
  -H "Idempotency-Key: invoice-123456XX-xrechnung" \
  -F "file=@invoice.xml;type=application/xml" \
  -F target=xrechnung \
  -o result.json
jq -e '.ok' result.json > /dev/null || { jq -r '.error + ": " + .message' result.json; exit 1; }
jq -r '.file.content' result.json | base64 --decode > "$(jq -r '.file.filename' result.json)"
TypeScript, Node.js 22.18+
import { readFile, writeFile } from "node:fs/promises"

type KlarbonFile = { content: string; filename: string; mime: string; bytes: number }
type Failure = { ok: false; error: string; message: string }
type Converted = { ok: true; file: KlarbonFile; credits: { spent: number; balance: number } }

const form = new FormData()
form.append("file", new Blob([await readFile("invoice.xml")], { type: "application/xml" }), "invoice.xml")
form.append("target", "xrechnung")

const response = await fetch("https://klarbon.app/api/v1/convert", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.KLARBON_API_KEY}`,
    "Idempotency-Key": "invoice-123456XX-xrechnung",
  },
  body: form,
})
const result = (await response.json()) as Converted | Failure
if (!result.ok) throw new Error(`${response.status} ${result.error}: ${result.message}`)
await writeFile(result.file.filename, Buffer.from(result.file.content, "base64"))
console.log(`${result.file.filename} written, ${result.credits.balance} credits left`)
Python 3.9+, requests
import base64
import os

import requests

with open("invoice.xml", "rb") as upload:
    response = requests.post(
        "https://klarbon.app/api/v1/convert",
        headers={
            "Authorization": f"Bearer {os.environ['KLARBON_API_KEY']}",
            "Idempotency-Key": "invoice-123456XX-xrechnung",
        },
        files={"file": ("invoice.xml", upload, "application/xml")},
        data={"target": "xrechnung"},
        timeout=120,
    )
result = response.json()
if not result["ok"]:
    raise SystemExit(f"{response.status_code} {result['error']}: {result['message']}")
with open(result["file"]["filename"], "wb") as output:
    output.write(base64.b64decode(result["file"]["content"]))
print(result["file"]["filename"], "written,", result["credits"]["balance"], "credits left")
PHP 8+, curl
<?php
$curl = curl_init('https://klarbon.app/api/v1/convert');
curl_setopt_array($curl, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 120,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('KLARBON_API_KEY'),
        'Idempotency-Key: invoice-123456XX-xrechnung',
    ],
    CURLOPT_POSTFIELDS => [
        'file' => new CURLFile('invoice.xml', 'application/xml', 'invoice.xml'),
        'target' => 'xrechnung',
    ],
]);
$body = curl_exec($curl);
if ($body === false) {
    fwrite(STDERR, curl_error($curl) . "\n");
    exit(1);
}
$result = json_decode($body, true);
if (!$result['ok']) {
    $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
    fwrite(STDERR, "$status {$result['error']}: {$result['message']}\n");
    exit(1);
}
file_put_contents($result['file']['filename'], base64_decode($result['file']['content']));
echo "{$result['file']['filename']} written, {$result['credits']['balance']} credits left\n";
C#, .NET 10
using System.Net.Http.Headers;
using System.Text.Json;

using var http = new HttpClient();
var key = Environment.GetEnvironmentVariable("KLARBON_API_KEY");
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", key);

var upload = new ByteArrayContent(await File.ReadAllBytesAsync("invoice.xml"));
upload.Headers.ContentType = new MediaTypeHeaderValue("application/xml");

using var request = new HttpRequestMessage(HttpMethod.Post, "https://klarbon.app/api/v1/convert?target=xrechnung") { Content = upload };
request.Headers.Add("Idempotency-Key", "invoice-123456XX-xrechnung");
using var response = await http.SendAsync(request);
using var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
var result = json.RootElement;
if (!result.GetProperty("ok").GetBoolean())
{
    Console.Error.WriteLine($"{(int)response.StatusCode} {result.GetProperty("error")}: {result.GetProperty("message")}");
    return 1;
}
var written = result.GetProperty("file");
await File.WriteAllBytesAsync(written.GetProperty("filename").GetString()!,
    Convert.FromBase64String(written.GetProperty("content").GetString()!));
Console.WriteLine($"{written.GetProperty("filename")} written, {result.GetProperty("credits").GetProperty("balance")} credits left");
return 0;

What comes back

The answer to the curl call above, recorded from that run with KoSIT XRechnung test suite, business case 01.01a (CII), Apache-2.0.

Shortened for the page: file.content: base64 cut

{
  "ok": true,
  "file": {
    "content": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgi...",
    "filename": "123456XX-xrechnung.xml",
    "mime": "application/xml",
    "bytes": 6867
  },
  "report": {
    "detected": {
      "syntax": "cii",
      "customizationId": "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0",
      "profile": "xrechnung",
      "container": "xml"
    },
    "ruleset": {
      "id": "xrechnung-3.0-cii",
      "name": "XRechnung 3.0 (CII)",
      "version": "3.0.2"
    },
    "valid": true,
    "engineVersion": "1.0.0",
    "durationMs": 45,
    "findings": [
      {
        "ruleId": "BR-DE-TMP-32",
        "severity": "info",
        "message": "[BR-DE-TMP-32] Eine Rechnung sollte zur Angabe des Liefer-/Leistungsdatums entweder BT-72 \"Actual delivery date\", BG-14 \"Invoicing period\" oder in jeder Rechnungsposition BG-26 \"Invoice line period\" enthalten.",
        "location": "/rsm:CrossIndustryInvoice[1]/rsm:SupplyChainTradeTransaction[1]",
        "test": "ram:ApplicableHeaderTradeDelivery/ram:ActualDeliverySupplyChainEvent/ram:OccurrenceDateTime or ram:ApplicableHeaderTradeSettlement/ram:BillingSpecifiedPeriod or (every $line in ram:IncludedSupplyChainTradeLineItem satisfies $line/ram:SpecifiedLineTradeSettlement/ram:BillingSpecifiedPeriod)",
        "title": "BR-DE-TMP-32",
        "what": "[BR-DE-TMP-32] Eine Rechnung sollte zur Angabe des Liefer-/Leistungsdatums entweder BT-72 \"Actual delivery date\", BG-14 \"Invoicing period\" oder in jeder Rechnungsposition BG-26 \"Invoice line period\" enthalten.",
        "fix": "",
        "link": null,
        "officialUrl": null
      }
    ]
  },
  "losses": [],
  "credits": {
    "spent": 1,
    "balance": 200,
    "plan": 0,
    "bought": 200
  }
}

What it costs

Prepaid API packs, bought by card on your account page: 1,000 calls for €39.00, 5,000 calls for €129.00, 25,000 calls for €449.00, 100,000 calls for €990.00, VAT included, never expiring. Your first key brings 25 free calls once per email address, excluding disposable addresses. Paid calls spend the API balance first, then the same credits as the site. A call that fails costs nothing.

Call Cost
POST /open Free, fair use 1,000 a day per account
POST /check Free within your plan's daily checks (20 a day on the free plan)
POST /convert, POST /make 1 call, only when the written file validates
POST /bank/convert 1 call per started 500 transactions, only when converted

Limits

  • Files up to 20 MB, PDFs up to 50 pages.
  • 60 requests a minute per key, 120 per account. A per-minute rate-limit refusal carries Retry-After.
  • Up to 10 keys per account. Idempotency-Key is honoured for 24 hours, so a retry never charges twice.

Errors

Every error is { ok: false, error, message }: a stable code and one plain sentence.

Status Code When
401 auth No key, or an unknown or revoked key.
402 no_credits The combined API balance and credits cannot cover this call. Nothing was charged; topUpUrl says where to buy.
413 too_large Over 20 MB or 50 PDF pages.
415 unsupported A file format the route does not take.
422 invalid The file could not be read, or the written file did not validate. Not charged.
429 limit A rate limit or the day's free calls.
503 engine_unavailable Validation is briefly unavailable. Not charged.

Privacy

Files are processed for the request, without a document archive. Klarbon's application logs carry no file name, no content and no key. The full key is not stored, only an HMAC of it and a short display prefix.

Next steps

Frequently asked questions

Is the XRechnung I get back valid?

It is offered only when it passes the XRechnung rule set the engine picks for it, and the report in the same answer shows which one and every finding.

What if my invoice lacks a field XRechnung requires?

The call answers 422 with the report naming the missing field, for example the buyer reference. Nothing is charged; add the data and send it again.

Can I convert XRechnung back to UBL or CII?

Yes. Send the XRechnung with target ubl or cii and you get the plain EN 16931 file in that syntax.

Does Klarbon submit the invoice to a public-sector portal?

No. Klarbon writes and checks the file. You upload or send it through ZRE, OZG-RE or your own channel.

Sources

Updated 6 November 2026