Full API reference: browse the rendered reference · openapi.json · error catalog
Get started in four steps
- Request a key. Email us
with your product and rough invoice volume. We send a key shaped like
fkt_live_<id>_<secret>over a private channel. It is shown once — store it as a secret. - Set it as an environment variable so it never lands in source control:
export FAKTURATE_API_KEY="fkt_live_…"
- Make your first call. POST a JSON invoice to
/v1/invoices(see below) and save the returned UBL XML. - Handle the response. A
200returns XML; anything else is a problem+json object you can branch on bytype.
Authentication
Send your key as a bearer token on every request. Keys are server-to-server only — browser CORS is intentionally disabled because a key cannot be kept secret in frontend code.
Authorization: Bearer fkt_live_<id>_<secret>
One endpoint
POST https://api.fakturate.com/v1/invoices Authorization: Bearer fkt_live_<id>_<secret> Content-Type: application/json Idempotency-Key: 2026-042 # optional 200 OK Content-Type: application/xml X-Fakturate-Api-Version: 1.0.0-experimental X-Fakturate-Validation: input-semantic-en16931-subset X-Fakturate-Schematron: not-run
Example request
curl https://api.fakturate.com/v1/invoices \
--header "Authorization: Bearer $FAKTURATE_API_KEY" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: 2026-042" \
--output invoice.xml \
--data '{
"profile": "peppol_bis_3",
"invoice_number": "2026-042",
"issue_date": "2026-06-15",
"due_date": "2026-07-15",
"currency": "EUR",
"language": "en",
"supplier": {
"name": "Example Supplier BV",
"tax_id": "BE0123456749",
"address": { "street": "Example Street 12", "postal_code": "2000", "city": "Antwerp", "country_code": "BE" },
"email": "billing@example.test",
"phone": "+32 470 000000",
"iban": "BE71096123456769",
"contact_name": "Example Contact"
},
"customer": {
"name": "Example Customer BV",
"tax_id": "BE0987654394",
"address": { "street": "Customer Street 1", "postal_code": "1000", "city": "Brussels", "country_code": "BE" },
"email": "accounts@example.test",
"reference": "PO-4711"
},
"vat_treatment": "standard",
"payment_terms": "Pay within 30 days.",
"lines": [
{ "description": "Design services", "quantity": 1.5, "unit_code": "HUR", "unit_price": 100, "vat_rate": 21 }
]
}'
// Node 18+ (built-in fetch). No SDK needed — it is one HTTP call.
import { writeFile } from "node:fs/promises";
const invoice = {
profile: "peppol_bis_3",
invoice_number: "2026-042",
issue_date: "2026-06-15",
due_date: "2026-07-15",
currency: "EUR",
language: "en",
supplier: {
name: "Example Supplier BV",
tax_id: "BE0123456749",
address: { street: "Example Street 12", postal_code: "2000", city: "Antwerp", country_code: "BE" },
email: "billing@example.test",
phone: "+32 470 000000",
iban: "BE71096123456769",
contact_name: "Example Contact",
},
customer: {
name: "Example Customer BV",
tax_id: "BE0987654394",
address: { street: "Customer Street 1", postal_code: "1000", city: "Brussels", country_code: "BE" },
email: "accounts@example.test",
reference: "PO-4711",
},
vat_treatment: "standard",
payment_terms: "Pay within 30 days.",
lines: [
{ description: "Design services", quantity: 1.5, unit_code: "HUR", unit_price: 100, vat_rate: 21 },
],
};
const res = await fetch("https://api.fakturate.com/v1/invoices", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FAKTURATE_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": invoice.invoice_number,
},
body: JSON.stringify(invoice),
});
if (!res.ok) {
const problem = await res.json(); // application/problem+json
throw new Error(`${problem.title}: ${problem.detail}`);
}
await writeFile("invoice.xml", Buffer.from(await res.arrayBuffer()));
# pip install requests — the API is plain HTTP, no SDK required.
import os
import requests
invoice = {
"profile": "peppol_bis_3",
"invoice_number": "2026-042",
"issue_date": "2026-06-15",
"due_date": "2026-07-15",
"currency": "EUR",
"language": "en",
"supplier": {
"name": "Example Supplier BV",
"tax_id": "BE0123456749",
"address": {"street": "Example Street 12", "postal_code": "2000", "city": "Antwerp", "country_code": "BE"},
"email": "billing@example.test",
"phone": "+32 470 000000",
"iban": "BE71096123456769",
"contact_name": "Example Contact",
},
"customer": {
"name": "Example Customer BV",
"tax_id": "BE0987654394",
"address": {"street": "Customer Street 1", "postal_code": "1000", "city": "Brussels", "country_code": "BE"},
"email": "accounts@example.test",
"reference": "PO-4711",
},
"vat_treatment": "standard",
"payment_terms": "Pay within 30 days.",
"lines": [
{"description": "Design services", "quantity": 1.5, "unit_code": "HUR", "unit_price": 100, "vat_rate": 21},
],
}
res = requests.post(
"https://api.fakturate.com/v1/invoices",
headers={
"Authorization": f"Bearer {os.environ['FAKTURATE_API_KEY']}",
"Content-Type": "application/json",
"Idempotency-Key": invoice["invoice_number"],
},
json=invoice,
)
if res.status_code != 200:
problem = res.json() # application/problem+json
raise RuntimeError(f"{problem['title']}: {problem['detail']}")
with open("invoice.xml", "wb") as handle:
handle.write(res.content)
There is no SDK to install: the API is a single HTTP call, so the standard HTTP client in any language works. The complete machine-readable contract powers the rendered reference and is served at api.fakturate.com/openapi.json.
Idempotency
Invoice generation is a pure, side-effect-free function of the request body — nothing is stored, so
resending the same request always yields the same XML. Retrying after a network timeout is therefore safe.
Pass an optional Idempotency-Key header (1–255 printable ASCII characters) to correlate
retries in your own logs; we echo it back on success. An invalid key returns
invalid-idempotency-key.
Versioning
The major version is pinned in the URL path (/v1/). A /v1/ route never changes
its request or response contract in a breaking way; breaking changes would ship under a new path such as
/v2/. Backward-compatible additions (new optional fields or enum values) may appear within
/v1/, so write your client to ignore unknown fields. Every response advertises the current
revision in the X-Fakturate-Api-Version header.
Errors
Errors use the RFC 9457
application/problem+json format. Branch on the type URI, which dereferences to
human-readable docs. Validation failures additionally carry an errors[] array of
{ path, code, message } objects pointing at each offending field.
{
"type": "https://api.fakturate.com/problems/invalid-invoice",
"title": "Invalid Invoice",
"status": 422,
"detail": "The invoice request failed validation.",
"errors": [
{ "path": "$.lines[0].vat_rate", "code": "out_of_range", "message": "Must be at most 100." }
]
}
| HTTP | type | When |
|---|---|---|
| 401 | unauthorized | Missing, unknown, or revoked API key. |
| 415 | unsupported-media-type | Content-Type is not application/json. |
| 400 | malformed-json | Body is not valid JSON. |
| 400 | invalid-idempotency-key | Idempotency-Key is malformed. |
| 413 | payload-too-large | Body exceeds 256 KiB. |
| 422 | invalid-invoice | Input or semantic validation failed; see errors[]. |
| 429 | rate-limit-exceeded | Over 60 requests/minute; honour Retry-After. |
| 500 | internal-error | Generation failed; no invoice data leaks or is retained. |
The complete list, including every field-level code, lives at
api.fakturate.com/problems.
Privacy boundary
API requests are encrypted in transit with HTTPS. Fakturate briefly processes the invoice JSON and generated XML in the API Worker's memory, then returns the result. Invoice contents are not retained, cached, written to logs, or sent to another service.
We retain only bounded operational measurements: a pseudonymous API-key ID, route, requested profile, outcome status and latency bucket. These measurements contain no names, addresses, VAT numbers, invoice numbers, amounts, request bodies, generated XML, authorization headers, raw IP addresses or user agents. See the privacy policy.
What V1 validates
Every request is checked in three layers before any XML is generated, reflected in the
X-Fakturate-Validation header (input-semantic-en16931-subset):
- Input. Strict JSON shape, types, ranges, lengths, and code lists (currency, country, unit of measure, VAT treatment). Unknown properties are rejected.
- Semantic. Profile and VAT-treatment requirements — e.g. reverse charge needs the buyer's VAT number, XRechnung needs a buyer reference and seller contact.
- EN 16931 business-rule subset. The input-dependent rules that most often cause downstream rejection: per-country VAT identifier structure (VIES), IBAN check digits (ISO 13616), and intra-EU VAT treatments restricted to EU parties.
The generation engine itself is regression-tested offline against the official EN 16931, Peppol and KoSIT rule sets, and satisfies the structural and calculation rules by construction.
V1 does not run the official XSD or Schematron rule sets separately for each
request (X-Fakturate-Schematron: not-run). The EN 16931 subset above is a
fast, high-value approximation, not a replacement. Full per-request UBL XSD and EN 16931 / Peppol
/ XRechnung Schematron validation is planned for V2.
Current limits
- Peppol BIS Billing 3.0 and XRechnung 3.0 invoice generation only.
- Maximum request body: 256 KiB; maximum invoice lines: 100.
- Invite-only API keys; 60 requests per minute per key.
- No invoice storage, Peppol delivery, webhooks, credit notes or SLA.
- Server-to-server use only. Browser CORS access is intentionally disabled because API keys cannot be kept secret in frontend code.