Référence de l'API de facturation électronique de l'UE
Cette documentation n'est disponible qu'en anglais.
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/generatebuilds 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/validatechecks 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 family | Syntax | Notes |
|---|---|---|
| XRechnung 1.2 through 3.0 | CII and UBL | every shipped version is recognised, in both syntaxes |
| EN 16931 (the base rule set) | CII and UBL | |
| Peppol BIS Billing 3.0 | UBL | 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.0 | CII 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 asgenerate, but without the generation-only rules: no forcedbuyerReference, currency is not restricted to EUR, anddocumentType/formatsare not required.application/xmlortext/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:
profileis an echo of the document — the profile URN the document itself declares, not a claim by this service that the document satisfies it.validatedAgainstnames the rule set that was actually applied, or isnullwhen no rule set applies — this is the service's own statement, and it is what you should read to know what was checked.validhas three states:true,false, ornull.nullmeans 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 carryruleId,errorClass,severity,message,locationandpart.locationis an XPath into the document, and isnullfor a JSON input — there is no XML tree a path could point into.partcannot 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
| Field | Type | Required | Constraints | EN 16931 |
|---|---|---|---|---|
number | string | always | at most 64 characters; this service does not check it for uniqueness — sequential, gap-free numbering is the caller's own responsibility | BT-1 |
issueDate | date | always | a plausible calendar year | BT-2 |
dueDate | date | no | a plausible calendar year; for generate, not before
issueDate, and either dueDate or
paymentTerms is required |
BT-9 |
deliveryDate | date | no | a plausible calendar year; never defaulted to the issue date when absent | BT-72 |
paymentTerms | string | no | at most 300 characters | BT-20 |
currency | string | always | a three-letter upper-case ISO 4217 code; generate
additionally accepts only EUR — validate
does not flag any other code | BT-5 |
buyerReference | string | generate only | at most 64 characters; XRechnung 3.0.2 makes it a fatal rule
(BR-DE-15) for a generated document | BT-10 |
documentType | enum | generate only | only one value exists today: INVOICE | BT-3 |
seller | object — see below | always | — | BG-4 |
buyer | object — see below | always | — | BG-7 |
items | array — see below | always | 1 to 1000 lines | BG-25 |
formats | set of enum | generate 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.
| Field | Type | Required | Constraints | EN 16931 |
|---|---|---|---|---|
name | string | always | at most 200 characters | — |
street | string | always | at most 200 characters | — |
zip | string | always | at most 20 characters | — |
city | string | always | at most 200 characters | — |
countryCode | string | always | ISO 3166-1 alpha-2 | — |
vatId | string | no | at most 30 characters; a two-letter country prefix and a national number | BT-31 (seller) / BT-48 (buyer) |
taxId | string | no | at most 30 characters; only meaningful for the seller | BT-32 (seller Steuernummer) |
legalRegistrationId | string | no | at most 50 characters | BT-30 (seller) / BT-47 (buyer) |
iban | string | no | at most 34 characters, IBAN form; used only on the seller | — |
bic | string | no | at most 11 characters, BIC form; used only on the seller | — |
contactName | string | no | at most 200 characters; the seller's is part of the contact group
XRechnung 3.0.2 requires (BR-DE-2) for a generated
document | BT-41 (seller) |
contactPhone | string | no | at most 30 characters | BT-42 (seller) |
contactEmail | string | no | 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
| Field | Type | Required | Constraints | EN 16931 |
|---|---|---|---|---|
name | string | always | at most 300 characters | BT-153 |
description | string | no | at most 2000 characters | BT-154 |
unitCode | string | always | a UN/CEFACT Rec 20/21 code, 1 to 3 letters or digits;
generate additionally restricts it to an allow-list of
known codes | BT-130 |
vatPercent | number | always | 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 |
unitPrice | number | always | at most 4 decimal digits; strictly positive for generate,
zero allowed for validate | BT-146 |
quantity | number | always | at most 4 decimal digits; strictly positive for generate,
zero allowed for validate | BT-129 |
Format vs. business-rule errors
Every finding this service reports about a document carries an
errorClass naming which of these three it is:
errorClass | German term | Meaning |
|---|---|---|
FORMAT | Formatfehler | 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_RULE | Geschä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 theX-Request-Idresponse 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 for422 PROFILE_NOT_ACHIEVABLE;fieldErrors— present only for the two request-form errors,400 VALIDATION_ERRORand400 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.
| Status | code | When |
|---|---|---|
| 400 | VALIDATION_ERROR |
the request body fails bean validation; carries fieldErrors
(up to 50 entries) |
| 400 | UNSUPPORTED_FORMAT |
an unknown value for formats; message lists the
values this contract accepts |
| 400 | UNSUPPORTED_DOCUMENT_TYPE |
an unknown value for documentType; same idea |
| 400 | UNSUPPORTED_ENUM_VALUE |
an unknown value for any other enum field |
| 400 | UNKNOWN_REQUEST_FIELD |
the JSON body carries a field this contract does not have;
message lists the accepted fields |
| 400 | CONTENT_TYPE_MISMATCH |
the body is empty, or does not match the declared
Content-Type |
| 400 | MALFORMED_REQUEST_BODY |
the body does not parse as JSON at all |
| 400 | PROFILE_PRECHECK_FAILED |
generate only — the request does not carry the
XRechnung mandatory minimum; carries fieldErrors |
| 400 | INVISIBLE_TEXT_REJECTED |
a text field is non-blank but contains only invisible characters |
| 400 | REQUEST_STRUCTURE_TOO_LARGE |
the JSON structure exceeds this service's structural limits although the byte size is within budget |
| 400 | REQUEST_STRUCTURE_TOO_DEEP |
the JSON nests more deeply than this service will read |
| 400 | XML_STRUCTURE_TOO_COMPLEX |
a submitted XML document's structure exceeds this service's parsing budget; the document was not examined |
| 400 | PDF_DECOMPRESSION_TOO_LARGE |
a submitted PDF's streams decompress to more than this service will expand for a body of that size; not examined |
| 400 | PDF_STRUCTURE_TOO_LARGE |
a submitted PDF's cross-reference structure decompresses to more than this service will expand; not examined |
| 400 | PDF_STRUCTURE_TOO_COMPLEX |
a submitted PDF declares more structural elements than this service will examine in one document; not examined |
| 400 | PDF_STRUCTURE_TOO_DEEP |
a tree inside a submitted PDF nests more deeply than this service will examine; not examined |
| 400 | PDF_FILTER_CHAIN_NOT_SUPPORTED |
a submitted PDF applies a filter chain this service does not decode; not examined |
| 400 | PDF_XML_STREAM_IS_AN_IMAGE_CODEC |
a submitted PDF's supposedly-XML stream is actually encoded with a raster image codec; not examined |
| 400 | MALFORMED_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 |
| 401 | API_KEY_REQUIRED |
no ApiRiver-Api-Key header, or the key was not found or has
been revoked — all three give the identical response |
| 402 | FREE_QUOTA_EXHAUSTED |
generate only — the free generation pool is used
up |
| 402 | SUBSCRIPTION_QUOTA_EXHAUSTED |
generate only — the subscription's monthly quota is
used up; there is no fallback to the free pool |
| 404 | NOT_FOUND | no such route |
| 405 | METHOD_NOT_ALLOWED |
the route exists but not for this HTTP method |
| 406 | NOT_ACCEPTABLE |
the request's Accept header rules out
application/json, the only media type these endpoints
produce |
| 415 | UNSUPPORTED_CONTENT_TYPE |
a Content-Type neither endpoint accepts — this
includes any non-JSON body sent to generate |
| 422 | PROFILE_NOT_ACHIEVABLE |
generate only — the submitted data cannot form a
document that passes the XRechnung self-check; carries
violations |
| 429 | TOO_MANY_REQUESTS |
document-processing requests are arriving faster than this service
accepts them; carries Retry-After |
| 500 | GENERATOR_SELF_VALIDATION_FAILED |
the document this service built did not pass its own self-check |
| 500 | GENERATOR_FAILED |
internal error while building the document |
| 500 | INTERNAL_ERROR |
an unexpected internal error, or a platform default for something not covered by a more specific code above |
| 502 | ENGINE_ERROR |
the validation engine could not process the document |
| 502 | VALIDATOR_FAILED |
same family as the row above, different internal cause |
| 503 | SERVER_BUSY |
no spare processing or body-buffering capacity right now; carries
Retry-After |
| 503 | PROCESSING_TIMEOUT |
processing the submitted document exceeded the time budget for a single
request; carries Retry-After |
| 503 | GATING_UNAVAILABLE |
the api key/quota check is temporarily unavailable |
| 408 | REQUEST_TIMEOUT |
the request body was not fully received in time |
| 413 | PAYLOAD_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
}