Attestwire Keys API

Self-serve API key issuance.

Operations 3

POST /v1/keys Issue a free-tier API key #
POST /v1/keys/rotate Replace the calling key with a new one #
GET /v1/usage Current period usage for the calling key #

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-keys-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-keys-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Attestwire Keys 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: Keys
  description: Self-serve API key issuance.
paths:
  /v1/keys:
    post:
      tags:
      - Keys
      operationId: createKey
      summary: Issue a free-tier API key
      description: 'No authentication. One free key per email address — the plaintext key is returned exactly once and is not recoverable afterwards. Send YOUR address: whatever you post here is claimed permanently (we keep only its hash, so it can never be re-issued), and the example below is a placeholder on the reserved `.example` TLD, not an address to send as-is.'
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - email
              properties:
                email:
                  type: string
                  format: email
                  maxLength: 254
            examples:
              signup:
                summary: REPLACE with your own address — it is claimed permanently.
                value:
                  email: you@your-company.example
      responses:
        '201':
          description: Key created. Store `key` now; it is never shown again.
          content:
            application/json:
              schema:
                type: object
                required:
                - key
                - tier
                - monthly_limit
                - created
                - warning
                properties:
                  key:
                    type: string
                    examples:
                    - aw_live_xxxxxxxx
                  tier:
                    type: string
                    enum:
                    - free
                    - solo
                    - starter
                    - scale
                  monthly_limit:
                    type: integer
                  created:
                    type: string
                    format: date-time
                  warning:
                    type: string
              examples:
                created:
                  summary: The only time the plaintext key exists on our side.
                  value:
                    key: aw_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
                    tier: free
                    monthly_limit: 100
                    created: '2026-08-11T09:12:33.104Z'
                    warning: Store this key now. It is shown once and cannot be recovered.
        '400':
          description: Missing or malformed email address (`invalid_email`), or a body that is not a JSON object (`bad_request`). Checked BEFORE the per-IP limiter, so a typo does not spend one of your five daily signups.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalid_email:
                  summary: Not an address
                  value:
                    error: invalid_email
                    message: That does not look like an email address.
                    docs: https://api.attestwire.com/docs
        '409':
          description: A free key already exists for this email address, and we store only its hash — it cannot be shown again. If you still hold it, replace it with POST /v1/keys/rotate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                key_already_issued:
                  summary: One free key per address
                  value:
                    error: key_already_issued
                    message: 'A free key already exists for this email address. If you still hold the key, replace it yourself with POST /v1/keys/rotate — your plan and usage carry over. If it is lost, a different email address gets you a working free key immediately, and a plus-alias counts. On a paid plan, email hello@attestwire.com from the address on your Paddle billing receipt and the replacement comes back to that address. Either way we cannot send you the old key: we store only its SHA-256 hash, so there is nothing to show again.'
                    docs: https://api.attestwire.com/docs#rotation
        '413':
          description: The request body is over 1 MB. Nothing was read and nothing was charged.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                payload_too_large:
                  summary: Body over 1 MB
                  value:
                    error: payload_too_large
                    message: Request body is over 1 MB.
                    docs: https://api.attestwire.com/docs
        '429':
          description: Too many key requests from this IP address today.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                too_many_requests:
                  summary: Five free keys per IP per UTC day
                  value:
                    error: too_many_requests
                    message: That is 5 key requests from this address today. If you need more, email hello@attestwire.com rather than looping.
                    docs: https://api.attestwire.com/docs#limits
        '500':
          description: Unhandled fault while issuing the key. 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.
  /v1/keys/rotate:
    post:
      tags:
      - Keys
      operationId: rotateKey
      summary: Replace the calling key with a new one
      description: 'Authenticated by the key being replaced — that bearer token IS the proof of ownership, and there is deliberately no email-triggered variant. No request body. Consumes no documents.


        Everything follows the new key atomically: tier and limit, the current period''s usage (rotation does NOT reset your quota), any past_due flag, the original `created` date, and the subscription link — including billing events that were already in flight when you rotated.


        The plaintext key is returned exactly once, and the old key answers 410 `key_rotated` from the moment this call succeeds. Deploy first, then rotate. Limited to 3 rotations per key per UTC day; the limit follows the key through a rotation, so chaining does not reset it. Two simultaneous calls cannot both succeed — the loser gets 409.


        If you no longer HAVE the key, this endpoint is closed to you — there is nothing to authenticate the call with. What is available instead depends on the tier, and on the free tier it is still immediate.


        If you are on a paid plan and the key is gone, email hello@attestwire.com from the address on your Paddle billing receipt. We check the request against Paddle''s record of your subscription rather than against the receipt itself — a receipt can be forwarded, so attaching one proves only that somebody has a copy of it — and the replacement is delivered one way only: as a reply to the billing address on file, whoever did the asking. Whoever handles it retires the old key and issues the new one.


        We cannot recover a lost free key — not as a matter of policy, but because we never collected anything that could prove it was yours: a free signup address is never verified, and the key itself only ever existed in one HTTP response. The practical answer is faster than support anyway. The limit is one free key per email address, so a different address gets you a working free key on the spot, and a plus-alias such as you+attestwire@example.com counts. That limit keeps casual signups to one key each; it is housekeeping rather than a security control, and it was never meant to trap you. If you would rather we were able to rescue you, that is part of what a paid plan buys: Paddle holds a billing identity we can check.'
      responses:
        '200':
          description: Rotated. Store `key` now; it is never shown again. The other fields are the continuity proof — same tier, same usage.
          content:
            application/json:
              schema:
                type: object
                required:
                - key
                - tier
                - monthly_limit
                - period
                - used
                - rotated_at
                properties:
                  key:
                    type: string
                    examples:
                    - aw_live_xxxxxxxx
                  tier:
                    type: string
                    enum:
                    - free
                    - solo
                    - starter
                    - scale
                  monthly_limit:
                    type: integer
                  period:
                    type: string
                    examples:
                    - 2026-08
                  used:
                    type: integer
                    description: Documents already used this period, carried over from the old key.
                  remaining:
                    type: integer
                  resets_at:
                    type: string
                    format: date-time
                  created:
                    type:
                    - string
                    - 'null'
                    format: date-time
                    description: Original signup date, unchanged by rotation.
                  rotated_at:
                    type: string
                    format: date-time
                  rotations_remaining_today:
                    type: integer
                  warning:
                    type: string
              examples:
                rotated:
                  summary: The old key is dead from this response onward.
                  value:
                    key: aw_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
                    tier: starter
                    monthly_limit: 5000
                    period: 2026-08
                    used: 418
                    remaining: 4582
                    resets_at: '2026-09-01T00:00:00.000Z'
                    created: '2026-01-15T09:12:04.000Z'
                    rotated_at: '2026-08-11T10:04:31.000Z'
                    rotations_remaining_today: 2
                    warning: Store this key now. It is shown once and cannot be recovered. The previous key stopped working when this response was generated and now answers 410 key_rotated.
        '401':
          description: Missing or unknown API key.
          headers:
            WWW-Authenticate:
              schema:
                type: string
                examples:
                - Bearer
              description: Always `Bearer`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                missing_api_key:
                  summary: No Authorization header
                  value:
                    error: missing_api_key
                    message: 'Send your key as an Authorization: Bearer header. Get one free at POST /v1/keys.'
                    docs: https://api.attestwire.com/docs#auth
                invalid_api_key:
                  summary: Bearer token we do not recognise
                  value:
                    error: invalid_api_key
                    message: 'That API key is not recognised. Check that you pasted the whole key: it is aw_live_ followed by 40 characters, and a truncated copy is the usual cause. If you rotated this key, use the key that the rotation returned. If you no longer have a key, a new one is free at POST /v1/keys.'
                    docs: https://api.attestwire.com/docs#auth
        '409':
          description: '`rotation_already_done` — this key has already been rotated, or a concurrent rotate call won the race. The replacement was returned to that call.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                rotation_already_done:
                  summary: The replacement exists and this call is not the one that sees it
                  value:
                    error: rotation_already_done
                    message: This key has already been rotated, so it cannot be rotated again. The replacement was returned exactly once, in the response to the request that performed the rotation — if two rotations were sent at the same time, the other response has your new key. If it is lost, email hello@attestwire.com from the address on your Paddle billing receipt.
                    docs: https://api.attestwire.com/docs#rotation
        '410':
          description: The key presented was retired by an earlier rotation. Its replacement is the live credential; this one is permanently gone, so retrying with it cannot work. Every authenticated endpoint answers this, not just /v1/keys/rotate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                key_rotated:
                  summary: The key was replaced by POST /v1/keys/rotate
                  value:
                    error: key_rotated
                    message: This API key was rotated on 2026-08-01 and no longer works. Use the replacement key returned by that rotation call — it was shown once and we cannot show it again. If you did not rotate it, or the replacement is lost, email hello@attestwire.com from the address on your Paddle billing receipt.
                    docs: https://api.attestwire.com/docs#rotation
        '429':
          description: '`rotation_rate_limited` — more than 3 rotations of this key today.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                rotation_rate_limited:
                  summary: The daily ceiling, which a rotation does not reset
                  value:
                    error: rotation_rate_limited
                    message: This key has already been rotated 3 times today, which is the daily limit. The limit follows the key through a rotation, so rotating again does not reset it. It clears at 00:00 UTC. If you are locked out of your own key, email hello@attestwire.com from the address on your Paddle billing receipt.
                    docs: https://api.attestwire.com/docs#rotation
        '500':
          description: 'Two different faults share this status and they mean different things. `error: "rotation_failed"` — a step of the rotation failed and the whole thing was unwound: NOTHING changed, your existing key still works, and it is safe to retry. `error: "internal_error"` — the unhandled fault every route can answer, which here leaves the outcome UNKNOWN. Do not rotate again on that one; call GET /v1/usage with the old key first. A `410 key_rotated` proves the rotation completed, and the replacement key was spent on the response you never got — email hello@attestwire.com rather than rotating again. A `200` is evidence it did not, but not proof: that lookup reads an eventually-consistent store, so a rotation that did land can still answer 200 for a short window. Re-check after a minute and only rotate again once 200 is stable.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                rotation_failed:
                  summary: Rotation unwound. The old key still works — this is the recoverable one.
                  value:
                    error: rotation_failed
                    message: The rotation could not be completed and nothing was changed. Your existing key still works; try again.
                    docs: https://api.attestwire.com/docs#rotation
                internal_error:
                  summary: Unhandled fault. Outcome unknown.
                  value:
                    error: internal_error
                    message: Something broke on our side. Try again.
  /v1/usage:
    get:
      tags:
      - Keys
      operationId: getUsage
      summary: Current period usage for the calling key
      description: Free, and does not consume a document.
      responses:
        '200':
          description: Usage for the current UTC month.
          content:
            application/json:
              schema:
                type: object
                required:
                - tier
                - period
                - used
                - limit
                - remaining
                - resets_at
                - past_due
                properties:
                  tier:
                    type: string
                  period:
                    type: string
                    examples:
                    - 2026-08
                  used:
                    type: integer
                  limit:
                    type: integer
                  remaining:
                    type: integer
                  resets_at:
                    type: string
                    format: date-time
                  past_due:
                    type: boolean
                    description: True when the last subscription payment failed and the provider is still retrying. The paid allowance is UNCHANGED while this is true; only if every retry fails does the key drop to the free tier. This is the only in-band billing warning the API gives — poll it if you want to detect trouble programmatically.
                  billing_warning:
                    type: string
                    description: Human-readable explanation, present only when past_due is true.
                  portal_endpoint:
                    $ref: '#/components/schemas/PortalEndpoint'
                  tier_expires:
                    type: string
                    format: date-time
                    description: 'Present only on a key whose tier is TIME-LIMITED — today, a key minted by an Invoice Fix Report claim (POST /v1/fix/claim), which carries 30 days of starter. Published both before and after the date passes: once past, `tier` above already reads `free`, and this field is the whole explanation of why. The key itself never expires and is never deleted; only the monthly allowance changes.'
              examples:
                fix_report:
                  summary: A key from a Fix Report claim, still inside its 30 days.
                  value:
                    tier: starter
                    period: 2026-08
                    used: 40
                    limit: 5000
                    remaining: 4960
                    resets_at: '2026-09-01T00:00:00.000Z'
                    past_due: false
                    tier_expires: '2026-09-14T09:12:33.104Z'
                free:
                  summary: A free key. No subscription, so no portal endpoint.
                  value:
                    tier: free
                    period: 2026-08
                    used: 12
                    limit: 100
                    remaining: 88
                    resets_at: '2026-09-01T00:00:00.000Z'
                    past_due: false
        '401':
          description: Missing or unknown API key.
          headers:
            WWW-Authenticate:
              schema:
                type: string
                examples:
                - Bearer
              description: Always `Bearer`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                missing_api_key:
                  summary: No Authorization header
                  value:
                    error: missing_api_key
                    message: 'Send your key as an Authorization: Bearer header. Get one free at POST /v1/keys.'
                    docs: https://api.attestwire.com/docs#auth
                invalid_api_key:
                  summary: Bearer token we do not recognise
                  value:
                    error: invalid_api_key
                    message: 'That API key is not recognised. Check that you pasted the whole key: it is aw_live_ followed by 40 characters, and a truncated copy is the usual cause. If you rotated this key, use the key that the rotation returned. If you no longer have a key, a new one is free at POST /v1/keys.'
                    docs: https://api.attestwire.com/docs#auth
        '410':
          description: The key presented was retired by an earlier rotation. Its replacement is the live credential; this one is permanently gone, so retrying with it cannot work. Every authenticated endpoint answers this, not just /v1/keys/rotate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                key_rotated:
                  summary: The key was replaced by POST /v1/keys/rotate
                  value:
                    error: key_rotated
                    message: This API key was rotated on 2026-08-01 and no longer works. Use the replacement key returned by that rotation call — it was shown once and we cannot show it again. If you did not rotate it, or the replacement is lost, email hello@attestwire.com from the address on your Paddle billing receipt.
                    docs: https://api.attestwire.com/docs#rotation
        '500':
          description: Unhandled fault while reading the meter. 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:
    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
    PortalEndpoint:
      type: string
      description: 'The ROUTE that mints a billing-portal link — not a link. Always `/v1/billing/portal`. Call it with the customer''s own key and it returns a one-time URL where they change plan, update the card, download invoices and cancel.


        It is an endpoint rather than a URL because Paddle has no static portal page: each link is a session bound to one customer and it expires, so there is nothing durable to publish here, and minting one on every `/v1/usage` poll would spend a live API call on a link nobody opens.


        Present on `/v1/usage` only for keys that actually have a subscription — a free key has nothing to manage, and pointing one at a route that will answer 409 is worse than saying nothing. Absent, never null.'
      examples:
      - /v1/billing/portal
  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