DE

API-Referenz für die EU-E-Rechnung

Diese Dokumentation ist nur auf Englisch verfügbar.

Overview

This API turns invoice data you already have into a legally correct EU e-invoice, and checks a document you already have against the EN 16931/XRechnung rules. Two endpoints:

  • POST /invoicing/generate builds an e-invoice from apiRiver's own JSON contract described below. This is not UBL and not CII on input — those are output formats, not input ones.
  • POST /invoicing/validate checks a document you already have — JSON, XML (CII or UBL), or a PDF — and reports whether it satisfies the rule set it declares.

Base address: https://api.apiriver.dev. Every call needs the header described in Authentication below.

Authentication

Every call to either endpoint carries this header:

ApiRiver-Api-Key: <your-api-key>

The key goes in this header only — never in the query string, never in the request body.

To get a key: sign in at /login with the link sent to your email, open your cabinet, and press “Create API key”. The full key is shown exactly once at that moment; only a hash of it is kept afterwards, so there is no way to display it again — issue a new one if you lose it.

A successful call to generate deducts one generation. While your account has an active subscription, it is taken from the subscription's monthly quota — and once that quota is used up, generate is refused with SUBSCRIPTION_QUOTA_EXHAUSTED, with no fallback to the free pool. Without an active subscription (none yet, or it has expired), it is taken from your free pool. See the pricing section on the home page for how many generations each of these holds. validate requires the same header but never deducts a generation, no matter how many times you call it.

No text on this page resembles a real key — every example below uses the literal placeholder <your-api-key>.

Supported formats

What generate produces

Exactly one syntax, always: XRechnung 3.0 in CII syntax. There is no Factur-X/ZUGFeRD PDF/A-3 packaging and no HTML preview in this release — generate never returns anything but XRechnung 3.0 CII.

What validate accepts and judges

A submitted document is judged only if its declared profile is one this service ships a rule set for. The table below is read from the service's own profile catalogue, not from memory:

Profile familySyntaxNotes
XRechnung 1.2 through 3.0CII and UBL every shipped version is recognised, in both syntaxes
EN 16931 (the base rule set)CII and UBL  
Peppol BIS Billing 3.0UBL validated against the general EN 16931 UBL rule set — not against Peppol-specific rules (PEPPOL-EN16931-R*); validatedAgainst names the EN 16931 set actually applied, not Peppol
Factur-X 1.0 / ZUGFeRD 2.0CII only profiles MINIMUM, BASIC WL, BASIC and EN16931
Factur-X/ZUGFeRD EXTENDED— not supported: a document declaring it gets valid: null and a APIRIVER-PROFILE-NOT-IN-ALLOWLIST notice, not a verdict

UBL is accepted only as an input syntax to validate — it never appears as an output of generate.

A submitted application/pdf is judged on its embedded XML only. The PDF/A-3 container itself is not assessed — a document that is not really a conformant PDF/A-3 container can still come back valid: true if the embedded XML passes, and the response carries a permanent APIRIVER-PDFA3-NOT-ASSESSED notice on every PDF input, saying exactly that.

Generating a document

POST /invoicing/generate, Content-Type: application/json:

curl -X POST https://api.apiriver.dev/invoicing/generate \
  -H "ApiRiver-Api-Key: <your-api-key>" \
  -H "Content-Type: application/json" \
  -d @request.json

Request body:

{
  "number": "INV-1",
  "issueDate": "2026-09-03",
  "deliveryDate": "2026-09-01",
  "paymentTerms": "Payable within 14 days of receipt",
  "currency": "EUR",
  "buyerReference": "LEITWEG-123",
  "documentType": "INVOICE",
  "seller": {
    "name": "Seller GmbH",
    "street": "Sellerstr 1",
    "zip": "12345",
    "city": "Berlin",
    "countryCode": "DE",
    "vatId": "DE123456789",
    "iban": "DE89370400440532013000",
    "bic": "COBADEFFXXX",
    "contactName": "Max Mustermann",
    "contactPhone": "+49 30 1234567",
    "contactEmail": "[email protected]"
  },
  "buyer": {
    "name": "Buyer AG",
    "street": "Buyerstr 2",
    "zip": "54321",
    "city": "Munich",
    "countryCode": "DE",
    "vatId": "DE987654321",
    "contactEmail": "[email protected]"
  },
  "items": [
    {
      "name": "Widget",
      "description": "A widget",
      "unitCode": "C62",
      "vatPercent": 19,
      "unitPrice": 10.00,
      "quantity": 2
    }
  ],
  "formats": ["XRECHNUNG_CII"]
}

200 response:

{
  "profile": "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0",
  "documents": [
    {
      "type": "XRECHNUNG_CII",
      "contentType": "application/xml",
      "encoding": "utf-8",
      "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<rsm:CrossIndustryInvoice…"
    }
  ],
  "warnings": []
}

Each entry of documents carries the whole file as a string: contentType and encoding tell you how to interpret it ("utf-8" for the CII XML this endpoint produces today). The content shown above is truncated after the start of the root element for this page only — the real response is not truncated.

warnings lists non-blocking findings (severity warning or notice) about the document that was still produced and returned — it is empty when there is nothing to say. A finding that would make the document non-compliant is never delivered inside a 200: the call fails instead (see Format vs. business-rule errors and HTTP errors below).

Validating a document

POST /invoicing/validate accepts three shapes, selected by Content-Type:

  • application/json — the same JSON contract as generate, but without the generation-only rules: no forced buyerReference, currency is not restricted to EUR, and documentType/formats are not required.
  • application/xml or text/xml — a CII or UBL document; the engine determines which syntax it is.
  • application/pdf — see the PDF note in Supported formats above.

Any other or missing Content-Type gets 415.

curl -X POST https://api.apiriver.dev/invoicing/validate \
  -H "ApiRiver-Api-Key: <your-api-key>" \
  -H "Content-Type: application/json" \
  -d @document.json

200 response for the same JSON document shown above:

{
  "profile": "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0",
  "validatedAgainst": "XRechnung 3.0 (CII)",
  "valid": true,
  "violations": []
}

Response fields:

  • profile is an echo of the document — the profile URN the document itself declares, not a claim by this service that the document satisfies it.
  • validatedAgainst names the rule set that was actually applied, or is null when no rule set applies — this is the service's own statement, and it is what you should read to know what was checked.
  • valid has three states: true, false, or null. null means no verdict was reached at all — the declared profile is outside this service's coverage, which is different from both a passing and a failing document.
  • violations[] entries carry ruleId, errorClass, severity, message, location and part. location is an XPath into the document, and is null for a JSON input — there is no XML tree a path could point into. part cannot be relied on to tell a PDF finding from an XML finding: in this version of the underlying engine it is almost always the same value regardless of where the problem actually is.

Request fields

Columns: field, type, whether it is required, constraints, and the EN 16931 business term (BT) or business group (BG) it corresponds to. A dash in the last column means the mapping code does not name one explicitly for this field.

Top level

FieldTypeRequiredConstraintsEN 16931
numberstringalways at most 64 characters; this service does not check it for uniqueness — sequential, gap-free numbering is the caller's own responsibilityBT-1
issueDatedatealways a plausible calendar yearBT-2
dueDatedateno a plausible calendar year; for generate, not before issueDate, and either dueDate or paymentTerms is required BT-9
deliveryDatedateno a plausible calendar year; never defaulted to the issue date when absentBT-72
paymentTermsstringno at most 300 charactersBT-20
currencystringalways a three-letter upper-case ISO 4217 code; generate additionally accepts only EUR — validate does not flag any other codeBT-5
buyerReferencestringgenerate only at most 64 characters; XRechnung 3.0.2 makes it a fatal rule (BR-DE-15) for a generated documentBT-10
documentTypeenumgenerate only only one value exists today: INVOICEBT-3
sellerobject — see belowalways —BG-4
buyerobject — see belowalways —BG-7
itemsarray — see belowalways 1 to 1000 linesBG-25
formatsset of enumgenerate only only one value exists today: XRECHNUNG_CII —

An unknown JSON field anywhere in the body is rejected outright with 400 UNKNOWN_REQUEST_FIELD, and the response names the fields this contract does accept — see HTTP errors.

seller and buyer — same shape, different rules

Both parties use the same object shape. Structurally, only the address fields below are unconditionally required. generate's own pre-check before mapping additionally requires, for the XRechnung mandatory minimum: the seller's contactName, contactPhone and contactEmail; the seller's iban (bic stays optional); the seller's vatId, or else both taxId and legalRegistrationId; and the buyer's contactEmail. A vatId given on either party must start with a country prefix the EN 16931 rules recognise. None of this is required by request validation itself, and none of it applies on validate at all; a request that misses it gets PROFILE_PRECHECK_FAILED with the missing fields named in fieldErrors.

FieldTypeRequiredConstraintsEN 16931
namestringalways at most 200 characters—
streetstringalways at most 200 characters—
zipstringalways at most 20 characters—
citystringalways at most 200 characters—
countryCodestringalways ISO 3166-1 alpha-2—
vatIdstringno at most 30 characters; a two-letter country prefix and a national numberBT-31 (seller) / BT-48 (buyer)
taxIdstringno at most 30 characters; only meaningful for the seller BT-32 (seller Steuernummer)
legalRegistrationIdstringno at most 50 characters BT-30 (seller) / BT-47 (buyer)
ibanstringno at most 34 characters, IBAN form; used only on the seller —
bicstringno at most 11 characters, BIC form; used only on the seller —
contactNamestringno at most 200 characters; the seller's is part of the contact group XRechnung 3.0.2 requires (BR-DE-2) for a generated documentBT-41 (seller)
contactPhonestringno at most 30 charactersBT-42 (seller)
contactEmailstringno a valid email address, at most 200 characters; also serves as the electronic address business term BT-43 / BT-34 (seller) / BT-49 (buyer)

items — one entry per invoice line

FieldTypeRequiredConstraintsEN 16931
namestringalways at most 300 charactersBT-153
descriptionstringno at most 2000 charactersBT-154
unitCodestringalways a UN/CEFACT Rec 20/21 code, 1 to 3 letters or digits; generate additionally restricts it to an allow-list of known codesBT-130
vatPercentnumberalways 0 to 100, at most 2 decimal digits — this is a percentage, not a fraction: 19 means 19%, and 0.19 means 0.19%, not 19%BT-152
unitPricenumberalways at most 4 decimal digits; strictly positive for generate, zero allowed for validateBT-146
quantitynumberalways at most 4 decimal digits; strictly positive for generate, zero allowed for validateBT-129

Format vs. business-rule errors

Every finding this service reports about a document carries an errorClass naming which of these three it is:

errorClassGerman termMeaning
FORMATFormatfehler the document could not be parsed, or does not conform to the expected schema or syntax. By the German tax authority's letter of 15 October 2025, this means the e-invoice does not exist for the purposes of §14 UStG.
BUSINESS_RULEGeschäftsregelfehler the document was read successfully, but it breaks an EN 16931 or XRechnung business rule (a Schematron BR-* or BR-DE-* rule), or fails this service's own independent arithmetic re-check of the document's totals.
NOT_ASSESSED— neither of the above: this service declined to judge — the declared profile is outside its coverage, or a specific aspect of the document (such as the PDF/A-3 container) was not examined.

Always read errorClass itself — never guess it from the prefix of ruleId. Rule codes this service invents itself all carry an APIRIVER- prefix, but that prefix does not by itself mean FORMAT: for example APIRIVER-ARITHMETIC-ISSUE (the document's amounts do not add up) is a BUSINESS_RULE finding, not a format one.

The same defect, reported two different ways — the sample document above with its buyerReference removed:

On generate, the call is refused before any document is produced:

{
  "requestId": "<request-id>",
  "code": "VALIDATION_ERROR",
  "message": "invalid fields: buyerReference",
  "violations": null,
  "fieldErrors": [
    {
      "field": "buyerReference",
      "reason": "required for generation: XRechnung 3.0.2 makes BT-10 fatal (BR-DE-15)"
    }
  ]
}

The same document sent to validate is read and reported on instead of refused — a 200, with the defect named as a business-rule finding:

{
  "profile": "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0",
  "validatedAgainst": "XRechnung 3.0 (CII)",
  "valid": false,
  "violations": [
    {
      "ruleId": "BR-DE-15",
      "errorClass": "BUSINESS_RULE",
      "severity": "error",
      "message": "[BR-DE-15] Das Element \"Buyer reference\" (BT-10) muss übermittelt werden.",
      "location": null,
      "part": "fx"
    }
  ]
}

And an example of the other class — a document this service cannot even parse, sent to validate:

<this is not well-formed xml

with Content-Type: application/xml, gets a 200 reporting a format defect, not an engine failure — along with a second finding of its own, since a document this small and this broken cannot even be confirmed as the syntax it would have needed to be:

{
  "profile": null,
  "validatedAgainst": null,
  "valid": false,
  "violations": [
    {
      "ruleId": "APIRIVER-FILE-TOO-SMALL",
      "errorClass": "FORMAT",
      "severity": "fatal",
      "message": "the submitted file is too small to be an e-invoice",
      "location": null,
      "part": "pdf"
    },
    {
      "ruleId": "APIRIVER-DECLARED-SYNTAX-NOT-CONFIRMED",
      "errorClass": "NOT_ASSESSED",
      "severity": "notice",
      "message": "this service could not confirm that the document is actually an instance of the syntax its declared profile identifies; any verdict and findings above reflect whatever this service was able to make of the document as submitted, not a judgement against its declared profile",
      "location": null,
      "part": null
    }
  ]
}

HTTP errors

Every 4xx/5xx from either endpoint uses the same envelope:

  • requestId — also returned as the X-Request-Id response header, on every response, including successful ones;
  • code — a stable machine-readable string, safe to branch on;
  • message — a human-readable explanation;
  • violations — present only for 422 PROFILE_NOT_ACHIEVABLE;
  • fieldErrors — present only for the two request-form errors, 400 VALIDATION_ERROR and 400 PROFILE_PRECHECK_FAILED.

Nothing from your request is ever echoed back inside an error body — not the value you sent, not the raw text of a library's own error message. Only the name of the offending field and a static, our-own reason.

This service enforces limits on request size, request rate, and how long it will spend processing a single document. None of those limits are printed on this page, because they can change without notice; when a request is rejected for exceeding the size limit, that response itself states the exact limit that applied.

A rate-limited or a temporarily-overloaded response may carry a Retry-After header, and it should be honoured before retrying.

StatuscodeWhen
400VALIDATION_ERROR the request body fails bean validation; carries fieldErrors (up to 50 entries)
400UNSUPPORTED_FORMAT an unknown value for formats; message lists the values this contract accepts
400UNSUPPORTED_DOCUMENT_TYPE an unknown value for documentType; same idea
400UNSUPPORTED_ENUM_VALUE an unknown value for any other enum field
400UNKNOWN_REQUEST_FIELD the JSON body carries a field this contract does not have; message lists the accepted fields
400CONTENT_TYPE_MISMATCH the body is empty, or does not match the declared Content-Type
400MALFORMED_REQUEST_BODY the body does not parse as JSON at all
400PROFILE_PRECHECK_FAILED generate only — the request does not carry the XRechnung mandatory minimum; carries fieldErrors
400INVISIBLE_TEXT_REJECTED a text field is non-blank but contains only invisible characters
400REQUEST_STRUCTURE_TOO_LARGE the JSON structure exceeds this service's structural limits although the byte size is within budget
400REQUEST_STRUCTURE_TOO_DEEP the JSON nests more deeply than this service will read
400XML_STRUCTURE_TOO_COMPLEX a submitted XML document's structure exceeds this service's parsing budget; the document was not examined
400PDF_DECOMPRESSION_TOO_LARGE a submitted PDF's streams decompress to more than this service will expand for a body of that size; not examined
400PDF_STRUCTURE_TOO_LARGE a submitted PDF's cross-reference structure decompresses to more than this service will expand; not examined
400PDF_STRUCTURE_TOO_COMPLEX a submitted PDF declares more structural elements than this service will examine in one document; not examined
400PDF_STRUCTURE_TOO_DEEP a tree inside a submitted PDF nests more deeply than this service will examine; not examined
400PDF_FILTER_CHAIN_NOT_SUPPORTED a submitted PDF applies a filter chain this service does not decode; not examined
400PDF_XML_STREAM_IS_AN_IMAGE_CODEC a submitted PDF's supposedly-XML stream is actually encoded with a raster image codec; not examined
400MALFORMED_REQUEST a platform default for a rejection Spring itself decides, not covered by a more specific code above
4xx (other)BAD_REQUEST the same kind of platform default, for a status other than 400
401API_KEY_REQUIRED no ApiRiver-Api-Key header, or the key was not found or has been revoked — all three give the identical response
402FREE_QUOTA_EXHAUSTED generate only — the free generation pool is used up
402SUBSCRIPTION_QUOTA_EXHAUSTED generate only — the subscription's monthly quota is used up; there is no fallback to the free pool
404NOT_FOUNDno such route
405METHOD_NOT_ALLOWED the route exists but not for this HTTP method
406NOT_ACCEPTABLE the request's Accept header rules out application/json, the only media type these endpoints produce
415UNSUPPORTED_CONTENT_TYPE a Content-Type neither endpoint accepts — this includes any non-JSON body sent to generate
422PROFILE_NOT_ACHIEVABLE generate only — the submitted data cannot form a document that passes the XRechnung self-check; carries violations
429TOO_MANY_REQUESTS document-processing requests are arriving faster than this service accepts them; carries Retry-After
500GENERATOR_SELF_VALIDATION_FAILED the document this service built did not pass its own self-check
500GENERATOR_FAILED internal error while building the document
500INTERNAL_ERROR an unexpected internal error, or a platform default for something not covered by a more specific code above
502ENGINE_ERROR the validation engine could not process the document
502VALIDATOR_FAILED same family as the row above, different internal cause
503SERVER_BUSY no spare processing or body-buffering capacity right now; carries Retry-After
503PROCESSING_TIMEOUT processing the submitted document exceeded the time budget for a single request; carries Retry-After
503GATING_UNAVAILABLE the api key/quota check is temporarily unavailable
408REQUEST_TIMEOUT the request body was not fully received in time
413PAYLOAD_TOO_LARGE the request body exceeds the maximum allowed size; the response names the limit, in bytes

Example — calling validate without a valid key (a missing, unknown or revoked key all get this same response):

{
  "requestId": "<request-id>",
  "code": "API_KEY_REQUIRED",
  "message": "a valid ApiRiver-Api-Key header is required for this endpoint",
  "violations": null,
  "fieldErrors": null
}