Invoices

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.

post/v1/validate

Request

  • Base URL: https://api.attestwire.com
  • URL: https://api.attestwire.com/v1/validate
  • Auth: HTTP bearer

Query parameters

max_charactersinteger

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.

recordboolean

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

profile'en16931' | 'xrechnung-ubl' | 'xrechnung-cii' | 'facturx-en16931' | 'peppol-bis-3' | 'auto' required

Selects the rule set, and on POST /v1/generate the syntax. Narrower profiles add rules; they never remove core ones. "auto" (POST /v1/validate and /v1/generate) has the API choose from the buyer, without any network call: a Leitweg-ID (electronic address scheme 0204, or a German buyer reference) is xrechnung-ubl; a Belgian buyer or a Peppol electronic address is peppol-bis-3; a French buyer is facturx-en16931; a German or Austrian business is xrechnung-cii, or facturx-en16931 with ?format=pdf; anything else is en16931 (facturx-en16931 with ?format=pdf). The answer names the choice and the rule in profile_choice and the Attestwire-Profile headers.

invoiceNumberstring required

BT-1 invoice number.

issueDatestring date required

BT-2 issue date, ISO 8601.

currencystring required

BT-5 invoice currency code, ISO 4217.

invoiceTypeCodestring

BT-3 document type code, UNTDID 1001: "380", the default, is a commercial invoice and "381" a credit note. It decides the document type: "381" makes a UBL CreditNote or a CII document with type code 381, from the same fields. State a credit note's amounts positively; the type code carries the direction. BR-DE-17 restricts XRechnung to 326/380/381/384/389/875/876/877.

dueDatestring date

BT-9 payment due date, ISO 8601. A UBL credit note carries it inside the payment means (cac:PaymentMeans/cbc:PaymentDueDate), so there it needs payment; without that group it is dropped and reported as ATW-CREDIT-NOTE-DUE-DATE-UNBOUND.

notestring

BT-22 invoice note, free text.

noteSubjectCodestring

BT-21 invoice note subject code, UNCL 4451. Written into the note itself as #CODE#text, so it only means something when note is set.

buyerReferencestring

BT-10 buyer reference, which XRechnung requires (BR-DE-15). A German public-sector buyer's is its Leitweg-ID.

orderReferencestring

BT-13 purchase order reference.

salesOrderReferencestring

BT-14 sales order reference: your own order number, not the buyer's.

projectReferencestring

BT-11 project reference. UBL-CreditNote-2.1.xsd has no element for it, so on a UBL credit note it is dropped and reported as ATW-CREDIT-NOTE-PROJECT-REFERENCE-UNBOUND. CII keeps it.

contractReferencestring

BT-12 contract reference.

despatchAdviceReferencestring

BT-16 despatch advice reference.

receivingAdviceReferencestring

BT-15 receiving advice reference.

tenderOrLotReferencestring

BT-17 tender or lot reference.

buyerAccountingReferencestring

BT-19 buyer accounting reference: the buyer's cost centre or ledger account.

taxPointDatestring date

BT-7 value added tax point date, ISO 8601. BR-CO-03 does not allow it together with invoicingPeriod.descriptionCode (BT-8).

vatScenario'domestic' | 'intra-eu-goods' | 'intra-eu-services' | 'export' | 'small-business-exemption'

What happened, in business terms, for every line, document level allowance and charge that does not say otherwise: the engine writes the VAT category (BT-151, BT-95, BT-102), the rate and the exemption reason (BT-120, BT-121) from it. "domestic": a taxed sale inside the seller's own country — category S at the rate you state in vatRate, which is never guessed. "intra-eu-goods": goods shipped to a VAT-registered business in another EU member state — K, rate 0, VATEX-EU-IC; the invoice also needs both parties' VAT identifiers, deliverTo.countryCode and a delivery date or invoicing period. "intra-eu-services": a service to a business in another EU member state, which accounts for the VAT itself (reverse charge) — AE, rate 0, VATEX-EU-AE; needs both VAT identifiers. "export": goods leaving the EU — G, rate 0, VATEX-EU-G; needs the seller's VAT identifier. "small-business-exemption": the seller does not charge VAT under its small-business scheme, Germany's § 19 UStG or France's franchise en base — E, rate 0, with the wording each country prescribes; a seller elsewhere is refused with ATW-VAT-SCENARIO-UNSUPPORTED. The exemption text is written in German for a seller in DE or AT, in French for FR, in English otherwise. It is not a tax determination: you decide which scenario applies, and the engine only writes its codes. An item's own vatScenario or vatCategory overrides this default. The document is byte for byte the one the explicit codes produce.

paymentTermsstring

BT-20 payment terms, free text.

deliveryDatestring date

BT-72 actual delivery date, ISO 8601. Required with BT-80 by BR-IC-11 for category K. On XRechnung profiles, an invoice with no time of supply at all (no BT-72, no invoicingPeriod, no line period) raises BR-DE-TMP-32 at severity information.

deliverToNamestring

BT-70 deliver-to party name.

vatAccountingCurrencystring

BT-6 VAT accounting currency, ISO 4217, when VAT is reported in a currency other than BT-5. Supplying it obliges you to supply taxAmountInAccountingCurrency (BR-53).

taxAmountInAccountingCurrencynumber

BT-111 invoice total VAT amount in the VAT accounting currency. It depends on the exchange rate the tax authority requires for the tax point, which is not on the invoice, so it cannot be derived: you supply it.

paidAmountnumber

BT-113 paid amount: a prepayment or deposit already received. It is subtracted from the amount due (BT-115).

roundingAmountnumber

BT-114 rounding amount, added to the total to produce a payable round figure.

vatExemptionReasonsobject

BT-120 VAT exemption reason text, keyed by VAT category, such as {"E": "…"}. Defaults are applied for AE, K, G and O; category E has no standard text, so BR-E-10 requires you to supply one.

vatExemptionReasonCodesobject

BT-121 VAT exemption reason code from the CEF VATEX list, keyed by VAT category.

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.

validboolean required

True when there are no fatal errors. Neither warnings nor information findings affect it.

profile'en16931' | 'xrechnung-ubl' | 'xrechnung-cii' | 'facturx-en16931' | 'peppol-bis-3' required
syntax'ubl' | 'cii'

Which reader ran: ubl for a UBL 2.1 Invoice, cii for a UN/CEFACT CrossIndustryInvoice. Decided from the root element of the document you sent, never from a parameter — there is no way to ask for the wrong one. Document requests only.

containerstring nullable

When a Factur-X or ZUGFeRD PDF was sent: the name of the attachment the XML was read from, and judged. What the PDF gets wrong about that attachment and its XMP metadata is an AW-PDF-* finding; PDF/A conformance itself, and whether the page agrees with the XML, are not checked. null for an XML file. Document requests only.

customizationIdstring nullable

BT-24, exactly as the document states it — not as we would have written it. profile is derived from this. null when the document states none, in which case the profile was guessed and the guess is reported in unmapped. Document requests only.

profileIdstring nullable

BT-23, exactly as the document states it. null when the document states none. Document requests only.

sourcestring

Plain-English statement of what was done and what it does not prove: the document was read into the invoice model and the model was validated, which is a pre-flight and not a schematron — and, for a PDF, what of the PDF was checked (its attachment and XMP metadata) and what was not (PDF/A conformance, the page). Document requests only.

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.