Skip to content

REST API

Access updated

Try fixed samples without a key on the MCP setup page. Processing your own documents requires a key. New free subscriptions and anonymous trial keys are closed; existing keys retain their allowance.

Base URL: https://facturx.orvel.dev. Interactive OpenAPI UI at /api, machine-readable spec at /openapi.json.

Authentication

Endpoint Anonymous With API key
POST /v1/invoices/validate API key required counted against your quota
POST /v1/invoices/extract API key required counted against your quota
POST /v1/invoices/generate 401 required
POST /v1/invoices/embed 401 required
POST /v1/invoices/preview-xml 401 required
GET /v1/usage 401 required

Send Authorization: Bearer <key> or X-API-Key: <key>. Plans and keys.

Metered responses carry X-Plan, X-Usage-Used, X-Usage-Quota and X-Usage-Remaining.

GET /v1/demo

No key. Returns fixed sample downloads and published validation evidence; no custom document is processed.

POST /v1/trial-keys

Retired: 410 trial_issuance_closed, with demo and setup links. GET redirects to the MCP guide. Previously issued keys remain valid until their original expiry; their total quota does not renew.

Endpoints

POST /v1/invoices/generate

Body: {"invoice": Invoice, "options": GenerateOptions}. See Invoice fields for the Invoice object.

options:

Field Default Values
profile en16931 basicwl, en16931, extended, extended-ctc-fr (MINIMUM/BASIC are read and validated, not produced: the French reform requires at least EN 16931)
output facturx-pdf facturx-pdf (PDF/A-3 + CII), cii-xml, ubl-xml
language fr fr, en (visual PDF language)
check fr-ctc base (EN 16931 only), fr-ctc (EN 16931 + French BR-FR rules)
logo_url none HTTPS URL of a PNG/SVG logo shown in the PDF header
accent_color #1f3a5f Hex color of the PDF headings
footer_text none Free text in the PDF footer
party_check on for extended-ctc-fr, off otherwise true asks EU Verify before a file is emitted. false skips the check.

Response: the document as binary (application/pdf or application/xml) with a Content-Disposition filename. With Accept: application/json you get:

{
  "filename": "F-2026-0042-facturx.pdf",
  "media_type": "application/pdf",
  "profile": "en16931",
  "output": "facturx-pdf",
  "content_base64": "JVBERi0xLjcK...",
  "totals": {"line_total": "2739.70", "tax_exclusive": "2739.70", "tax_total": "539.28",
             "tax_inclusive": "3278.98", "amount_due": "3278.98",
             "vat_breakdown": [{"category": "S", "rate": "20", "taxable_amount": "2680.00", "tax_amount": "536.00"}]},
  "validation": {"valid": true, "checks_run": ["xsd", "schematron:base", "schematron:fr-ctc"], "findings": []},
  "evidence": [{"id": "evd_4f0c9a12b7e34d18", "role": "seller", "siren": "552081317", "vat": "FR03552081317", "verdict": "invoice_ready"}]
}

Extra headers: X-Facturx-Profile, X-Facturx-Output, X-Facturx-Amount-Due, X-Facturx-Warnings. When a party check ran: X-Facturx-Evidence-Id (one id, or several separated by commas) and, if a register was down, X-Facturx-Party-Check: upstream_unavailable.

The generated XML is always validated (XSD + schematron for the requested check) before it is returned. If a rule fails you get a 400 invalid_input with the findings, never a non-compliant file. If a party is not invoice-ready you get 400 party_not_ready and no file. See Invoice-ready parties.

Invoice-ready parties

party_check asks EU Verify whether the seller and the buyer can appear on the invoice. It runs before any PDF or XML is built.

  • It is on when profile is extended-ctc-fr and you omit party_check.
  • It is off for en16931, extended, and basicwl unless you set party_check to true.
  • Set party_check to false to skip it, including on extended-ctc-fr.

A French party (country FR, a SIREN or SIRET, or a VAT number starting with FR) must have a SIREN. VAT is checked, and derived from the SIREN when you omit it. The seller IBAN is checked when payment.iban is set. Address and EORI are checked only when you send them.

invoice_ready returns the file. Each pack in evidence can contain id (evd_ plus 16 hex characters), role, siren, siret, name, vat, vies_request_identifier, vies_name, iban, iban_checksum (mod-97), address, ban_id, eori, and verdict. The same id is written on the invoice as a note with subject code AAI (EU Verify evidence evd_…). Nothing is stored.

not_ready is HTTP 400. No document is produced.

{
  "code": "party_not_ready",
  "message": "A party is not invoice-ready. No document was produced.",
  "details": [{
    "source": "eu-verify",
    "severity": "fatal",
    "rule": "vat_not_registered",
    "message": "not registered",
    "location": null,
    "test": null,
    "json_pointer": "/invoice/buyer/vat"
  }],
  "evidence": []
}

details[].json_pointer is the field to fix: /invoice/seller/siren, /invoice/buyer/vat, /invoice/payment/iban, and the same shape for address and EORI. Closed reason codes: siren_missing, siren_checksum, siren_unknown, siren_closed, siren_siret_mismatch, vat_missing, vat_malformed, vat_not_registered, iban_invalid, address_invalid, address_unmatched, eori_malformed, eori_not_registered.

A register outage does not block the file. The document is returned and X-Facturx-Party-Check is upstream_unavailable. If EU Verify itself cannot be reached, that header is party check unavailable plus the error type, and the file is still returned. A definitive failure still blocks, even if another register is down at the same time. evidence on a party_not_ready response lists only the parties that passed.

On MCP, generate_invoice raises the same refusal as a tool error and includes the evidence id in the text summary when the file is emitted.

POST /v1/invoices/embed

multipart/form-data:

Part Required Description
pdf yes Your visual invoice PDF
xml one of Factur-X / CII XML file to embed
invoice_json one of Invoice JSON; the XML is generated from it
profile no Profile when generating from invoice_json (default en16931)
check no base or fr-ctc (default)
language no fr or en metadata language
party_check same rule as generate Applied only when invoice_json is sent. Raw XML is not party-checked.

Response: application/pdf, a PDF/A-3 file with the XML attached as factur-x.xml and the Factur-X XMP metadata. The XML is validated first; UBL cannot be embedded (Factur-X is CII only).

POST /v1/invoices/validate?check=fr-ctc

Send the file as multipart/form-data (file) or as the raw request body (Content-Type: application/pdf or application/xml). Accepts Factur-X / ZUGFeRD PDFs, CII XML and UBL 2.1 Invoice or CreditNote XML.

{
  "valid": true,
  "flavor": "factur-x",
  "profile": "en16931",
  "checks_run": ["pdf", "xsd", "schematron:base", "schematron:fr-ctc"],
  "checks_skipped": [],
  "error_count": 0,
  "warning_count": 1,
  "findings": [
    {"source": "schematron:base", "severity": "warning", "rule": "BR-CO-25", "message": "...", "location": "/rsm:...", "json_pointer": "/invoice/number"}
  ],
  "pdf": {"pages": 1, "attachments": ["factur-x.xml"], "pdfa": "3B", "facturx_xmp": "EN 16931"}
}
  • flavor: factur-x (CII), ubl-2.1-invoice, ubl-2.1-creditnote.
  • profile: detected from the XML (minimum, basicwl, basic, en16931, extended, extended-ctc-fr).
  • severity: error (fatal) or warning. valid is false when there is at least one error.
  • json_pointer: the invoice JSON field for that finding when it can be named (/invoice/seller/siren, /invoice/number, …). Empty when the rule does not map to one field. An agent can patch that field and call generate again.
  • pdf: only for PDF input. pdfa is read from the XMP (3B expected), facturx_xmp is the ConformanceLevel.

POST /v1/invoices/extract?include_xml=true

Same input as validate. Returns:

{
  "flavor": "factur-x",
  "profile": "en16931",
  "xml_filename": "factur-x.xml",
  "xml": "<?xml ...>",
  "fields": {
    "number": "F20260023", "type_code": "380", "issue_date": "2026-01-15", "due_date": "2026-02-14",
    "currency": "EUR",
    "seller": {"name": "...", "siren": "...", "vat_id": "FR...", "address": {...}},
    "buyer": {"name": "...", "siren": "...", "address": {...}},
    "totals": {"tax_exclusive": "...", "tax_total": "...", "tax_inclusive": "...", "amount_due": "..."},
    "vat_breakdown": [{"category": "S", "rate": "20", "taxable_amount": "...", "tax_amount": "..."}],
    "lines": [{"id": "1", "item_name": "...", "quantity": "1", "unit_price": "...", "net_amount": "...", "vat_rate": "20"}]
  }
}

POST /v1/invoices/preview-xml

Same body as generate. Returns the CII (or UBL) XML without the schematron pass. Cheap way to iterate on a field mapping; still validated against the XSD.

GET /v1/schema/invoice, GET /v1/schema/options

JSON Schema (draft 2020-12) of the Invoice and GenerateOptions objects. Use them for client generation or form validation.

GET /v1/usage

{"plan": "starter", "used": 412, "quota": 1000, "remaining": 588, "period": "2026-09"}

GET /v1/plans

Public, no key. Plan catalog with the self-service checkout links and the customer portal URL (what the pricing page buttons use).

{"currency": "EUR", "plans": [{"id": "starter", "name": "Starter", "price_eur_month": 29, "quota": 1000, "checkout_url": "https://facturx.orvel.dev/v1/checkout/starter"}, ...], "portal_url": "https://facturx.orvel.dev/docs/account/", "mcp_url": "https://facturx.orvel.dev/mcp", "billing": "stripe"}

GET /v1/checkout/{plan}

Public review page (HTTP 200), without creating a payment session. plan is starter, pro or scale. prepaid and the retired evaluation route redirect to the signed-in workspace. The retired free route redirects to the demo.

POST /v1/checkout/{plan}

Explicit purchase action; creates a session and returns HTTP 303 to Stripe Checkout. Cross-site browser submissions are rejected. Monthly plans are recurring subscriptions. Prepaid purchases require a verified account and are created through the workspace: EUR 10 for 100 operations, no expiry or automatic recharge. After payment Stripe redirects to Your API key with ?session_id=.

GET /v1/billing/session/{session_id}

Public. After a completed Checkout session, returns {api_key, plan, quota, customer_email}. The key is fx.<subscription_id>.<hmac> and works immediately on REST and MCP.

POST /v1/billing/portal

Requires a Stripe-issued key (Authorization: Bearer fx.…). Returns {url} to the Stripe customer portal (upgrade, cancel, invoices).

GET /health

{"status": "ok", "version": "0.1.9", "schematron": true}. schematron: false means the rule engine is restarting; validation calls return 503 until it is back (a few seconds).

Errors

All errors share one shape:

{"code": "invalid_input", "message": "XML does not pass EN 16931 business rules", "details": [ ... ]}
HTTP code Meaning
400 party_not_ready A seller or buyer failed the EU Verify check. details name the field. No document is returned.
400 invalid_input Bad document, or the generated/embedded XML fails a business rule (details = findings)
400 unsupported_profile Requested profile/output combination is not possible (e.g. UBL into a Factur-X PDF)
400 not_an_einvoice The PDF has no Factur-X attachment / the XML is neither CII nor UBL
401 api_key_required, invalid_api_key Missing or unknown key
402 quota_exceeded Monthly quota of the plan reached
402 checkout_incomplete Stripe Checkout session is not paid yet
503 billing_unavailable Checkout secrets are not configured
413 too_large Body above 15 MB
422 validation_error Request JSON does not match the schema (details[].loc points at the field)
429 rate_limited Anonymous rate limit hit; add a key
500 render_error PDF rendering failed (report it, include the request id from logs)
503 schematron_unavailable Rule engine warming up; retry after a few seconds

Limits

  • Body size 15 MB. Typical Factur-X PDFs are under 500 KB.
  • Generation takes 0.5 to 2 s (PDF rendering + schematron). Validation 0.2 to 1 s.
  • No document is stored. Logs contain the invoice number and rule ids, never amounts or party data.