API reference
Submit invoice data, receive a compliant Factur-X PDF or XML.
Authentication
Every protected endpoint requires an API key. Keys are minted by an administrator
(CLI php bin/users.php mint <user_id>, or via /admin)
and look like inv_…. The plaintext value is shown once at creation.
Send the key in either of two headers:
Authorization: Bearer inv_xxxxxxxxxxxx
X-Api-Key: inv_xxxxxxxxxxxx
The browser form on / also accepts an API key (stored in localStorage if you tick the box) and sends it as Authorization: Bearer ….
POST /invoice
Builds the CII XML, optionally wraps it as a PDF/A-3, and returns the file.
Query parameters
| Name | Values | Default | Notes |
|---|---|---|---|
profile | basicwl, basic, en16931, extended | basic | MINIMUM is intentionally rejected (structural constraints). |
format | pdf, xml | derived from Accept, else pdf | Overrides the Accept header. |
Request bodies
| Content-Type | Body |
|---|---|
application/json | The invoice as a JSON object (see /example.json). |
text/csv | A sectioned CSV (see /example.csv). |
multipart/form-data | File field file with a .csv / .json / .xlsx upload; or text fields json / csv. |
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet | Reserved; currently returns 501. |
Responses
| Status | Content-Type | Body |
|---|---|---|
| 200 | application/pdf | PDF/A-3 with embedded CII XML. Content-Disposition: attachment. |
| 200 | application/xml | CII XML (already validated against the official XSD). |
| 4xx / 5xx | application/json | {"error": "..."} — see Errors. |
Input schemas
JSON
The JSON body is the invoice object passed straight into the builder. Download /example.json for a complete sample. Top-level fields:
{
"number": "INV-2026-0042", // required
"issue_date": "2026-05-05", // required (YYYY-MM-DD or YYYYMMDD)
"due_date": "2026-06-04",
"currency": "EUR",
"type_code": "380", // UNTDID 1001; 380 = commercial invoice
"buyer_reference": "PO-9981",
"order_reference": "CMD-77",
"payment_terms": "Paiement à 30 jours net.",
"prepaid": 0,
"seller": { /* party */ }, // required
"buyer": { /* party */ }, // required
"lines": [
{
"name": "...", // required
"description": "...", // only kept in EN16931/EXTENDED
"quantity": 1, // required
"unit_price": 1800.00, // required
"unit_code": "C62", // UN/CEFACT, default C62 (each)
"vat_rate": 20.0, // required
"vat_category": "S" // UNTDID 5305, default S
}
],
"notes": [
"Free-form note",
{ "subject_code": "PMT", "content": "..." } // UNTDID 4451 SubjectCode
],
"payment_means": { "type_code": "30", "iban": "FR76…" }
}
Party shape:
{
"name": "Atelier Lumen SARL", // required
"vat_id": "FR43123456789",
"legal_id": "123456789",
"legal_id_scheme": "0002", // 0002 = SIRET, 0009 = SIREN
"electronic_address": "facturation@atelier-lumen.fr",
"electronic_address_scheme": "EM", // EM = email; 0088 = GLN; …
"address": {
"line1": "12 rue des Tisserands",
"line2": null,
"postcode": "75011",
"city": "Paris",
"country": "FR"
}
}
Sectioned CSV
One file = one invoice. Lines starting with # open a section. Two shapes:
Key/value sections (header, seller, buyer, payment_means): second row is the literal header field,value, then key,value pairs. Dotted keys (e.g. address.line1) build nested objects.
Tabular sections (lines, notes): second row is the column header, each subsequent row is one entity. Empty cells are dropped.
#header
field,value
number,INV-2026-0042
issue_date,2026-05-05
currency,EUR
#seller
field,value
name,Atelier Lumen SARL
vat_id,FR43123456789
address.line1,12 rue des Tisserands
address.postcode,75011
address.city,Paris
address.country,FR
#buyer
field,value
name,Maison Dubois SAS
...
#lines
name,description,quantity,unit_price,unit_code,vat_rate,vat_category
Audit ergonomique,Audit UX,1,1800.00,C62,20.0,S
Atelier de design,,4,650.00,HUR,20.0,S
#notes
subject_code,content
,Merci pour votre confiance.
PMT,"Tout retard de paiement..."
#payment_means
field,value
type_code,30
iban,FR7630006000011234567890189
Unknown sections produce a 400. Download /example.csv for the canonical template.
Profiles
| profile | URN | Notes |
|---|---|---|
basicwl | urn:factur-x.eu:1p0:basicwl | Header-only ("Without Lines"). Useful for summary invoices. |
basic | urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic | Default. Line items, per-rate VAT, no product descriptions. |
en16931 | urn:cen.eu:en16931:2017 | Full EN 16931. Line descriptions allowed. |
extended | urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended | Superset of EN 16931 with additional fields. |
| — | Returns 400 — has structural constraints that need a separate code path. |
Errors
| Status | Meaning |
|---|---|
| 400 | Bad input: missing field, malformed JSON, unknown CSV section, unknown profile. |
| 401 | Missing or invalid API key. WWW-Authenticate: Bearer realm="invox". |
| 403 | Caller is authenticated but not authorized (e.g. non-admin hitting /admin). |
| 404 | No such route. |
| 405 | Method not allowed. |
| 415 | Unsupported Content-Type / file format. |
| 501 | Format is recognized but not implemented yet (e.g. XLSX upload). |
| 500 | Unexpected server error. Check error_log for the trace. |
Error bodies are JSON: {"error": "human-readable message"}.
curl examples
JSON → PDF
curl -sS -X POST http://127.0.0.1:8000/invoice \
-H "Authorization: Bearer inv_..." \
-H "Content-Type: application/json" \
-H "Accept: application/pdf" \
--data @invoice.json \
-o invoice.pdf
JSON → XML
curl -sS -X POST 'http://127.0.0.1:8000/invoice?format=xml' \
-H "Authorization: Bearer inv_..." \
-H "Content-Type: application/json" \
--data @invoice.json \
-o invoice.xml
CSV upload → PDF
curl -sS -X POST 'http://127.0.0.1:8000/invoice?profile=en16931&format=pdf' \
-H "X-Api-Key: inv_..." \
-F "file=@invoice.csv;type=text/csv" \
-o invoice.pdf
Raw CSV body → PDF
curl -sS -X POST http://127.0.0.1:8000/invoice \
-H "Authorization: Bearer inv_..." \
-H "Content-Type: text/csv" \
--data-binary @invoice.csv \
-o invoice.pdf