Attestwire Records API

Shareable proof that a check ran. Public, unauthenticated, and free on every plan.

Operations 2

GET /v1/records/{id} Read a stored validation record #
GET /.well-known/attestwire-signing-keys.json The Ed25519 public keys Attestwire signs with #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/attestwire-com:attestwire-com-records-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

attestwire-com-records-api-openapi.yml Raw ↑
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