Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Attestwire Records API
version: 0.1.0
summary: EN 16931 e-invoice validation and generation.
description: 'Validate an invoice against EN 16931 and its national CIUS rules (XRechnung, Peppol BIS 3) and get back errors that teach the regulation: every failure carries the official rule id, the business term it constrains, what the rule requires, and how to fix it. Invoices go in and come out in both EN 16931 syntaxes, UBL 2.1 and UN/CEFACT CII; the profile chooses the syntax on generate, and on validate the document''s root element does. The `facturx-en16931` profile is checked against core EN 16931 only — there are no Factur-X-specific rules. PDF: Factur-X and ZUGFeRD files are CII XML inside a PDF/A-3 container. POST /v1/validate and POST /v1/parse open one sent as `application/pdf`, on every plan, and read the XML attached to it; /v1/validate also reports on the PDF. 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. The API builds one in exactly one case, profile `facturx-en16931`: a Factur-X / ZUGFeRD PDF (EN 16931 profile), a page drawn from the invoice, written as PDF/A-3B, with the CII embedded as `factur-x.xml` — watermarked as a preview on the free plan, and without the watermark on a paid plan. `POST /v1/generate?format=pdf` returns it, `POST /v1/bundle?format=pdf` puts it in an evidence bundle beside the XML and a signed validation report, and `POST /v1/send` with `"format": "pdf"` e-mails it. Every other invoice the API writes is XML. Sending: `POST /v1/send` e-mails one invoice to one address on request; the API does not operate Peppol or French-platform transmission.'
contact:
name: Attestwire
url: https://attestwire.com
license:
name: Proprietary
servers:
- url: https://api.attestwire.com
description: Production
security:
- bearerAuth: []
tags:
- name: Records
description: Shareable proof that a check ran. Public, unauthenticated, and free on every plan.
paths:
/v1/records/{id}:
get:
tags:
- Records
operationId: getValidationRecord
summary: Read a stored validation record
security: []
description: 'Returns the record created by a `POST /v1/validate?record=true` call, exactly as it was stored.
NO API KEY. The 128-bit id in the path is the credential: it is unguessable, so anyone holding the link reads the record and anyone who does not cannot enumerate their way to one. That is deliberate — the audience for a record is a buyer, an auditor or an onboarding desk, none of whom are our customers.
It is safe to hand out because of what is in it: a SHA-256 fingerprint of the document, the findings, and what ran. **The document itself was never stored**, and neither was the email or the API key of whoever ran the check.
The URL is the verification. This page is served by Attestwire from its own stored copy of the record, so whoever holds the link sees exactly what Attestwire recorded when the check ran — nobody can edit it, and the record cannot be re-issued with different findings under the same address. The link is also the only credential: the record id is 128 random bits and is not guessable, so anyone you send it to can read the record, and anyone you do not, cannot. Nothing here identifies the customer who ran the check, and the document itself was never stored — only its SHA-256 fingerprint, which you can reproduce from your own copy of the file.
IMMUTABLE, so the response carries a one-year `Cache-Control: public, max-age=31536000, immutable`. There is no update path — a record cannot be re-issued with different findings at the same address. The only thing that changes is that it stops existing 365 days after creation.
CORS is `*`, so a supplier-onboarding page can render one in the browser.
The same record with a human face is at `/r/{id}`, which is not described here because it is an HTML page rather than an API.'
parameters:
- name: id
in: path
required: true
description: The record id from the `record.id` field of the validation response.
schema:
type: string
pattern: ^[0-9a-f]{32}$
responses:
'200':
description: The record. Read `statement` before quoting anything else from it.
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationRecord'
examples:
record:
summary: A record of a check that FOUND something — which is a perfectly ordinary record, not a failure to produce one.
value:
type: attestwire.validation-record
version: 1
id: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
created: '2026-08-14T09:12:33.000Z'
expires: '2027-08-14T09:12:33.000Z'
document:
sha256: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
hash_input: document-bytes
bytes: 8214
valid: false
counts:
errors: 1
warnings: 0
information: 0
findings:
- rule: BR-DE-15
severity: fatal
message: XRechnung requires a buyer reference (BT-10). For German public-sector buyers this is the Leitweg-ID; business buyers may supply any reference, but the field must be present.
xpath: /ubl:Invoice/cbc:BuyerReference
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'
statement: This record states that a document with the SHA-256 fingerprint shown was checked by Attestwire engine 0.14.1 against ruleset en16931@0.14.1 on 2026-08-14, producing the findings listed. It is a record of an automated software check. It is not a certification of legal or tax compliance, not a guarantee that any platform, authority, or recipient will accept the document, and not legal advice.
signature_ed25519:
algorithm: Ed25519
key_id: kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k
value: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
signature:
algorithm: HMAC-SHA256
key_id: xxxxxxxxxxxxxxxx
value: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
'404':
description: 'No record with that id. Two causes, and they are indistinguishable from here because an expired record leaves nothing behind: the id is wrong or incomplete, or the record is past its 365-day expiry and has been deleted. Not cached — a client that races its own validation response must not get a "gone" pinned in a shared cache.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
record_not_found:
summary: Unknown or expired
value:
error: record_not_found
message: No validation record with that id. Either the id is wrong or incomplete, or the record has passed its one-year expiry and has been deleted — from here the two are indistinguishable, because an expired record leaves nothing behind. Records are kept for 365 days from creation and are not recoverable afterwards; ask whoever ran the check to validate the document again.
docs: https://api.attestwire.com/docs#records
'500':
description: Unhandled fault while reading a validation record. Never expected; the body carries no detail on purpose (a fault message can quote the offending value). Retry, and email hello@attestwire.com if it persists.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
internal_error:
summary: Unhandled fault
value:
error: internal_error
message: Something broke on our side. Try again.
/.well-known/attestwire-signing-keys.json:
get:
tags:
- Records
operationId: signingKeys
summary: The Ed25519 public keys Attestwire signs with
description: 'A JWK Set (RFC 7517) of the Ed25519 public keys this deployment signs with. Free, unauthenticated, open to any origin, and cacheable for a day while a key is published (five minutes while none is).
Every Ed25519 signature Attestwire hands out names its key by `key_id`; find the entry whose `kid` matches and verify with any Ed25519 library, OpenSSL 3 (`pem`), or `node:crypto`. `kid` is the key''s own RFC 7638 thumbprint, so you can recompute it from `x`. `status` is `active` for the key that signs today and `retired` for keys kept so that what they signed can still be checked. Meeting a `key_id` you have not seen means the key was rotated: fetch the set again.
Take the keys from this address. A key or a URL that travels with the thing you are checking proves nothing about who made it.
A deployment with no signing key configured answers `keys: []` and a `note`, and nothing it issues carries an Ed25519 signature — every response that would have carried one says it is unsigned instead.'
security: []
responses:
'200':
description: 'The published keys: the active one first, then any retired ones.'
content:
application/json:
schema:
$ref: '#/components/schemas/SigningKeySet'
examples:
active:
summary: A deployment that signs
value:
issuer: Attestwire
docs: https://api.attestwire.com/docs#signing-keys
keys:
- kty: OKP
crv: Ed25519
x: 11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo
kid: kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k
use: sig
status: active
pem: '-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=
-----END PUBLIC KEY-----
'
unconfigured:
summary: No signing key configured
value:
issuer: Attestwire
docs: https://api.attestwire.com/docs#signing-keys
note: No signing key is configured on this deployment, so nothing it issues now carries an Ed25519 signature.
keys: []
'500':
description: Unhandled fault while reading the signing keys. Never expected; the body carries no detail on purpose (a fault message can quote the offending value). Retry, and email hello@attestwire.com if it persists.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
internal_error:
summary: Unhandled fault
value:
error: internal_error
message: Something broke on our side. Try again.
components:
schemas:
TruncationCount:
type: object
required:
- total
- returned
properties:
total:
type: integer
returned:
type: integer
RecordFinding:
type: object
description: 'One finding as a record states it. FOUR FIELDS, not the eight a `TeachingError` carries: a record is read by a third party asking "what did the check say", not by the person editing the invoice, so `fix`, `example`, `docsUrl` and `field` are left in the validation response where they are useful. `message` is the engine''s own text, verbatim.'
required:
- rule
- severity
- message
properties:
rule:
type:
- string
- 'null'
examples:
- BR-DE-15
severity:
type:
- string
- 'null'
enum:
- fatal
- warning
- information
- null
message:
type:
- string
- 'null'
xpath:
type: string
description: Absent when the finding names no location.
SigningKeySet:
type: object
description: A JWK Set. `keys` is the standard member and the only one a JWKS library reads; `issuer`, `docs` and `note` are for a human opening the URL.
required:
- issuer
- docs
- keys
properties:
issuer:
type: string
enum:
- Attestwire
docs:
type: string
format: uri
note:
type: string
description: Present only when no key signs on this deployment today, and says so.
keys:
type: array
items:
$ref: '#/components/schemas/SigningKey'
RecordSignatureEd25519:
type: object
description: 'The `signature_ed25519` field is an Ed25519 signature over this record, which anybody can check against the public key Attestwire publishes, without asking us: it proves the record was issued by the holder of that key and has not changed by one byte since. Signed bytes: the canonical JSON of the record without `signature` and `signature_ed25519` (object keys sorted at every level, no whitespace), as UTF-8. The public key is the entry in `GET /.well-known/attestwire-signing-keys.json` whose `kid` equals `key_id`.'
required:
- algorithm
- key_id
- value
properties:
algorithm:
type: string
enum:
- Ed25519
key_id:
type: string
description: The `kid` of the published key that made the signature.
value:
type: string
pattern: ^[0-9a-f]{128}$
description: The 64-byte signature, lowercase hex.
RecordSignature:
type: object
description: 'The `signature` field is an HMAC-SHA256 over this record, which proves the record was issued by Attestwire and has not been modified since. HMAC is symmetric: the key that signs is the key that verifies and only Attestwire holds it, so checking that signature means asking us — email hello@attestwire.com with the record id. The Ed25519 signature, on a record that carries one, is the one you can check yourself.'
required:
- algorithm
- key_id
- value
properties:
algorithm:
type: string
enum:
- HMAC-SHA256
key_id:
type: string
description: Which signing key was used — the first 16 hex of its SHA-256. Names the key across a rotation; reveals nothing about it.
value:
type: string
description: Lowercase hex MAC over the canonical record.
Provenance:
type: object
description: 'What ran. Present on every metered response — POST /v1/validate and POST /v1/parse, JSON body or XML document, and the JSON envelope of POST /v1/generate including the `422` it returns for an invoice it will not emit.
WHY IT EXISTS. Pin `@attestwire/en16931` and your CI gets the same answer for the same invoice forever; the hosted API always runs the current rule set, because a compliance service that froze its rules would be answering last quarter''s regulation. Both are correct and the difference used to be invisible — a `valid: false` looked identical whether your invoice had changed or our rules had. This says which.
The one metered response WITHOUT it is `/v1/generate?format=xml`, which returns the document alone: an XML file has nowhere to put this, and this API does not write its own metadata into a customer''s tax document.'
required:
- engine
- engine_version
- ruleset
- profile
- kosit_conformance
properties:
engine:
type: string
description: The npm package every compliance decision on this API comes from. Install this package at `engine_version` and you have the build that judged your document.
examples:
- '@attestwire/en16931'
engine_version:
type: string
examples:
- 0.14.1
ruleset:
type: string
description: The standard and our build of it, as one identifier to quote.
examples:
- en16931@0.14.1
profile:
type:
- string
- 'null'
description: 'The CIUS that ACTUALLY ran, read from the result rather than from your request: a body naming no profile is judged as `en16931`, and this is where you see that. `null` when there was no verdict to read it from.'
examples:
- xrechnung-ubl
kosit_conformance:
$ref: '#/components/schemas/KositConformance'
SigningKey:
type: object
description: One Ed25519 public key as a JWK (RFC 8037), plus `status` and `pem`, which a JWK reader ignores.
required:
- kty
- crv
- x
- kid
- use
- status
- pem
properties:
kty:
type: string
enum:
- OKP
crv:
type: string
enum:
- Ed25519
x:
type: string
description: The 32-byte public key, base64url without padding.
kid:
type: string
description: 'The key id every signature names: the RFC 7638 thumbprint of this key — SHA-256 over `{"crv":"Ed25519","kty":"OKP","x":"…"}`, base64url.'
use:
type: string
enum:
- sig
status:
type: string
enum:
- active
- retired
description: '`active` — signs what this deployment issues today. `retired` — no longer signs, and stays published so that what it signed can still be checked.'
pem:
type: string
description: The same key as a PEM `PUBLIC KEY` block, for `openssl pkeyutl -verify -pubin -inkey`.
Error:
type: object
required:
- error
- message
properties:
error:
type: string
description: Stable machine-readable code. Branch on this, not on `message`.
message:
type: string
description: Human-readable explanation.
docs:
type: string
format: uri
upgrade_url:
type: string
format: uri
description: 'Present on `quota_exceeded` only. The page where a human buys a bigger plan. It is a plain link, not a checkout transaction: nothing is bought and no billing state is created by reading it. Absent on every other error code.'
examples:
- https://attestwire.com/pricing#upgrade
xml_code:
type: string
description: 'Present when POST /v1/validate or POST /v1/parse (or /v1/bundle, or /v1/send''s `document`) was sent a document that could not be read and the engine''s reader raised an error. It is that reader''s own stable code, echoed unchanged, and it is finer-grained than `error`: a whole family of well-formedness defects shares `error: "xml_malformed"` and each member carries its own `xml_code`. For a PDF the reader is the PDF one, and the codes start `pdf_` or `facturx_` — the name of the field predates PDFs. Absent when the file was recognised on sight as something else (an HTML page, a ZIP archive), where the reader raised nothing. The list below is a sample, not the full set, and it is deliberately open — the reader gains codes as it learns to refuse more, and a caller that switches exhaustively on this field will break when it does. Branch on `error` first and on this only if you need the detail.'
examples:
- unsupported_syntax
- xml_doctype_forbidden
- xml_entity_forbidden
- xml_too_deep
- xml_too_many_elements
- xml_too_many_attributes
- xml_unbalanced
- xml_mixed_content
- xml_duplicate_attribute
- xml_bad_comment
- facturx_no_xml_attachment
- facturx_xml_encoding
- pdf_no_startxref
- pdf_stream_too_large
KositConformance:
type: object
description: 'When the official KoSIT validator was last run against this engine''s committed fixtures, and with what.
**NOTHING IS SENT TO KoSIT WHEN YOU CALL US.** This is a dated record of a check we ran on release, not a per-call verification, and it says nothing about YOUR document.'
required:
- recorded
- validator
- configuration
- configuration_published
properties:
recorded:
type: string
format: date
description: The date of the last recorded full fixture run.
examples:
- '2026-09-24'
validator:
type: string
description: Version of the KoSIT standalone validator used.
examples:
- 1.6.3
configuration:
type: string
description: Version of the XRechnung validator configuration used.
examples:
- 3.0.2
configuration_published:
type: string
format: date
description: The date that configuration was published by KoSIT.
examples:
- '2026-08-31'
FindingsTruncated:
type: object
description: 'Present ONLY when a findings array was truncated, and absent — not null, not zeroed — otherwise. Each of `errors`, `warnings` and `information` is capped independently; past the cap the array is cut and this object states what was left out. `valid` still reflects the whole document: truncation changes what is printed, never the verdict. Reaching the cap takes a hundred-plus invoice lines on which every field is wrong, and at that scale the errors are systematic — every distinct problem in the document already appears in what you were given. Treat it as a hint to fix the pattern and re-validate, not as an error.'
required:
- total
- returned
- cap
- by_severity
- note
properties:
total:
type: integer
description: Findings the engine produced, across all three severities.
examples:
- 20012
returned:
type: integer
description: Findings actually listed in this response.
examples:
- 500
cap:
type: integer
description: The per-severity cap this deployment applied.
examples:
- 500
by_severity:
type: object
description: Per-array totals, so you can see which one overflowed.
required:
- errors
- warnings
- information
properties:
errors:
$ref: '#/components/schemas/TruncationCount'
warnings:
$ref: '#/components/schemas/TruncationCount'
information:
$ref: '#/components/schemas/TruncationCount'
note:
type: string
description: Prose explanation, safe to show a user or an agent verbatim.
ValidationRecord:
type: object
description: 'A stored validation record. IMMUTABLE — there is no update path, so this object is byte-identical on every read until it expires.
READ THE `statement` FIELD. It is the exact contract this artefact makes and it is deliberately narrow: a record of an automated software check, not a legal or tax conclusion and not an acceptance guarantee.
SIGNED TWICE when the deployment has both keys. `signature_ed25519` is over the canonical JSON of every field here EXCEPT the two signatures, and anybody can check it against `GET /.well-known/attestwire-signing-keys.json`. `signature` (HMAC-SHA256) is over every field EXCEPT `signature` itself — the Ed25519 signature included — and only Attestwire can check it. Canonical means object keys sorted at every level, no whitespace. Records from before the Ed25519 key carry only `signature`; a deployment with no key configured issues records with neither.'
required:
- type
- version
- id
- created
- expires
- document
- valid
- counts
- findings
- provenance
- statement
properties:
type:
type: string
enum:
- attestwire.validation-record
version:
type: integer
enum:
- 1
id:
type: string
pattern: ^[0-9a-f]{32}$
created:
type: string
format: date-time
expires:
type: string
format: date-time
document:
type: object
description: The document, as a fingerprint. THE DOCUMENT ITSELF WAS NEVER STORED.
required:
- sha256
- hash_input
- bytes
properties:
sha256:
type: string
pattern: ^[0-9a-f]{64}$
hash_input:
type: string
enum:
- document-bytes
- xml-utf8-bytes
- canonical-json
description: '`document-bytes` — the submitted file byte for byte, as it arrived: XML in whatever encoding it declares, or the PDF itself; `shasum -a 256 invoice.xml` (or `invoice.pdf`) reproduces it. `canonical-json` — the invoice model with keys sorted and whitespace removed, used when the invoice arrived as our JSON model and there was no file to hash. `xml-utf8-bytes` is on records stored before 2026-09-24, when a document was fingerprinted as its text re-encoded as UTF-8 — the file''s own bytes only for a UTF-8 file without a byte-order mark.'
bytes:
type: integer
description: Length of what was hashed.
valid:
type: boolean
description: No fatal findings. Copied from the verdict.
counts:
type: object
required:
- errors
- warnings
- information
properties:
errors:
type: integer
warnings:
type: integer
information:
type: integer
findings:
type: array
items:
$ref: '#/components/schemas/RecordFinding'
findings_truncated:
$ref: '#/components/schemas/FindingsTruncated'
provenance:
$ref: '#/components/schemas/Provenance'
statement:
type: string
description: The liability wording, in full and verbatim on every record. Rendered unaltered on the HTML page too.
signature_ed25519:
$ref: '#/components/schemas/RecordSignatureEd25519'
signature:
$ref: '#/components/schemas/RecordSignature'
securitySchemes:
bearerAuth:
type: http
scheme: bearer
description: 'Send your key as `Authorization: Bearer aw_live_...`. The public demo key, `Authorization: Bearer demo`, works on POST /v1/validate, POST /v1/generate and POST /v1/recipient only, with no account: 6 requests a minute and 30 documents a day per client IP, its PDF always watermarked, and every other route answers `403 demo_key_not_allowed`.'
externalDocs:
description: Hand-written docs, the quickstart, and the per-rule pages at attestwire.com/rules/BR-*. This deployment also serves /llms.txt for agents and an MCP endpoint at /mcp, neither of which is describable in OpenAPI.
url: https://api.attestwire.com/docs