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
profileisextended-ctc-frand you omitparty_check. - It is off for
en16931,extended, andbasicwlunless you setparty_checktotrue. - Set
party_checktofalseto skip it, including onextended-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) orwarning.validisfalsewhen 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.pdfais read from the XMP (3Bexpected),facturx_xmpis 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¶
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.