# Attestwire API > HTTP API for EN 16931 e-invoice compliance. Validate an invoice supplied as > JSON against EN 16931 and its national CIUS rule sets (XRechnung UBL/CII, > Peppol BIS 3, Factur-X), and generate compliant XML. Validation failures are > returned as "teaching errors": each carries the official rule id, the business > term (BT-/BG-) it constrains, a plain-English statement of what the regulation > requires, and a concrete fix. Base URL: https://api.attestwire.com OpenAPI 3.1: https://api.attestwire.com/openapi.json Human docs: https://api.attestwire.com/docs MCP server: https://api.attestwire.com/mcp (streamable HTTP) — see "MCP" below ## Authentication Bearer token. Header: `Authorization: Bearer aw_live_...` Get a free key with no signup flow: curl -X POST https://api.attestwire.com/v1/keys -H 'content-type: application/json' \ -d '{"email":"you@example.com"}' WARNING: the key is returned once and cannot be recovered; store it before proceeding. One key per email. Only its SHA-256 hash is kept, so we cannot look it up, resend it, or recover it for you — a lost key has to be revoked and replaced (hello@attestwire.com). ### Rotation A key you still hold can be replaced without support: curl -X POST https://api.attestwire.com/v1/keys/rotate \ -H 'authorization: Bearer aw_live_YOUR_CURRENT_KEY' No body. Free — it consumes no documents. Returns 200 {key, tier, monthly_limit, period, used, remaining, created, rotated_at, rotations_remaining_today, warning}. The plan, the current period's usage and the Stripe subscription all follow the new key; `created` stays the original signup date. The new key is shown once, exactly like signup. The old key is dead immediately and answers 410 key_rotated from then on — deploy the replacement before rotating. Limit 3 rotations per key per UTC day, and the limit follows the key, so chaining does not reset it. Two simultaneous rotations cannot both win: the loser gets 409 rotation_already_done. There is NO email-triggered rotation and no way to rotate a key you no longer have: that would let anyone who knows an address take the account over. A lost key is a support path (hello@attestwire.com, from the Stripe billing address). For the same reason the MCP server exposes no rotation tool — rotating is a human decision about a credential the agent cannot re-deploy. ## Endpoints - POST /v1/validate — body: InvoiceInput. Returns ValidationResult {valid, profile, errors[], warnings[]}. Always 200 when the request is well-formed, including for invalid invoices. Costs 1 document. - POST /v1/generate — body: InvoiceInput, optional ?format=xml. Returns {xml, profile} as JSON, or raw application/xml. Costs 1 document. Available in this build. - POST /v1/keys — body: {email}. Returns 201 {key, tier, monthly_limit, created}. Free. - POST /v1/keys/rotate — no body, authenticated by the key being replaced. Returns 200 with a new key and the carried-over tier/usage. Free. See "Rotation" above. - GET /v1/usage — current period usage for the calling key. Free. - GET /v1/health — {ok, runtime, generation, billing}. Free, unauthenticated. - POST /v1/billing/checkout — body: {key, plan: "starter"|"scale"}. Returns a Stripe Checkout URL, or 503 when billing is not configured. Returns 409 already_subscribed if the key already has a subscription — change plan in the Stripe billing portal, never with a second checkout (that bills for both). ## InvoiceInput (required fields) profile: "en16931" | "xrechnung-ubl" | "xrechnung-cii" | "facturx-en16931" | "peppol-bis-3" invoiceNumber: string (BT-1) issueDate: ISO 8601 date (BT-2) currency: ISO 4217 (BT-5) buyerReference: string (BT-10) — required by BR-DE-15 for XRechnung profiles seller, buyer: {name, vatId?, address:{line1?, city, postalCode, countryCode}, electronicAddress?, contact?} lines[]: {id, description, quantity, unitCode, unitPrice, vatCategory, vatRate?} vatCategory (BT-151): S standard | Z zero-rated | E exempt | AE reverse charge | K intra-community | G export | O out of scope paymentTerms?: string (BT-20) ## TeachingError {rule, field, severity: "fatal"|"warning", message, fix, xpath?, docsUrl, example?} "valid" is false only when at least one error has severity "fatal". ## Errors Request-level failures use {error, message, docs} with a stable `error` code. Branch on `error`, not on `message`. Codes: bad_request, invalid_email, missing_api_key, invalid_api_key, not_found, method_not_allowed, key_already_issued, already_subscribed, rotation_already_done, key_rotated, payload_too_large, quota_exceeded, too_many_requests, rotation_rate_limited, generation_not_yet_available, billing_not_enabled, internal_error. ## Quotas Documents per calendar month, per key, resetting 00:00 UTC on the 1st. Free 100 · Starter 2,500 ($49/mo) · Scale 25,000 ($199/mo). Metered responses carry X-RateLimit-Limit / -Remaining / -Reset / -Period. Over quota: 429 quota_exceeded. Refused calls do not consume quota. Upgrading is self-serve: POST /v1/billing/checkout {key, plan} returns {url, session_id, plan}; redirect the customer to `url` (Stripe-hosted Checkout). A HUMAN must complete that page — an agent cannot buy on their behalf. The key's tier changes when Stripe confirms the subscription. Over MCP the same thing is get_upgrade_link, which costs no documents. Failed payments: while Stripe is retrying (past_due) the paid allowance is untouched and GET /v1/usage reports "past_due": true. Only once every retry has failed (unpaid) does the key drop to the free allowance. Cancelling keeps the paid allowance until the end of the period already paid for. No downgrade ever revokes a key — only the monthly limit changes. ## Profiles | profile | validate | generate | | --- | --- | --- | | en16931 | yes | yes | | xrechnung-ubl | yes | yes | | xrechnung-cii | yes | no | | facturx-en16931 | yes | no | | peppol-bis-3 | yes | yes | Generation is UBL syntax only. There is no Factur-X or CII generation: those profiles return 400 unsupported_profile from /v1/generate. Do not describe Factur-X generation as available. ## MCP https://api.attestwire.com/mcp is a Model Context Protocol server over streamable HTTP. Stateless: POST only, no SSE stream, no session id. Protocol revisions 2024-11-05 through 2026-07-28 are negotiated; `server/discover` is implemented alongside `initialize`. Eight tools. FIVE ARE FREE AND NEED NO API KEY — connect with no credential and they work immediately: - explain_rule {rule_id} — what one rule (BR-DE-15, BR-CO-15, PEPPOL-EN16931-R010, …) requires, why, and how to fix it. 57 rules. Prefer this over validate_invoice when you already have a rule id: it is free. - check_vies_status {country?} — VIES availability, overall or per member state. - check_french_readiness {query} — French company lookup by SIREN/SIRET/name. Cannot prove platform registration, and says so. - list_approved_platforms {query?} — DGFiP-approved French platforms. - issue_api_key {email} — mint a free key. Returned ONCE, unrecoverable. Two require a key and cost 1 document each, the same as the HTTP endpoints: - validate_invoice {invoice} - generate_invoice {invoice} Authentication is an `Authorization: Bearer aw_live_...` header on the server in your MCP client config. It cannot be passed as a tool argument. Without one, the keyed tools return isError with the remedy named (call issue_api_key). Client config: claude mcp add --transport http attestwire https://api.attestwire.com/mcp \ --header "Authorization: Bearer aw_live_..." An npx stdio bridge exists for clients without remote-HTTP support: `npx -y @attestwire/mcp`, reading ATTESTWIRE_API_KEY from the environment. ## Related Attestwire properties - https://attestwire.com/rules/ — one page per EN 16931 / XRechnung / Peppol rule, with the exact error payload and a note on where our check diverges from the published rule. The `docsUrl` in every teaching error points here. - https://vies.attestwire.com — live VIES (EU VAT number validation) availability monitor: 28 member state endpoints, 5-minute polling, free badges, open JSON API at /api/status.json. VIES facts for integrators live in https://vies.attestwire.com/llms.txt. - https://check.attestwire.com — free lookup of a French company's registration in the DGFiP approved-platform directory. - https://attestwire.com/privacy — what we store.