Validate an invoice against EN 16931 business rules
Counts as one document against your monthly allowance. A response with valid: false is a successful call — it still costs a document. Findings come back in three arrays by severity: errors (fatal, the only ones that set valid: false), warnings, and information (advisory).
TWO KINDS OF BODY, chosen by content-type.
application/json — our InvoiceInput model. Unchanged. It is also what a body with no content-type is read as.
application/xml or text/xml for an XML file, application/pdf for a Factur-X or ZUGFeRD PDF, application/octet-stream for either — an invoice document you already have, sent as the raw bytes. We read it into the same invoice model and run the same rules, and the response adds syntax (which reader ran), container (the PDF attachment the XML came from, or null), customizationId (BT-24), profileId (BT-23), unmapped (everything in the file that did not reach the model) and source. Every rule finding carries the location of its element in your file. A Factur-X or ZUGFeRD file that declares MINIMUM or BASIC WL also gets AW-PROFILE-SUBSET first: a finding about the whole file, with no location, that says why the rules fail it and what to export instead.
The reader takes both EN 16931 syntaxes and both document types: a UBL 2.1 Invoice, a UBL 2.1 CreditNote, and a UN/CEFACT CII CrossIndustryInvoice, which is one document for invoices and credit notes alike — in the encoding the file declares (a byte-order mark, or encoding= in the XML declaration: UTF-16 and ISO-8859-1 files read as what they are; a file converted to UTF-8 whose declaration still names a single-byte encoding is refused by name, not read as mojibake). Send the file as it is — there is no parameter for the syntax and none for the document type, because the file decides both and the response reports what was read.
A Factur-X or ZUGFeRD PDF is opened: the rules judge its XML, and its attachment and metadata are checked. The CII invoice attached to it (factur-x.xml, zugferd-invoice.xml, xrechnung.xml) is extracted and validated; container names it. Of the PDF itself, the attachment (its name, /AFRelationship and MIME type) and the XMP metadata are checked, and each problem is an AW-PDF-* finding, none of them fatal; PDF/A conformance itself, and whether the page agrees with the XML, are not checked. source says so on every such response. The AW-PDF-* findings are warnings and information, with a message and a fix but no docsUrl, xpath or location: a PDF key has no line. It is on every plan and costs one document, like any other file; WRITING a PDF is /v1/generate?format=pdf, watermarked on the free plan. A PDF with no invoice attached, a ubl:DebitNote, or anything that is not an invoice document is refused with 415, and the message names what it is.
A document sent without the content-type that says so — no content-type, application/json, or curl's default form type — is read as JSON, fails, and is answered 415 unsupported_media_type, naming the file and the content-type to send it with. A form upload (curl -F) is refused the same way: send the file itself as the body.
Reading a file is a pre-flight, not an authority. We validate the model we read, not the XML a receiver judges, so rules that constrain the document itself (BR-DE-13, BR-DE-21 on BT-24) do not run and a document that passes here can still be rejected by KoSIT or by a receiving platform.
METERING: a document costs one document once the file has been read and judged — valid: true and valid: false cost the same, exactly as on the JSON path, and a PDF is one document, not two. A file that cannot be read at all (400, 413, 415) never reaches the rules and costs nothing; those responses carry the unchanged X-RateLimit-* set so you can see the allowance did not move.
Request
- Base URL: https://api.attestwire.com
- URL: https://api.attestwire.com/v1/validate
- Auth: HTTP bearer
Query parameters
Raise the XML reader's character cap for this one request, up to 16,000,000. The default is the engine's 8,000,000 and is unchanged for any request that does not ask.
THE CASE THIS EXISTS FOR is a document carrying a base64-embedded attachment (BG-24, EmbeddedDocumentBinaryObject): base64 spends four characters per three bytes, so a 300 kB PDF inside an otherwise ordinary invoice clears the default on the attachment alone.
Asking for it also raises the body ceiling of an application/xml or text/xml request from 1 MB to 32 MB — a raise the bytes cannot get through is not a raise. The two ceilings are independent and whichever you meet first refuses the request. For a PDF it raises the cap on the XML attached to it, as far as the 4 MB the reader takes out of one PDF, and leaves the PDF's own 10 MB ceiling where it is.
The depth, element and attribute caps do NOT move, and that is what makes this safe: the element cap bounds the parsed tree whatever the character count. A value above the ceiling, or one that is not a positive whole number, is a 400 — the number you ask for is never quietly clamped. Ignored on an application/json request, which has no document reader to configure.
Store a validation record for this call and return a link to it. Off unless you ask; true or 1 turns it on, false or 0 is the default, and any other value is a 400 rather than a silently ignored request.
A record is a shareable, permanent-for-a-year page at /r/{id} (and the same object as JSON at /v1/records/{id}) that states what was checked, by which engine and ruleset, when, and what the findings were. It is what you send to a buyer, an auditor or a platform onboarding desk instead of pasting a JSON body into an email.
IT CONTAINS NO DOCUMENT. The invoice is fingerprinted with SHA-256 and the fingerprint is stored; the file itself is never written anywhere. Nor is your email, your key, or any invoice content beyond what a finding already quotes. That is what makes the link safe to hand to somebody who is not you.
WHAT IT IS FINGERPRINTING depends on how you sent the invoice. An application/xml request fingerprints the document as UTF-8 bytes, so shasum -a 256 invoice.xml reproduces it. An application/json request fingerprints the invoice model in a canonical form (keys sorted, no whitespace) — hashing your raw body would make the fingerprint a fact about your HTTP client's field order rather than about your invoice. The record says which of the two it did.
COSTS NOTHING EXTRA. The call is metered exactly as it would be without this parameter, on every plan including free. A separate ceiling of 500 stored records per key per UTC day bounds storage abuse; hitting it returns the validation as normal with a record_unavailable block instead of record, and nothing extra is charged.
THE ID IS THE CREDENTIAL. It is 128 random bits, and GET /v1/records/{id} and GET /r/{id} are public: anyone holding the link reads the record, anyone who does not cannot guess it. Records expire 365 days after creation and are not recoverable.
Request body
Example request
{
"currency": "EUR",
"invoiceTypeCode": "380",
"seller": {
"vatId": "DE123456789",
"address": {
"countryCode": "DE"
},
"electronicAddress": {
"schemeId": "9930"
}
},
"buyer": {
"vatId": "DE123456789",
"address": {
"countryCode": "DE"
},
"electronicAddress": {
"schemeId": "9930"
}
},
"taxRepresentative": {
"address": {
"countryCode": "DE"
}
},
"lines": [
{
"unitCode": "C62",
"itemClassifications": [
{
"schemeId": "TST"
}
]
}
],
"payment": {
"meansCode": "58"
},
"declaredTotals": {
"defects": [
{
"field": "BT-115"
}
],
"overPrecise": [
{
"field": "BT-131"
}
]
}
}Response
Validation ran. Check valid. Every example below is a 200 — an invalid invoice is a successful call.
Example response
{
"errors": [
{
"rule": "BR-DE-15",
"field": "BT-10",
"location": {
"attachment": "factur-x.xml"
}
}
],
"warnings": [
{
"rule": "BR-DE-15",
"field": "BT-10",
"location": {
"attachment": "factur-x.xml"
}
}
],
"information": [
{
"rule": "BR-DE-15",
"field": "BT-10",
"location": {
"attachment": "factur-x.xml"
}
}
],
"findings_truncated": {
"total": 20012,
"returned": 500,
"cap": 500
},
"container": "factur-x.xml",
"customizationId": "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0",
"profileId": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0",
"unmapped": [
{
"path": "/ubl:Invoice/cac:AccountingSupplierParty/cac:Party/cbc:WebsiteURI"
}
],
"provenance": {
"engine": "@attestwire/en16931",
"engine_version": "0.14.1",
"ruleset": "en16931@0.14.1",
"profile": "xrechnung-ubl",
"kosit_conformance": {
"recorded": "2026-09-24",
"validator": "1.6.3",
"configuration": "3.0.2",
"configuration_published": "2026-08-31"
}
}
}Changes
No recorded changes to this operation across all 1 revision of this API.