Naar de inhoud

E-factuur-validatie-API

Controleer e-facturen via de REST-API op geselecteerde EN 16931-, XRechnung-, Peppol- en Factur-X-regels. Gratis binnen de daglimiet.

Deze functie is in bèta: ze werkt en is getest, maar details kunnen nog veranderen.

Op deze pagina

De e-factuur-validatie-API van Klarbon controleert UBL-, CII-, XRechnung-, Peppol BIS-, Factur-X- en ZUGFeRD-facturen met één REST-aanroep tegen de geldende gepubliceerde regels. Hij leidt de regelset af uit het bestand of neemt de set die je opgeeft, en geeft meldingen uit de gekozen controles terug met regel-id en ernst, met uitleg waar beschikbaar. Bestanden worden voor het verzoek verwerkt, zonder documentarchief.

Wat de API doet

  • POST /api/v1/check neemt het XML-bestand of de hybride pdf, standaard met ruleset=auto.
  • country kiest de beschikbare regelset voor een land als je weet waar de factuur naartoe gaat. Daarmee worden niet alle eisen van een specifieke ontvanger gecontroleerd.
  • Routes die e-facturen schrijven, geven ook controleresultaten terug. Bekijk de meldingen voordat je het bestand gebruikt.

Wat de API niet doet

Een geldig resultaat betekent dat het bestand de gekozen controles doorstaat. Het betekent niet dat de ontvanger het accepteert, en het is geen fiscaal of juridisch advies. Validatie is in bèta en gratis binnen de daglimiet van je abonnement. Omzetten doet de e-factuur-conversie-API.

Snel aan de slag

Elk voorbeeld doet dezelfde aanroep. Voor publicatie draait elk voorbeeld tegen een lokale Klarbon-server en de echte validatie.

Maak op uw accountpagina een sleutel aan en zet die in KLARBON_API_KEY. Zet de sleutel nooit in het adres.

POST /api/v1/check

curl + jq
curl -sS https://klarbon.app/api/v1/check \
  -H "Authorization: Bearer $KLARBON_API_KEY" \
  -F "file=@invoice.xml;type=application/xml" \
  -o result.json
jq -e '.ok' result.json > /dev/null || { jq -r '.error + ": " + .message' result.json; exit 1; }
jq '{valid: .report.valid, findings: [.report.findings[] | {ruleId, severity, message}]}' 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 Checked = { ok: true; report: { valid: boolean; findings: { ruleId: string; severity: string; message: string }[] } }

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

const response = await fetch("https://klarbon.app/api/v1/check", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.KLARBON_API_KEY}`,
  },
  body: form,
})
const result = (await response.json()) as Checked | Failure
if (!result.ok) throw new Error(`${response.status} ${result.error}: ${result.message}`)
console.log(result.report.valid ? "valid" : "not valid")
for (const finding of result.report.findings) console.log(finding.severity, finding.ruleId, finding.message)
Python 3.9+, requests
import os

import requests

with open("invoice.xml", "rb") as upload:
    response = requests.post(
        "https://klarbon.app/api/v1/check",
        headers={
            "Authorization": f"Bearer {os.environ['KLARBON_API_KEY']}",
        },
        files={"file": ("invoice.xml", upload, "application/xml")},
        timeout=120,
    )
result = response.json()
if not result["ok"]:
    raise SystemExit(f"{response.status_code} {result['error']}: {result['message']}")
print("valid" if result["report"]["valid"] else "not valid")
for finding in result["report"]["findings"]:
    print(finding["severity"], finding["ruleId"], finding["message"])
PHP 8+, curl
<?php
$curl = curl_init('https://klarbon.app/api/v1/check');
curl_setopt_array($curl, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 120,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('KLARBON_API_KEY'),
    ],
    CURLOPT_POSTFIELDS => [
        'file' => new CURLFile('invoice.xml', 'application/xml', 'invoice.xml'),
    ],
]);
$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);
}
echo $result['report']['valid'] ? "valid\n" : "not valid\n";
foreach ($result['report']['findings'] as $finding) {
    echo "{$finding['severity']} {$finding['ruleId']} {$finding['message']}\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/check") { Content = upload };
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 report = result.GetProperty("report");
Console.WriteLine(report.GetProperty("valid").GetBoolean() ? "valid" : "not valid");
foreach (var finding in report.GetProperty("findings").EnumerateArray())
    Console.WriteLine($"{finding.GetProperty("severity")} {finding.GetProperty("ruleId")} {finding.GetProperty("message")}");
return 0;

Het antwoord

Het antwoord op de curl-aanroep hierboven, vastgelegd bij die run met KoSIT XRechnung test suite, business case 01.01a (UBL), Apache-2.0.

{
  "ok": true,
  "format": "xrechnung",
  "report": {
    "detected": {
      "syntax": "ubl",
      "customizationId": "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0",
      "profile": null,
      "container": "xml"
    },
    "ruleset": {
      "id": "xrechnung-3.0-ubl",
      "name": "XRechnung 3.0 (UBL)",
      "version": "3.0.2"
    },
    "valid": true,
    "engineVersion": "1.0.0",
    "durationMs": 11,
    "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": "/*:Invoice[namespace-uri()='urn:oasis:names:specification:ubl:schema:xsd:Invoice-2'][1]",
        "test": "cac:Delivery/cbc:ActualDeliveryDate or cac:InvoicePeriod or (every $line in (cac:InvoiceLine | cac:CreditNoteLine) satisfies $line/cac:InvoicePeriod)",
        "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
      }
    ]
  },
  "invoice": {
    "kind": "invoice",
    "typeCode": "380",
    "number": "123456XX",
    "issueDate": "2016-04-04",
    "currency": "EUR",
    "buyerReference": "04011000-12345-03",
    "paymentTerms": "Zahlbar sofort ohne Abzug.",
    "processId": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0",
    "specificationId": "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0",
    "notes": [
      {
        "text": "Es gelten unsere Allgem. Geschäftsbedingungen, die Sie unter […] finden.",
        "subjectCode": "ADU"
      }
    ],
    "precedingInvoices": [],
    "seller": {
      "name": "[Seller name]",
      "tradingName": "[Seller trading name]",
      "identifiers": [],
      "legalRegistration": {
        "value": "[HRA-Eintrag]"
      },
      "vatId": "DE 123456789",
      "legalInformation": "123/456/7890, HRA-Eintrag in […]",
      "electronicAddress": {
        "value": "seller@email.de",
        "scheme": "EM"
      },
      "address": {
        "line1": "[Seller address line 1]",
        "city": "[Seller city]",
        "postCode": "12345",
        "country": "DE"
      },
      "contact": {
        "name": "nicht vorhanden",
        "telephone": "+49 1234-5678",
        "email": "seller@email.de"
      }
    },
    "buyer": {
      "name": "[Buyer name]",
      "identifiers": [
        {
          "value": "[Buyer identifier]"
        }
      ],
      "electronicAddress": {
        "value": "buyer@info.de",
        "scheme": "EM"
      },
      "address": {
        "line1": "[Buyer address line 1]",
        "city": "[Buyer city]",
        "postCode": "12345",
        "country": "DE"
      }
    },
    "payment": {
      "meansCode": "58",
      "creditTransfers": [
        {
          "iban": "DE79000000001234567890"
        }
      ]
    },
    "allowances": [],
    "charges": [],
    "totals": {
      "lineTotal": "314.86",
      "taxExclusive": "314.86",
      "tax": "22.04",
      "taxInclusive": "336.9",
      "payable": "336.9"
    },
    "vatBreakdown": [
      {
        "category": "S",
        "rate": "7",
        "taxableAmount": "314.86",
        "taxAmount": "22.04"
      }
    ],
    "attachments": [],
    "lines": [
      {
        "id": "Zeitschrift [...]",
        "note": "Die letzte Lieferung im Rahmen des abgerechneten Abonnements erfolgt in 12/2016 Lieferung erfolgt / erfolgte direkt vom Verlag",
        "quantity": "1",
        "unit": "XPP",
        "netAmount": "288.79",
        "orderLineReference": "6171175.1",
        "period": {
          "start": "2016-01-01",
          "end": "2016-12-31"
        },
        "allowances": [],
        "charges": [],
        "price": {
          "amount": "288.79"
        },
        "vat": {
          "category": "S",
          "rate": "7"
        },
        "item": {
          "name": "Zeitschrift [...]",
          "description": "Zeitschrift Inland",
          "sellerId": "246",
          "classifications": [
            {
              "value": "0721-880X",
              "scheme": "IB"
            }
          ],
          "attributes": []
        }
      },
      {
        "id": "Porto + Versandkosten",
        "quantity": "1",
        "unit": "XPP",
        "netAmount": "26.07",
        "allowances": [],
        "charges": [],
        "price": {
          "amount": "26.07"
        },
        "vat": {
          "category": "S",
          "rate": "7"
        },
        "item": {
          "name": "Porto + Versandkosten",
          "classifications": [],
          "attributes": []
        }
      }
    ],
    "nationalTerms": [],
    "source": {
      "format": "xrechnung",
      "syntax": "ubl",
      "customizationId": "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0"
    }
  },
  "usage": {
    "used": 1,
    "limit": 20
  }
}

Kosten

Vooraf betaalde API-pakketten, per kaart gekocht op uw accountpagina: 1.000 aanroepen voor € 39,00, 5.000 aanroepen voor € 129,00, 25.000 aanroepen voor € 449,00, 100.000 aanroepen voor € 990,00, zonder vervaldatum. Uw eerste sleutel geeft 25 gratis aanroepen. Betaalde aanroepen gebruiken eerst het API-saldo, daarna dezelfde credits als de site. Een mislukte aanroep kost niets.

Aanroep Kosten
POST /open Gratis, redelijk gebruik tot 1.000 per dag per account
POST /check Gratis binnen de daglimiet van uw abonnement (20 per dag in het gratis abonnement)
POST /convert, POST /make 1 aanroep, alleen als het gemaakte bestand geldig is
POST /bank/convert 1 aanroep per begonnen 500 transacties, alleen na omzetting

Limieten

  • Bestanden tot 20 MB, pdf’s tot 50 pagina’s.
  • 60 verzoeken per minuut per sleutel, 120 per account. Een 429 bevat Retry-After.
  • Tot 10 sleutels per account. Idempotency-Key geldt 24 uur, dus een nieuwe poging wordt nooit twee keer berekend.

Fouten

Elke fout is { ok: false, error, message }: een vaste code en één duidelijke zin.

Status Code Wanneer
401 auth Geen sleutel, of een onbekende of ingetrokken sleutel.
402 no_credits API-saldo en credits zijn allebei te laag. Er is niets gedaan; topUpUrl wijst naar de aankooppagina.
413 too_large Meer dan 20 MB of 50 pdf-pagina’s.
415 unsupported Een bestandsformaat dat de route niet aanneemt.
422 invalid Het bestand was niet leesbaar, of het gemaakte bestand is niet geldig. Niet berekend.
429 limit Een snelheidslimiet of de gratis aanroepen van de dag.
503 engine_unavailable De validatie is even niet bereikbaar. Niet berekend.

Privacy

Bestanden worden voor het verzoek verwerkt, zonder documentarchief. De applicatielogs van Klarbon bevatten geen bestandsnaam, geen inhoud en geen sleutel. De volledige sleutel wordt niet bewaard, alleen een HMAC ervan en een kort weergaveprefix.

Verder

De documentatie voor ontwikkelaars is in het Engels.

Veelgestelde vragen

Tegen welke regelsets kan ik controleren?

EN 16931 in UBL en CII, XRechnung, Peppol BIS Billing, de profielen van Factur-X en ZUGFeRD, NLCIUS en de Franse CTC-regels. GET /api/v1/formats toont wat beschikbaar is.

Hoe kiest de API de regelset?

Uit de customization-id en de container van het bestand, tenzij je ruleset of country meegeeft. Het rapport noemt de gebruikte set.

Wat is het verschil tussen fout, waarschuwing en informatie?

Een fatale melding betekent dat het bestand niet voldoet aan een regel van de gekozen controle. Waarschuwingen en informatie laten die controle niet mislukken, maar kunnen wel van belang zijn voor de ontvanger.

Accepteert mijn klant een geldige factuur altijd?

Niet altijd. Geldig betekent dat het bestand de gekozen controles heeft doorstaan. Ontvangers en platforms kunnen eigen eisen stellen.

Bronnen

Bijgewerkt op 6 november 2026