Skip to content

REST API

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 20 req/min per IP counted against your quota, no rate limit
POST /v1/invoices/extract 20 req/min per IP counted against your quota, no rate limit
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 the key as Authorization: Bearer <key> (or X-API-Key: <key>). Keys are issued at the end of Stripe Checkout, see Pricing.

Every metered response carries X-Plan, X-Usage-Used, X-Usage-Quota and X-Usage-Remaining.

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

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": []}
}

Extra headers: X-Facturx-Profile, X-Facturx-Output, X-Facturx-Amount-Due, X-Facturx-Warnings.

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.

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

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:..."}
  ],
  "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.
  • 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": "free", "name": "Free", "price_eur_month": 0, "quota": 50, "checkout_url": "https://facturx.orvel.dev/v1/checkout/free"}, ...], "portal_url": "https://facturx.orvel.dev/docs/account/", "mcp_url": "https://facturx.orvel.dev/mcp", "billing": "stripe"}

GET /v1/checkout/{plan}

Public. plan is free, starter, pro or scale. HTTP 303 to Stripe Checkout. 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.0", "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 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.