Skip to content
Zugflow

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_XXXXXXXXXXXXXXXXXXXXXXXX

Endpoints

  • POST/api/v1/documents

    Compile 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/documents

    List 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}/pdf

    Download the hybrid PDF/A-3 file.

    Served inline by default; add ?download to force a save dialog.

  • GET/api/v1/documents/{id}/xml

    Download the embedded CII XML on its own.

    For pushing the structured invoice to Peppol or Chorus Pro without the wrapper.

  • POST/api/v1/validate

    Check 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 rate

    Requires a rate above zero.

  • ZZero rated

    Rate must be 0.

  • EExempt

    Rate must be 0; an exemption reason is added automatically.

  • AEReverse charge

    VAT payable by the recipient. Rate must be 0.

  • KIntra-Community supply

    Art. 138 exemption. Rate must be 0.

  • GExport outside the EU

    Art. 146 exemption. Rate must be 0.

  • ONot subject to VAT

    Rate 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.

StatusCodeMeaning
400invalid_payloadThe body failed schema validation. `details.issues` names each field.
401missing_api_key / invalid_api_keyNo bearer token, or one we do not recognise.
402quota_exceededThe monthly document limit is reached. Nothing was generated.
403profile_not_in_planThe requested conformance profile is above your plan.
409duplicate_numberThat invoice number already exists for your organisation.
413payload_too_largeThe body exceeds 2 MB.
422totals_mismatchTotals you supplied disagree with the computed ones.
422business_rule_violationAn EN 16931 rule failed; `details.rule` names it.
422xsd_validation_failedThe generated XML did not satisfy the Factur-X schema.