API reference
One endpoint does the work. The rest is bookkeeping.
Every request is authenticated with a bearer API key. All amounts are decimal strings, never floats, and all dates are ISO calendar dates.
Authentication
Create keys in the dashboard. A key is shown once and stored only as a SHA-256 digest, so a lost key is replaced rather than recovered. Test and live keys carry the environment in the prefix so one pasted into the wrong config is obvious.
Authorization: Bearer zf_live_XXXXXXXXXXXXXXXXXXXXXXXXEndpoints
- POST
/api/v1/documentsCompile an invoice into a Factur-X PDF/A-3 document.
Counts one document against the monthly quota. Returns 201 with the document and your updated usage. Accepts ?profile= and ?locale=de to override the defaults.
- GET
/api/v1/documentsList documents, newest first.
Cursor paginated with ?limit= (max 100) and ?cursor=. Filter with ?status=.
- GET
/api/v1/documents/{id}Fetch one document's metadata.
Scoped to your organisation; an id you do not own returns 404, not 403.
- GET
/api/v1/documents/{id}/pdfDownload the hybrid PDF/A-3 file.
Served inline by default; add ?download to force a save dialog.
- GET
/api/v1/documents/{id}/xmlDownload the embedded CII XML on its own.
For pushing the structured invoice to Peppol or Chorus Pro without the wrapper.
- POST
/api/v1/validateCheck a payload against EN 16931 without generating anything.
Free and unmetered — run it in CI. Returns 200 with valid:true or valid:false plus one message per violation.
Totals are verified, not trusted
You may send a totals block. If you do, every field in it is compared against what the engine computes and a disagreement is a 422 naming each mismatch. It is never silently overwritten: substituting our number would put a figure on a legal document that your own ledger does not have.
{
"error": {
"code": "totals_mismatch",
"message": "Supplied totals do not match the computed totals",
"details": [
{ "field": "grandTotal", "supplied": "120.00", "computed": "119.00" }
]
}
}VAT categories
UNTDID 5305 codes. The category and the rate must agree: a zero-rated category with a non-zero rate is rejected rather than quietly reconciled.
SStandard rateRequires a rate above zero.
ZZero ratedRate must be 0.
EExemptRate must be 0; an exemption reason is added automatically.
AEReverse chargeVAT payable by the recipient. Rate must be 0.
KIntra-Community supplyArt. 138 exemption. Rate must be 0.
GExport outside the EUArt. 146 exemption. Rate must be 0.
ONot subject to VATRate must be 0.
Webhook ingestion
Create an endpoint in the dashboard and you get a URL and a signing secret. We verify the signature over the raw request body before parsing it, and record the provider’s event id under a unique constraint — so a redelivery returns the original document rather than issuing a second invoice number.
POST https://zugflow.altixcode.com/api/webhooks/stripe/{slug}
POST https://zugflow.altixcode.com/api/webhooks/shopify/{slug}
POST https://zugflow.altixcode.com/api/webhooks/generic/{slug}
Stripe Stripe-Signature: t=...,v1=... (300s tolerance)
Shopify X-Shopify-Hmac-Sha256: <base64>
Generic X-Signature: sha256=<hex>A payload we can never process returns 200 with an ignored status rather than a 5xx: asking a provider to retry an event that will fail identically every time wastes their retry budget and yours.
Errors
Every failure is { error: { code, message, details? } } with a stable code, so you can branch on the reason rather than on prose.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_payload | The body failed schema validation. `details.issues` names each field. |
| 401 | missing_api_key / invalid_api_key | No bearer token, or one we do not recognise. |
| 402 | quota_exceeded | The monthly document limit is reached. Nothing was generated. |
| 403 | profile_not_in_plan | The requested conformance profile is above your plan. |
| 409 | duplicate_number | That invoice number already exists for your organisation. |
| 413 | payload_too_large | The body exceeds 2 MB. |
| 422 | totals_mismatch | Totals you supplied disagree with the computed ones. |
| 422 | business_rule_violation | An EN 16931 rule failed; `details.rule` names it. |
| 422 | xsd_validation_failed | The generated XML did not satisfy the Factur-X schema. |