Errors
Every error response is a RFC 9457 Problem Details document at Content-Type: application/problem+json. The HTTP status code carries the broad category; the code field carries the specific, machine-readable reason.
Status codes
| Code | When it happens |
|---|---|
400 | Malformed request - bad JSON, missing required field, unknown enum value |
401 | Missing or invalid Authorization: Bearer … header |
403 | Caller lacks the permission this endpoint requires |
404 | Resource doesn't exist (or belongs to a different organization) |
409 | Conflict - duplicate, version mismatch, invalid state transition |
422 | Validation passed at the schema layer but business rules rejected the request |
429 | Per-organization rate limit hit - see headers + retry below |
500 | Internal server error - open an issue with the request_id |
503 | A downstream dependency (gateway, Keycloak, e-invoice service) is unavailable |
Problem document shape
Code
| Field | What it carries |
|---|---|
type | URI identifying the error class. Stable; safe to use for documentation links. |
title | One-line human summary. |
status | The HTTP status code, repeated in the body (RFC 9457 requirement). |
detail | Free-form explanation of this occurrence - usually safe to surface to end users. |
instance | The URI of the request that produced the error. |
code | Switch on this. Short SCREAMING_SNAKE identifier, stable across versions. |
request_id | Server-side correlation ID - include it when reporting an issue. |
Some errors carry extra top-level fields (RFC 9457 §3.2 extension members). For example, a not-found error includes entity_type and entity_id; a version-conflict error includes expected_version and current_version. Treat unknown fields as ignorable.
Always switch on
code, not ontitleordetail. Thecodeis part of the API contract; the prose can change without notice.
Validation errors
Field-level validation errors carry a fields array so a UI can highlight every bad field at once:
Code
Each entry carries field, code, and message. Some validators (notably the Keycloak passthrough on identity flows) also include a params array carrying positional arguments for i18n template substitution - your localised UI can render the message from code + params instead of trusting the English message.
Common error codes
The full catalog lives in internal/domain/error_codes_*.go and shows up per-endpoint in the API reference. Generic ones you'll see across most endpoints:
| Code | Status | Meaning |
|---|---|---|
VALIDATION | 400 | Schema-level field validation failed (violations array) |
UNAUTHORIZED | 401 | Missing / invalid bearer token |
FORBIDDEN | 403 | Permission missing - see Roles |
NOT_FOUND | 404 | Entity ID doesn't exist in this org |
CONFLICT | 409 | Resource is in a state that blocks the operation, optimistic-concurrency mismatch, or an Idempotency-Key collision (the detail text disambiguates) |
DUPLICATE | 409 | Natural key collision (e.g. an existing email on customer create) |
INSUFFICIENT_BALANCE | 422 | Wallet debit / hold would exceed the available amount |
RATE_LIMIT | 429 | Per-organization request budget exhausted |
Domain-specific codes (SUBSCRIPTION_PAUSED, PAYMENT_METHOD_NOT_FOUND, PROMOTION_BUDGET_EXHAUSTED, etc.) are documented on the relevant endpoint in the API reference.
Rate limiting
Limits are enforced per organization, separately for reads (GET/HEAD/OPTIONS) and writes (POST/PUT/PATCH/DELETE). When the budget is exhausted you get a 429 with the standard problem document plus headers:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed in the current window |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
Retry-After | Seconds to wait before retrying (always present on 429) |
Use exponential backoff with jitter when retrying. The window is one minute.
Idempotency
POST requests must include an Idempotency-Key header - Kontorion rejects POSTs without one. The key is a free-form string (use a UUID or your own request ID); it lives for 24 hours per organization.
Code
Behavior:
- Same key, same body, retry within 24h → the original response is replayed (status code + body), nothing is re-executed.
- Same key, different body →
409 CONFLICTwithdetail: "Idempotency-Key was already used with a different request body."This catches client-side bugs where the key wasn't regenerated for a new request. - Same key, request still in-flight →
409 CONFLICTwithdetail: "A request with this Idempotency-Key is already being processed."Wait and retry. - POST without an
Idempotency-Keyheader at all →400 VALIDATIONwith the missing header infields[].
Body equivalence is checked via SHA-256 of the request body, so trivial whitespace changes count as a different body. Re-encode deterministically.
Keys are scoped per organization. Two different orgs can use the same string without colliding.
Related
- Authentication - bearer token + sandbox/test-clock setup
- Pagination - cursor shape returned on list endpoints
- API reference - per-endpoint code lists and request schemas