Dokumentation

E-Rechnungs-API

Ein POST-Aufruf validiert eine UBL-, CII-, XRechnung- oder Factur-X-Rechnung gegen die offiziellen EN-16931-Regeln und liefert jeden Fehler verständlich erklärt. Ein zweiter baut aus schlichtem JSON eine Factur-X-Rechnung und validiert sie, bevor er sie zurückgibt. Diese Seite ist die gesamte Referenz.

Quickstart

Erstellen Sie einen kostenlosen API-Schlüssel (100 Validierungen plus 20 erzeugte Rechnungen pro Monat, ohne Karte) und validieren Sie die erste Rechnung direkt im Terminal:

Kostenlosen API-Schlüssel holen

curl https://api.einvoicekit.com/v1/validate \
  -H "Authorization: Bearer eik_live_..." \
  -H "Content-Type: application/xml" \
  --data-binary @invoice.xml

Authentifizierung

Ihr Schlüssel steht im Authorization-Header. Nur POST /v1/validate antwortet auch ohne Schlüssel, über den kostenlosen Pool aus seinem Abschnitt; die Generierung braucht immer einen Schlüssel:

Authorization: Bearer eik_live_...

Schlüssel beginnen mit eik_live_ und werden genau einmal angezeigt, bei der Erstellung im Dashboard. Behandeln Sie sie wie Passwörter: Umgebungsvariablen, nie ein Repository.

Pakete

Dieselbe Validierung als Paket, ohne HTTP-Code zu schreiben: eine Funktion und ein Befehl, auf npm und auf PyPI. Ohne Schlüssel laufen sie über den kostenlosen Pool aus dem Abschnitt zu POST /v1/validate; mit gesetztem EINVOICEKIT_API_KEY über Ihr Konto.

Node.js 20+, von npm

npx @einvoicekit/einvoicekit invoice.pdf

npm install @einvoicekit/einvoicekit

Python 3.10+, von PyPI

pipx run einvoicekit invoice.pdf

pip install einvoicekit

Der Befehl gibt jede verletzte Regel aus und endet mit Exit-Code 0, wenn alle Dateien gültig sind, 2, wenn eine Datei gar nicht geprüft werden konnte, und 1, wenn der Rest mindestens eine ungültige Rechnung enthält; so passt er unverändert in eine CI-Pipeline. Die Funktion validate liefert dasselbe Ergebnis im Code: in JavaScript das JSON dieser API, wie es ankommt, in Python ein typisiertes Ergebnis, das dieses JSON unter .raw behält. Beide Pakete sind schlanke Clients: Die Datei wird per HTTPS gesendet, im Speicher verarbeitet und verworfen, nie gespeichert. Keine Abhängigkeiten, MIT-Lizenz, Quellcode und vollständige READMEs auf GitHub.

POST /v1/validate

Validiert ein Rechnungsdokument und liefert das Ergebnis mit jeder verletzten Regel. Beide Syntaxen (UBL und CII) und beide Profilfamilien (XRechnung, Factur-X/ZUGFeRD) werden automatisch erkannt.

Anfrage

Senden Sie das XML als rohen Request-Body mit Content-Type: application/xml oder als multipart/form-data mit einem einzelnen file-Feld. Ein Factur-X-/ZUGFeRD-PDF wird direkt akzeptiert: das eingebettete XML wird vor der Validierung extrahiert. Maximale Größe: 5 MB.

Antwort

Immer JSON. valid ist das Ergebnis; errors listet jede verletzte Regel mit EN-16931-Regel-ID, der offiziellen Regelmeldung (sie nennt die betroffenen Geschäftsterme, BT-x) und dem XPath des betroffenen Elements. warnings hat dieselbe Form und ändert das Ergebnis nicht.

{
  "valid": false,
  "syntax": "cii",
  "profile": "urn:cen.eu:en16931:2017",
  "errors": [
    {
      "rule": "BR-CO-15",
      "message": "[BR-CO-15]-Invoice total amount with VAT (BT-112) = Invoice total amount without VAT (BT-109) + Invoice total VAT amount (BT-110).",
      "path": "/Q{urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100}CrossIndustryInvoice[1]"
    }
  ],
  "warnings": []
}

Ohne Schlüssel

Senden Sie dieselbe Anfrage ohne Authorization-Header, und sie läuft über den anonymen Pool der kostenlosen Tool-Seiten: 10 Validierungen pro Tag und IP-Adresse, geteilt zwischen der Validator-Seite, der Generator-Seite und schlüssellosen API-Aufrufen, zurückgesetzt um Mitternacht UTC. Nur ein gelieferter Befund zählt; ein 400, ein 422 oder ein Fehler auf unserer Seite gibt den Lauf zurück. Der Aufruf nach dem letzten ist ein 429 pool_exhausted, der die Rücksetzzeit und den kostenlosen Schlüssel nennt, der 100 Validierungen im Monat gibt. Ein vorhandener, aber falscher Schlüssel ist ein 401, nie ein Rückfall auf den Pool.

curl -s -X POST \
  --data-binary @invoice.xml \
  https://api.einvoicekit.com/v1/validate
{
  "error": "pool_exhausted",
  "message": "10 free validations a day per IP address without a key. A free key gives 100 a month: https://einvoicekit.com/get-started?from=api-pool",
  "resetsAt": "2026-09-04T00:00:00.000Z",
  "upgrade": "https://einvoicekit.com/get-started?from=api-pool"
}

Aufruf aus Ihrer Sprache

curl

curl https://api.einvoicekit.com/v1/validate \
  -H "Authorization: Bearer eik_live_..." \
  -H "Content-Type: application/xml" \
  --data-binary @invoice.xml

Python

# pip install requests
import requests

with open("invoice.xml", "rb") as f:
    response = requests.post(
        "https://api.einvoicekit.com/v1/validate",
        headers={
            "Authorization": "Bearer eik_live_...",
            "Content-Type": "application/xml",
        },
        data=f.read(),
    )

result = response.json()
print(result["valid"], result["errors"])

Node.js

// Node.js 18+ (built-in fetch), run as an ES module
import { readFile } from "node:fs/promises";

const response = await fetch("https://api.einvoicekit.com/v1/validate", {
  method: "POST",
  headers: {
    Authorization: "Bearer eik_live_...",
    "Content-Type": "application/xml",
  },
  body: await readFile("invoice.xml"),
});

const result = await response.json();
console.log(result.valid, result.errors);

PHP

<?php
$ch = curl_init("https://api.einvoicekit.com/v1/validate");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer eik_live_...",
        "Content-Type: application/xml",
    ],
    CURLOPT_POSTFIELDS => file_get_contents("invoice.xml"),
]);

$result = json_decode(curl_exec($ch), true);
echo $result["valid"] ? "valid" : "invalid", PHP_EOL;

C#

// .NET 6+ (top-level statements)
using System.Net.Http.Headers;
using System.Text.Json;

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", "eik_live_...");

var content = new ByteArrayContent(File.ReadAllBytes("invoice.xml"));
content.Headers.ContentType = new MediaTypeHeaderValue("application/xml");

var response = await http.PostAsync("https://api.einvoicekit.com/v1/validate", content);
using var result = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
Console.WriteLine(result.RootElement.GetProperty("valid"));

Java

// Java 11+
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Path;

public class ValidateInvoice {
    public static void main(String[] args) throws Exception {
        var client = HttpClient.newHttpClient();
        var request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.einvoicekit.com/v1/validate"))
            .header("Authorization", "Bearer eik_live_...")
            .header("Content-Type", "application/xml")
            .POST(HttpRequest.BodyPublishers.ofFile(Path.of("invoice.xml")))
            .build();

        var response = client.send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.body());
    }
}

Go

package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
)

func main() {
	xml, err := os.ReadFile("invoice.xml")
	if err != nil {
		log.Fatal(err)
	}
	req, err := http.NewRequest(http.MethodPost, "https://api.einvoicekit.com/v1/validate", bytes.NewReader(xml))
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("Authorization", "Bearer eik_live_...")
	req.Header.Set("Content-Type", "application/xml")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(body))
}

Rust

// cargo add reqwest --features blocking,json
// cargo add serde_json
use std::fs;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let xml = fs::read("invoice.xml")?;
    let result: serde_json::Value = reqwest::blocking::Client::new()
        .post("https://api.einvoicekit.com/v1/validate")
        .header("Authorization", "Bearer eik_live_...")
        .header("Content-Type", "application/xml")
        .body(xml)
        .send()?
        .json()?;

    println!("{} {}", result["valid"], result["errors"]);
    Ok(())
}

Swift

// Swift 5.7+, macOS 12+ — run as main.swift or 'swift validate.swift'
// (on Linux, add: import FoundationNetworking)
import Foundation

var request = URLRequest(url: URL(string: "https://api.einvoicekit.com/v1/validate")!)
request.httpMethod = "POST"
request.setValue("Bearer eik_live_...", forHTTPHeaderField: "Authorization")
request.setValue("application/xml", forHTTPHeaderField: "Content-Type")
request.httpBody = try Data(contentsOf: URL(fileURLWithPath: "invoice.xml"))

let (data, _) = try await URLSession.shared.data(for: request)
let result = try JSONSerialization.jsonObject(with: data) as! [String: Any]
print(result["valid"] ?? "?", result["errors"] ?? [])

Französische Regeln (BR-FR)

Frankreich ergänzt EN 16931 um eigene Geschäftsregeln: das BR-FR-Regelwerk, das zugelassene Plattformen im Rahmen der E-Rechnungspflicht 2026 anwenden, veröffentlicht vom FNFE-MPE. Die Aktivierung erfolgt pro Anfrage über den target-Parameter:

curl -s -X POST \
  -H "Authorization: Bearer eik_live_..." \
  --data-binary @invoice.xml \
  "https://api.einvoicekit.com/v1/validate?target=france"

Der Parameter ist opt-in, weil sich französische Regeln nicht automatisch erkennen lassen: eine französische EN-16931-Rechnung ist strukturell identisch mit jeder anderen. Ein Aufruf mit target=france kostet dasselbe eine Dokument wie jede Validierung.

Akzeptierte Dokumente: EN-16931-Rechnungen in UBL (Invoice und CreditNote) oder CII sowie die Factur-X-Profile BASIC WL, EN 16931 und EXTENDED. Jedes andere Profil liefert 422, statt die französische Stufe stillschweigend zu überspringen: ein grünes Ergebnis heißt immer, dass die französischen Regeln wirklich gelaufen sind.

Die Antwort gibt target zurück und listet verletzte französische Regeln neben allen anderen Befunden. Die Regelmeldungen sind auf Französisch, genau wie der FNFE-MPE sie veröffentlicht:

{
  "valid": false,
  "syntax": "cii",
  "profile": "urn:cen.eu:en16931:2017",
  "target": "france",
  "errors": [
    {
      "rule": "BR-FR-05_BT-22_PMT",
      "message": "BR-FR-05/BT-22 : La mention relative aux frais de recouvrement (code PMT) est absente. Elle est obligatoire dans les notes (BG-1).",
      "path": "/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:ExchangedDocument[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]"
    }
  ],
  "warnings": []
}

POST /v1/generate

Erzeugt eine Factur-X-Rechnung aus einfachem JSON. Wir berechnen sämtliche Summen selbst, prüfen das fertige Dokument gegen dasselbe offizielle Regelwerk wie /v1/validate und lehnen den Aufruf lieber ab, als Ihnen eine Datei auszuliefern, die eine empfangende Plattform zurückweisen würde.

Diese Version erzeugt Factur-X, wahlweise im Profil en16931 oder in extended, als CII-XML oder als PDF, in dem dieses XML steckt. Entsprechend akzeptiert format nur facturx, output die Werte xml oder pdf und profile die Werte en16931 (der Standard) oder extended; jeder andere Wert führt zu einem 400, der ihn benennt. language akzeptiert fr, en oder de und bestimmt die Beschriftungen auf dem PDF. Ihre eigenen Texte bleiben davon unberührt: Firmierungen, Positionsbezeichnungen, Zahlungsbedingungen und Hinweise erscheinen genau so, wie Sie sie gesendet haben. logo wird auf das PDF gezeichnet und bei xml ignoriert.

Anfrage

Senden Sie die Rechnung per POST als JSON. Das Schema ist geschlossen: Ein unbekannter Schlüssel führt zu einem 400 mit einem JSON-Pointer auf die betroffene Stelle, statt stillschweigend verworfen zu werden. Ein Feld, das EN 16931 zwar definiert, diese Version aber nicht unterstützt, führt ebenfalls zu einem 400, der das Feld benennt, statt so behandelt zu werden, als hätten Sie es nie gesendet.

curl https://api.einvoicekit.com/v1/generate \
  -H "Authorization: Bearer eik_live_..." \
  -H "Content-Type: application/json" \
  --data-binary @invoice.json
{
  "format": "facturx",
  "profile": "en16931",
  "output": "xml",
  "language": "fr",
  "invoice": {
    "number": "F-2026-0042",
    "issueDate": "2026-09-15",
    "typeCode": "380",
    "currency": "EUR",
    "dueDate": "2026-10-15",
    "paymentTerms": "Paiement a 30 jours.",
    "buyerReference": "04011000-1234512345-06",
    "purchaseOrderReference": "PO-889",
    "notes": [{ "text": "Penalites de retard: 3x le taux legal." }],
    "seller": {
      "name": "Atelier Dupont",
      "vatId": "FR12345678901",
      "legalRegistrationId": "12345678900012",
      "legalInformation": "SARL au capital de 10 000 EUR - RCS Paris B 123 456 789",
      "electronicAddress": { "value": "12345678900012", "scheme": "0009" },
      "address": {
        "line1": "3 rue des Lilas",
        "city": "Paris",
        "postCode": "75011",
        "country": "FR"
      },
      "contact": {
        "name": "Marie Dupont",
        "phone": "+33 1 23 45 67 89",
        "email": "compta@atelier-dupont.fr"
      }
    },
    "buyer": {
      "name": "Beispiel GmbH",
      "vatId": "DE123456789",
      "address": {
        "line1": "Hauptstrasse 12",
        "city": "Berlin",
        "postCode": "10115",
        "country": "DE"
      }
    },
    "delivery": { "date": "2026-09-10", "country": "FR" },
    "payment": {
      "meansCode": "58",
      "iban": "FR7630006000011234567890189",
      "bic": "AGRIFRPP",
      "remittanceInformation": "F-2026-0042"
    },
    "allowances": [
      {
        "amount": "100.00",
        "reason": "Remise commerciale",
        "vat": { "category": "S", "rate": "20" }
      }
    ],
    "charges": [
      {
        "percentage": "1.50",
        "baseAmount": "7800.00",
        "reason": "Frais de dossier",
        "vat": { "category": "S", "rate": "20" }
      }
    ],
    "lines": [
      {
        "id": "1",
        "name": "Prestation de developpement",
        "quantity": "12",
        "unit": "HUR",
        "unitPrice": "650.00",
        "vat": { "category": "S", "rate": "20" }
      }
    ]
  }
}

Beträge, Mengen, Sätze und Prozentwerte sind Strings: "650.00", "12", "5.5". Das ist die kanonische Form, denn eine JSON-Zahl ist ein IEEE-Double, und für Geldbeträge ist das keine geeignete Darstellung. Nackte Zahlen werden an der Schnittstelle dennoch toleriert und über ihre kürzeste Dezimaldarstellung eingelesen; aus 650.00 wird dabei 650. Überschreitet ein Wert danach das Limit seines Feldes, ist die Antwort ein 400. Beträge dürfen höchstens 2 Nachkommastellen haben, Mengen und Einzelpreise bis zu 6, Sätze und Prozentwerte bis zu 2.

Summen senden Sie nicht, und Sie können sie deshalb auch nicht falsch berechnen: Wir leiten jede einzelne selbst ab und runden dabei kaufmännisch (bei 5 von null weg), einmal pro Position und einmal pro Umsatzsteuersatz. Senden Sie dennoch ein totals-Objekt, vergleichen wir es mit unserem Ergebnis und lehnen bei Abweichung mit 422 totals_mismatch ab, wobei beide Werte ausgewiesen werden. Es sind Ihre Bücher und Ihre Zahlen: Wir überschreiben sie nie stillschweigend, Sie erhalten stattdessen eine explizite Warnung.

Antwort

Standardmäßig JSON: das CII-Dokument als Base64, die von uns berechneten Summen und das Ergebnis jeder Pipeline-Stufe. pdfa3 steht auf skipped, weil bei output xml kein PDF gerendert wird und es daher nichts zu prüfen gab; fordern Sie ein PDF an, steht dort ein echtes Prüfergebnis. Befunde, die das Regelwerk als Warnung und nicht als Fehler einstuft, kommen in einem warnings-Array zurück und ändern das Ergebnis nicht. Ein rounding-Schlüssel erscheint innerhalb von totals nur dann, wenn Ihr roundingAmount ungleich 0 ist.

{
  "format": "facturx",
  "profile": "en16931",
  "xml": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4...",
  "totals": {
    "lineNet": "7800.00",
    "allowances": "100.00",
    "charges": "117.00",
    "taxBasis": "7817.00",
    "vat": "1563.40",
    "grossTotal": "9380.40",
    "prepaid": "0.00",
    "amountDue": "9380.40"
  },
  "validation": {
    "valid": true,
    "stages": { "xsd": "pass", "schematron": "pass", "pdfa3": "skipped" }
  }
}

Das XML direkt abrufen

Senden Sie Accept: application/xml, und die Antwort ist das CII-Dokument selbst: kein JSON-Umschlag und kein Base64, das erst durch jq und base64 -d laufen müsste. Accept: application/pdf tut dasselbe für das PDF. Header und Body müssen übereinstimmen: Fordern Sie die eine Darstellung an, während output die andere nennt, führt das zu einem 400 output_conflict. Wir raten bewusst nicht, welches der beiden Formate Sie gemeint haben: Genau auf diesem Weg landen Dateien in der falschen Pipeline.

curl https://api.einvoicekit.com/v1/generate \
  -H "Authorization: Bearer eik_live_..." \
  -H "Content-Type: application/json" \
  -H "Accept: application/xml" \
  -H "Idempotency-Key: F-2026-0042" \
  --data-binary @invoice.json \
  -o invoice.xml

Die PDF-Ausgabe

Fordern Sie output pdf an, erhalten Sie eine hybride Factur-X-Rechnung: ein PDF zum Lesen, mit derselben Rechnung als CII-XML darin, unter dem Namen factur-x.xml, den die Spezifikation vorschreibt. Es ist ein PDF/A-3B, das von der Norm verlangte Archivformat, und jede Datei wird vor der Abrechnung auf Konformität geprüft: Die Stufe pdfa3 in der Antwort trägt dieses Prüfergebnis. Schlüge es fehl, wäre der Aufruf ein 500 auf unserer Seite und würde Sie nichts kosten.

Der Request-Body ist derselbe, den Sie bereits haben. Nur output ändert sich, und das XML im PDF ist Byte für Byte dasselbe, das dieselbe Anfrage mit output xml zurückgibt. Sie müssen sich also nicht für eine der beiden Integrationen entscheiden.

curl https://api.einvoicekit.com/v1/generate \
  -H "Authorization: Bearer eik_live_..." \
  -H "Content-Type: application/json" \
  -H "Accept: application/pdf" \
  -H "Idempotency-Key: F-2026-0042" \
  --data-binary @invoice.json \
  -o invoice.pdf
{
  "format": "facturx",
  "profile": "en16931",
  "xml": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4...",
  "pdf": "JVBERi0xLjcKJYGBgYEKCjEgMCBvYmoKPDwKL1R5cGUgL1BhZ2VzCi...",
  "totals": {
    "lineNet": "7800.00",
    "allowances": "100.00",
    "charges": "117.00",
    "taxBasis": "7817.00",
    "vat": "1563.40",
    "grossTotal": "9380.40",
    "prepaid": "0.00",
    "amountDue": "9380.40"
  },
  "validation": {
    "valid": true,
    "stages": { "xsd": "pass", "schematron": "pass", "pdfa3": "pass" }
  }
}

Text, den die Rechnung nicht zeichnen kann

Ein PDF muss jedes Zeichen mit einer eingebetteten Schrift zeichnen, und keine Schrift deckt ganz Unicode ab. Statt ein leeres Rechteck auf eine Rechnung zu drucken, die Sie gleich versenden, lehnen wir ab: ein 422, dessen issues die Regel unrenderable_text, einen Zeiger auf das genaue Feld und die betreffenden Zeichen tragen. Der Ausweg steht in der Meldung selbst, denn output xml kennt keinerlei Schrift-Einschränkung und nimmt jedes Unicode-Zeichen an. Nur der PDF-Weg wird auf diese Weise geprüft.

{
  "error": "invalid_invoice",
  "message": "this invoice does not satisfy the rules it has to satisfy; see issues.",
  "issues": [
    {
      "rule": "unrenderable_text",
      "field": "/invoice/lines/0/name",
      "message": "the invoice font cannot draw \"😀\". Use output \"xml\", which has no font constraint."
    }
  ]
}

Alle Felder

Die Feldnamen sind einfaches Englisch; daneben steht jeweils der zugehörige EN-16931-Geschäftsterm, damit die Norm auffindbar bleibt, ohne dass sie Ihnen aufgezwungen wird. Diese Tabelle wird aus genau den Regeln generiert, gegen die der Endpunkt validiert; sie kann deshalb nicht von ihnen abweichen.

Immer bedeutet: Ohne dieses Feld wird der Aufruf abgelehnt. Bedingt bedeutet: Eine Geschäftsregel kann das Feld verpflichtend machen. Umsatzsteuerkategorie E erfordert einen Befreiungsgrund, eine Zahlungsgruppe einen Zahlungsmittelcode, eine Überweisung eine IBAN, Kategorie K ein Lieferdatum oder einen Lieferzeitraum sowie ein Lieferland, und jede Rechnung ein Fälligkeitsdatum oder Zahlungsbedingungen. Optional bedeutet: Keine Regel verlangt dieses Feld jemals. Bei einer Tabellenzeile für einen Array-Eintrag (einem Pfad, der auf [] endet) bleibt die Spalte leer: Verpflichtend oder optional ist das Array selbst, eine Zeile darüber.

unit (BT-130) ist auf jeder Position Pflicht und hat keinen Standardwert. BR-23 macht den Einheitencode verpflichtend und fatal; eine Position ohne Einheitencode ergäbe also ein Dokument, das unser eigenes Regelwerk ablehnt. Verwenden Sie Codes nach UN/ECE Recommendation 20 mit der Erweiterung aus Recommendation 21: HUR für eine Stunde, DAY, KGM, MTR oder C62 für eine einfache zählbare Einheit.

Befreiungsgründe geben Sie am vat-Objekt jeder Position, jedes Abschlags und jedes Zuschlags an; fachlich sind sie aber keine Positionsterme: BT-120 und BT-121 gehören zur Umsatzsteueraufschlüsselung des Dokuments (BG-23), und dort fassen wir sie für Sie zusammen. Für die Kategorien AE, K, G und O tragen wir den Standardwortlaut ein, wenn Sie keinen eigenen senden. Für Kategorie E existiert kein Standardwortlaut; dort ist ein Grund deshalb Pflicht, und sein Fehlen führt zu einem 422, der auf die betroffene Position zeigt.

Die Anfrage

Feld BT / BG Pflicht Typ und Grenzen
format optional einer von: facturx
profile optional einer von: en16931, extended
output optional einer von: xml, pdf
language optional einer von: fr, en, de
invoice immer object
totals optional object
logo optional string, max. 2.000.000 Zeichen

Rechnungskopf

Feld BT / BG Pflicht Typ und Grenzen
number BT-1 immer string, max. 100 Zeichen
issueDate BT-2 immer string, max. 10 Zeichen
typeCode BT-3 immer einer von: 380, 381, 384, 389
currency BT-5 immer string, max. 3 Zeichen
dueDate BT-9 bedingt string, max. 10 Zeichen
paymentTerms BT-20 bedingt string, max. 2.000 Zeichen
buyerReference BT-10 optional string, max. 200 Zeichen
purchaseOrderReference BT-13 optional string, max. 200 Zeichen
notes BG-1 optional array
notes[] BG-1 object
notes[]/text BT-22 immer string, max. 1.000 Zeichen
precedingInvoices optional array
precedingInvoices[] object
precedingInvoices[]/number BT-25 immer string, max. 100 Zeichen
precedingInvoices[]/issueDate BT-26 optional string, max. 10 Zeichen
prepaidAmount BT-113 optional decimal, max. 2 Nachkommastellen
roundingAmount BT-114 optional decimal, max. 2 Nachkommastellen

Verkäufer (BG-4)

Feld BT / BG Pflicht Typ und Grenzen
/invoice/seller immer object
name BT-27 immer string, max. 200 Zeichen
vatId BT-31 bedingt string, max. 50 Zeichen
taxRegistrationId BT-32 bedingt string, max. 50 Zeichen
legalRegistrationId BT-30 bedingt string, max. 50 Zeichen
legalInformation BT-33 optional string, max. 1.000 Zeichen
electronicAddress optional object
electronicAddress/value BT-34 optional string, max. 200 Zeichen
electronicAddress/scheme BT-34-1 optional string, max. 10 Zeichen
address immer object
address/line1 BT-35 optional string, max. 200 Zeichen
address/line2 BT-36 optional string, max. 200 Zeichen
address/city BT-37 optional string, max. 100 Zeichen
address/postCode BT-38 optional string, max. 20 Zeichen
address/country BT-40 immer string, max. 2 Zeichen
contact optional object
contact/name BT-41 optional string, max. 200 Zeichen
contact/phone BT-42 optional string, max. 50 Zeichen
contact/email BT-43 optional string, max. 200 Zeichen

Käufer

Feld BT / BG Pflicht Typ und Grenzen
/invoice/buyer immer object
name BT-44 immer string, max. 200 Zeichen
vatId BT-48 bedingt string, max. 50 Zeichen
taxRegistrationId optional string, max. 50 Zeichen
legalRegistrationId BT-47 bedingt string, max. 50 Zeichen
legalInformation optional string, max. 1.000 Zeichen
electronicAddress optional object
electronicAddress/value BT-49 optional string, max. 200 Zeichen
electronicAddress/scheme BT-49-1 optional string, max. 10 Zeichen
address immer object
address/line1 BT-50 optional string, max. 200 Zeichen
address/line2 BT-51 optional string, max. 200 Zeichen
address/city BT-52 optional string, max. 100 Zeichen
address/postCode BT-53 optional string, max. 20 Zeichen
address/country BT-55 immer string, max. 2 Zeichen
contact optional object
contact/name BT-56 optional string, max. 200 Zeichen
contact/phone BT-57 optional string, max. 50 Zeichen
contact/email BT-58 optional string, max. 200 Zeichen

Lieferung

Feld BT / BG Pflicht Typ und Grenzen
/invoice/delivery bedingt object
date BT-72 bedingt string, max. 10 Zeichen
periodStart BT-73 bedingt string, max. 10 Zeichen
periodEnd BT-74 bedingt string, max. 10 Zeichen
country BT-80 bedingt string, max. 2 Zeichen

Zahlungsanweisungen (BG-16)

Feld BT / BG Pflicht Typ und Grenzen
/invoice/payment optional object
meansCode BT-81 bedingt string, max. 10 Zeichen
iban BT-84 bedingt string, max. 34 Zeichen
bic BT-86 optional string, max. 11 Zeichen
accountName BT-85 optional string, max. 200 Zeichen
remittanceInformation BT-83 optional string, max. 200 Zeichen
mandateReference BT-89 optional string, max. 70 Zeichen
creditorId BT-90 optional string, max. 35 Zeichen

Rechnungspositionen (BG-25)

Feld BT / BG Pflicht Typ und Grenzen
/invoice/lines immer array
/invoice/lines[] object
id BT-126 immer string, max. 50 Zeichen
name BT-153 immer string, max. 200 Zeichen
description BT-154 optional string, max. 1.000 Zeichen
quantity BT-129 immer decimal, max. 6 Nachkommastellen
unit BT-130 immer string, max. 10 Zeichen
unitPrice BT-146 immer decimal, max. 6 Nachkommastellen
priceBaseQuantity BT-149 optional decimal, max. 6 Nachkommastellen
vat immer object
vat/category BT-151 immer string, max. 2 Zeichen
vat/rate BT-152 bedingt decimal, max. 2 Nachkommastellen
vat/exemptionReason BT-120 bedingt string, max. 200 Zeichen
vat/exemptionReasonCode BT-121 optional string, max. 30 Zeichen

Abschläge auf Dokumentenebene (BG-20)

Feld BT / BG Pflicht Typ und Grenzen
/invoice/allowances optional array
/invoice/allowances[] object
amount BT-92 bedingt decimal, max. 2 Nachkommastellen
percentage BT-94 bedingt decimal, max. 2 Nachkommastellen
baseAmount BT-93 bedingt decimal, max. 2 Nachkommastellen
reason BT-97 bedingt string, max. 200 Zeichen
reasonCode BT-98 bedingt string, max. 10 Zeichen
vat immer object
vat/category BT-95 immer string, max. 2 Zeichen
vat/rate BT-96 bedingt decimal, max. 2 Nachkommastellen
vat/exemptionReason BT-120 bedingt string, max. 200 Zeichen
vat/exemptionReasonCode BT-121 optional string, max. 30 Zeichen

Zuschläge auf Dokumentenebene (BG-21)

Feld BT / BG Pflicht Typ und Grenzen
/invoice/charges optional array
/invoice/charges[] object
amount BT-99 bedingt decimal, max. 2 Nachkommastellen
percentage BT-101 bedingt decimal, max. 2 Nachkommastellen
baseAmount BT-100 bedingt decimal, max. 2 Nachkommastellen
reason BT-104 bedingt string, max. 200 Zeichen
reasonCode BT-105 bedingt string, max. 10 Zeichen
vat immer object
vat/category BT-102 immer string, max. 2 Zeichen
vat/rate BT-103 bedingt decimal, max. 2 Nachkommastellen
vat/exemptionReason BT-120 bedingt string, max. 200 Zeichen
vat/exemptionReasonCode BT-121 optional string, max. 30 Zeichen

Abschläge auf Positionsebene (BG-27)

Feld BT / BG Pflicht Typ und Grenzen
/invoice/lines[]/allowances optional array
/invoice/lines[]/allowances[] object
amount BT-136 bedingt decimal, max. 2 Nachkommastellen
percentage BT-138 bedingt decimal, max. 2 Nachkommastellen
baseAmount BT-137 bedingt decimal, max. 2 Nachkommastellen
reason BT-139 bedingt string, max. 200 Zeichen
reasonCode BT-140 bedingt string, max. 10 Zeichen

Zuschläge auf Positionsebene (BG-28)

Feld BT / BG Pflicht Typ und Grenzen
/invoice/lines[]/charges optional array
/invoice/lines[]/charges[] object
amount BT-141 bedingt decimal, max. 2 Nachkommastellen
percentage BT-143 bedingt decimal, max. 2 Nachkommastellen
baseAmount BT-142 bedingt decimal, max. 2 Nachkommastellen
reason BT-144 bedingt string, max. 200 Zeichen
reasonCode BT-145 bedingt string, max. 10 Zeichen

Summen, falls Sie sie zur Kontrolle mitsenden

Feld BT / BG Pflicht Typ und Grenzen
lineNet BT-106 optional decimal, max. 2 Nachkommastellen
allowances BT-107 optional decimal, max. 2 Nachkommastellen
charges BT-108 optional decimal, max. 2 Nachkommastellen
taxBasis BT-109 optional decimal, max. 2 Nachkommastellen
vat BT-110 optional decimal, max. 2 Nachkommastellen
grossTotal BT-112 optional decimal, max. 2 Nachkommastellen
prepaid BT-113 optional decimal, max. 2 Nachkommastellen
amountDue BT-115 optional decimal, max. 2 Nachkommastellen

Grenzen

Der Request-Body ist auf 2 MiB begrenzt; darüber antwortet der Endpunkt mit 413. Pro Rechnung sind 500 Positionen zulässig; darüber mit 400. Für jedes String-Feld gilt die in der Tabelle oben angegebene Maximallänge. Pro Rechnung ergeben 500 Positionen neun PDF-Seiten. Ein Logo muss eine PNG- oder JPEG-Data-URI mit höchstens 2000 mal 2000 Pixeln sein; erkannt wird das Format an seinen Magic Bytes, nicht am deklarierten MIME-Typ.

Was es kostet

Abgerechnet wird der Aufruf, nicht das, was dabei herauskommt. Ein PDF anzufordern kostet genau so viel wie das XML anzufordern: Ein Factur-X-PDF ist eine Rechnung, die ihr eigenes XML mitführt, genau das ist der Standard, und nicht zwei Lieferungen. Kostenlose Konten erhalten 20 erzeugte Rechnungen pro Monat als eigenes Kontingent, getrennt von ihren 100 Validierungen: Ein Monat voller Validierungen kann Ihre Generierungen also nie aufbrauchen, und umgekehrt genauso wenig. Pro-Konten haben stattdessen ein gemeinsames Kontingent: 1.000 Credits pro Monat, wobei eine Prüfung einen Credit kostet und eine erzeugte Rechnung zwei, in beliebiger Aufteilung. Das sind 1.000 geprüfte Rechnungen oder 500 erzeugte oder alles dazwischen.

Abgelehnte Aufrufe werden nicht abgerechnet, und alles, was sich als unser Fehler herausstellt, ebenfalls nicht. Der Aufruf zählt erst dann, wenn das Dokument die XSD-Prüfung, das vollständige Regelwerk und bei einem PDF zusätzlich die PDF/A-Prüfung bestanden hat und an Sie ausgeliefert wird. Ein Aufruf, der seine vollen Kosten nicht decken kann, wird vor all dieser Arbeit abgelehnt und lässt Ihr Kontingent unberührt: Bei genau einem verbleibenden Credit geht in Pro eine Prüfung noch durch, eine Generierung endet mit <code>429</code>, statt eine Generierung halb gegen Credits abzurechnen, die sie nicht deckt.

Sicher wiederholen

Senden Sie einen Idempotency-Key-Header mit 1 bis 255 Zeichen; er gilt pro API-Schlüssel und wird 24 Stunden aufbewahrt. Sein Versprechen ist bewusst eng gefasst: Derselbe Schlüssel mit demselben Body wird nie zweimal abgerechnet. Antwort-Bodys werden nicht gespeichert; eine Wiederholung durchläuft daher die gesamte Pipeline erneut, statt eine gespeicherte Antwort auszuliefern. Sie erhalten trotzdem dasselbe Dokument: Das XML wird deterministisch aus Ihrer Anfrage erzeugt, derselbe Body ergibt also immer dieselben Bytes.

Derselbe Schlüssel mit einem anderen Body führt zu einem 409 idempotency_key_reuse, nicht zu einem stillen Austausch: Ein Schlüssel ist ein Versprechen über genau eine Anfrage. Ein 422 gibt den Schlüssel wieder frei, sodass die korrigierte Rechnung unter demselben Schlüssel gesendet werden kann.

Ein Replay ist für Sie kostenlos, verursacht auf unserer Seite aber trotzdem Aufwand und ist deshalb begrenzt: fünf Replays eines bereits abgerechneten Dokuments, danach 429 replay_limit_exceeded. Das deckt die Formen ab, in denen echte Wiederholungen ankommen (eine abgebrochene Verbindung, ein Client-Timeout, eine At-least-once-Queue), und stoppt die Schleife, die sonst aus einem bezahlten Dokument einen Tag kostenloser Rechenzeit machen würde.

Fehler

Immer echte Statuscodes: Ein Fehlschlag versteckt sich nie in einer 200-Antwort. Jede Ablehnung ist JSON mit einem error-Feld in snake_case, auf das Ihr Code verzweigen kann, und einer menschenlesbaren message, auf die er es nicht sollte.

Status error Bedeutung
400 unknown_parameter Dieser Endpunkt akzeptiert keine Query-Parameter. Der betroffene Parameter wird benannt.
400 invalid_json Der Body ist kein gültiges JSON.
400 invalid_request Der Body entspricht nicht der Struktur, die dieser Endpunkt erwartet: ein unbekannter Schlüssel, ein von dieser Version nicht unterstütztes Feld, ein Wert außerhalb einer Aufzählung, zu viele Nachkommastellen, ein zu langer String, mehr als 500 Positionen oder ein Abschlag, der sowohl einen Betrag als auch einen Prozentwert angibt.
400 invalid_idempotency_key Idempotency-Key ist vorhanden, hat aber nicht 1 bis 255 Zeichen.
400 output_conflict Accept und output nennen zwei verschiedene Darstellungen. Fordern Sie die an, die der Body ankündigt, oder lassen Sie den Header weg.
401 invalid_api_key Fehlender oder unbekannter API-Schlüssel.
405 method_not_allowed Es gibt nur POST.
409 idempotency_key_reuse Dieser Idempotency-Key wurde bereits mit einem anderen Body verwendet.
413 payload_too_large Der Request-Body ist größer als 2 MiB.
422 invalid_invoice Die Rechnung konnte geparst werden, verletzt aber eine Regel, die sie erfüllen muss: entweder eine unserer eigenen Prüfungen vor der Pipeline oder eine EN-16931-Regel, die das offizielle Regelwerk gefunden hat. issues benennt die verletzte Regel.
422 totals_mismatch Die von Ihnen gesendeten Summen weichen von den von uns berechneten ab. Beide Werte werden ausgewiesen.
429 quota_exceeded Das Monatskontingent ist erschöpft. Die Antwort enthält ein upgrade-Feld mit der URL, die zu Ihrem Konto passt.
429 replay_limit_exceeded Dieser Idempotency-Key hat sein abgerechnetes Dokument bereits fünfmal erneut ausgeliefert.
500 generation_failed Ein Fehler auf unserer Seite, nicht auf Ihrer: Unser eigenes XML ist an unserem eigenen XSD gescheitert, der Validator war nicht erreichbar, oder eine Rechenregel hat Arithmetik beanstandet, die wir selbst berechnet haben. Es wird nichts abgerechnet, und wir werden alarmiert.
504 generation_timeout Die Validierungsstufe hat nicht innerhalb von 20 Sekunden geantwortet. Es wird nichts abgerechnet, wir werden alarmiert, und ein erneuter Versuch gelingt in der Regel.

issues lesen

Ablehnungen, die Ihre Daten betreffen, enthalten ein issues-Array. rule benennt die verletzte Regel: einen unserer eigenen Codes bei einem Schemaproblem oder die EN-16931-Regel-ID, wenn das offizielle Regelwerk sie gefunden hat. field ist ein JSON-Pointer in den Body, den Sie gesendet haben; message ist der Regeltext. Mitbewerber liefern einen XPath in ein XML zurück, das Sie nie geschrieben haben; wir kontrollieren beide Enden dieser Zuordnung, deshalb erhalten Sie stattdessen den JSON-Pointer. Lässt sich ein Befund keinem einzelnen Feld zuordnen, ist field null, und daneben steht die echte Regel-ID, nie ein von uns erfundener Pointer.

{
  "error": "invalid_invoice",
  "message": "this invoice does not satisfy the rules it has to satisfy; see issues.",
  "issues": [
    {
      "rule": "BR-CL-23",
      "field": "/invoice/lines/0/unit",
      "message": "[BR-CL-23]-Unit code MUST be coded according to the UN/ECE Recommendation 20 with Rec 21 extension"
    }
  ]
}

Codes, die bei einem 400 in rule stehen können: unknown_field, unsupported_field, unsupported_value, too_many_decimals, too_long, invalid_value, too_many_lines, invalid_amount_shape. Bei einem 422: missing_field, missing_exemption_reason, exemption_reason_not_allowed, rate_not_allowed, invalid_vat_rate, invalid_vat_category_mix, missing_reason, missing_seller_identification, missing_buyer_identification, missing_due_date_or_payment_terms, negative_unit_price, invalid_price_base_quantity, invalid_logo, unrenderable_text, conflicting_exemption_reasons sowie die EN-16931-Regel-IDs selbst.

Der erste Aufruf nach einer Ruhephase

Die Validierungs-Engine wird nach zehn Minuten ohne Aufruf schlafen gelegt. Der erste Generate-Aufruf danach kann das gesamte Budget von 20 Sekunden mit Warten verbringen und als 504 generation_timeout zurückkommen. Ein erneuter Versuch gelingt: Im warmen Zustand liegt der Median bei etwa 230 ms für XML und bei etwa 750 ms für ein PDF. Eine Rechnung mit 500 Positionen ist der langsamste Fall: als PDF einige Sekunden. Der fehlgeschlagene Aufruf wird nicht abgerechnet, und wir werden über jeden einzelnen alarmiert. Wir dokumentieren dieses Verhalten lieber hier, als dass Sie es in der Produktion entdecken.

Eine bekannte Warnung

Im Profil extended kommt ein Dokument ohne Lieferdaten mit der Warnung PEPPOL-EN16931-R008 zurück, „Document MUST not contain empty elements“. Das CII-XSD macht das Lieferelement verpflichtend (minOccurs 1), während das Regelwerk keine leeren Elemente zulässt: Schema und Regelwerk widersprechen sich hier, und das Schema gewinnt. Es handelt sich um eine Warnung, das Ergebnis bleibt gültig, und auf Ihrer Seite gibt es nichts zu korrigieren.

Fehler & Kontingente

Jede Nicht-200-Antwort ist JSON mit einem error-Feld. Diese Status existieren:

Status Bedeutung
400 Leerer Body, Multipart ohne file-Feld, ein unbekannter target-Wert oder ein Dokument, das kein Parser erkennt.
401 Fehlender oder unbekannter API-Schlüssel.
405 Es gibt nur POST.
413 Dokument über 5 MB bei /v1/validate. /v1/generate deckelt stattdessen den Anfrage-Body bei 2 MiB.
422 Das Dokument ließ sich parsen, ist aber keine unterstützte E-Rechnungs-Syntax.
429 Monatskontingent erschöpft. Die Antwort enthält ein upgrade-Feld mit der passenden URL: Preise für kostenlose Konten, das Volumenformular für Pro-Konten.
502 Die Validierungs-Engine ist kurz nicht erreichbar. Mit Backoff erneut versuchen.

Kontingente gelten monatlich, pro Konto, über alle Schlüssel. Ein kostenloses Konto hat 100 Validierungen plus getrennt davon 20 erzeugte Rechnungen; ein Pro-Konto hat ein gemeinsames Kontingent von 1.000 Credits für beides, wobei eine Validierung einen Credit kostet und eine erzeugte Rechnung zwei. Das Fenster ist am Abo-Datum verankert (bzw. am Registrierungsdatum bei Free), nicht am Kalendermonat. Gezählt werden nur Anfragen, die wir angenommen und beantwortet haben: ein abgewiesener Aufruf, und alles was sich als unser Fehler herausstellt, wird nie berechnet.

Was kommt

Der Extraktions-Endpunkt, der jede E-Rechnung in normalisiertes JSON überführt, ist noch in Entwicklung. Seine geplante Form steht im API-Bereich der Startseite. Alles Übrige auf dieser Seite ist heute aufrufbar.