Skip to main content

Authentication

Every request to Quetzal Invoicing carries four pieces of identity. Three are headers, one is the body. Get any wrong and the request fails closed.

Required headers

HeaderRequired onWhat it does
Authorization: Bearer <token>All endpointsOAuth2 / JWT access token. Carries the caller identity, the tenant the caller can act for, and the scopes they hold.
X-Tenant-Id: <slug>All endpointsExplicit tenant context for the request. Must match the tenant claim in your token. Sent separately so multi-tenant tokens (rare) can still be unambiguous per request.
Idempotency-Key: <uuid>POST, PATCHA UUID you generate. Quetzal stores the request fingerprint under this key for 24h. Retrying the same (method, path, key) returns the original response.
X-Flow-Id: <id> (optional)AllA correlation ID. If you send one, Quetzal logs it on every event for that request — useful for tracing across systems.

Token scopes

The bearer token carries scopes. Each endpoint requires one or more of:

ScopeGrants
invoicing.document.readGET /v1/documents, GET /v1/documents/{id}, GET /v1/documents/{id}/events, GET /v1/documents/{id}/evidence
invoicing.document.writePOST /v1/documents, PATCH /v1/documents/{id}, POST /v1/documents/{id}/cancel, POST /v1/documents/{id}/void
invoicing.party.read / .writeThe corresponding operations under /v1/parties
invoicing.issuer_profile.read / .writeThe corresponding operations under /v1/issuer_profiles
invoicing.webhook.read / .writeThe corresponding operations under /v1/webhook_subscriptions

A token can hold multiple scopes. Ask for the narrowest set you need.

How to get a token

Quetzal does not issue tokens itself — it relies on your organization's identity provider. Talk to your Quetzal admin to:

  1. Register an OAuth2 client (or service account) for your integration.
  2. Get a client_id and client_secret.
  3. Request scopes appropriate to your workload.

The exact provider endpoint and token format depend on your deployment. The runtime contract is fixed though: any RFC 8725-compliant JWT with the right scopes and a tenant claim will validate.

Why Idempotency-Key is mandatory on writes

Network retries, queue redelivery, worker restarts — every production system retries. If retries each created a new document, you'd end up with duplicate fiscal emissions, which means duplicate tax filings, which means an audit conversation you don't want.

Idempotency-Key fixes this. Generate a UUID per intent (one document you want to emit), include it on the request, and:

  • The first request that arrives wins. Quetzal creates the document and remembers the key.
  • Any later request with the same key — even with a different body — returns the original document. The body is not re-processed.
  • The key is scoped to your tenant + the endpoint. Sending the same key on POST /v1/parties does not collide with one on POST /v1/documents.
  • Keys are remembered for 24 hours. After that the slot is free.

Always generate a fresh UUID per new document. Reusing a key across distinct documents will silently return the wrong one.

Common authentication errors

StatusWhenWhat to check
401 UnauthorizedNo Authorization header, or token is malformed / expired / signed by the wrong issuer.The token's exp claim. The signing key on Quetzal's side.
403 ForbiddenToken is valid but doesn't carry the scope this endpoint needs.Compare your token's scope claim with the scope table above.
403 Forbidden (tenant mismatch)X-Tenant-Id doesn't match the token's tenant claim.The header and the claim must be the same tenant slug.
409 ConflictThe Idempotency-Key is reused with a different request body.Either reuse the key with the exact same body (intended replay), or generate a new UUID.
422 Unprocessable EntityIdempotency-Key is not a valid UUID.Generate one with uuidgen, crypto.randomUUID(), etc.

All error responses are RFC 7807 Problem JSON — application/problem+json with a machine-readable type, a human title, a detail, and an instance.

Putting it together

A complete POST looks like this:

POST /v1/documents HTTP/1.1
Host: quetzal-api.mattilda.io
Authorization: Bearer eyJhbGciOiJIUzI1NiI...
X-Tenant-Id: mattilda
Idempotency-Key: 9f8a1c4e-2e6a-4f9c-b3a5-1d4e5f6a7b8c
X-Flow-Id: req-0193a8c2-f4d1
Content-Type: application/json

{ ... }

Get this right once in your HTTP client setup; from then on it's invisible.

Next

  • Quickstart — try a complete request end-to-end.
  • API Reference — every endpoint with its required scopes.
  • Principles — why the contract looks the way it does.