Conventions
| Topic | Rule |
|---|---|
| Amounts | Always integer øre (125000 = DKK 1,250.00). Never floats. |
| Times | UTC, ISO-8601 (2026-07-27T09:00:00Z). |
| Language | Field names and error codes are English and never translated. Documents we send (invoices, reminders) follow the debtor’s language. |
| Success envelope | { "data": … }, optionally { "data": …, "meta": … } |
| Error envelope | { "error": { "code": "…", "requestId": "…" } } — validation errors (400/422) add details: [{ "field": "…", "message": "…" }] so you can see exactly which field failed. Auth errors are deliberately detail-free. |
| Request id | Every response carries X-Request-Id. Include it when contacting support. |
| Versioning | Path-versioned (/api/v1). Additive changes only; breaking changes get a new version with a published deprecation window. |
Error codes
Section titled “Error codes”| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_request | Body failed validation |
| 400 | invalid_idempotency_key | Missing/malformed Idempotency-Key header (required on all POST/PATCH/PUT/DELETE) |
| 400 | invalid_cursor | Tampered or expired pagination cursor |
| 401 | invalid_api_key | Missing, malformed, unknown, expired or revoked key (deliberately indistinguishable) |
| 403 | insufficient_scope | Key lacks the scope the route requires |
| 403 | api_not_included | Your plan does not include API access |
| 404 | case_not_found / customer_not_found / invoice_not_found / … | Unknown id — or another tenant’s id (same neutral answer) |
| 409 | idempotency_conflict | Same idempotency key, different payload |
| 409 | idempotency_in_progress | Same key is being processed right now — retry after Retry-After |
| 413 | payload_too_large | Body over the route’s size limit |
| 422 | case_rejected | Payload valid but rejected by domain rules |
| 422 | invoice_not_editable / invoice_already_issued / invoice_not_issued / customer_email_missing | Lifecycle rule violated (see Invoices) |
| 422 | external_reference_in_use / sku_in_use / account_number_in_use | Unique value already taken within your organisation |
| 429 | rate_limited | Budget exhausted — honour Retry-After |
| 503 | temporarily_unavailable / audit_unavailable | Transient; retry with backoff |
Rate limits
Section titled “Rate limits”Budgets are per organisation, split by load class so a polling-heavy reader can never starve your writes:
| Class | Applies to | Budget |
|---|---|---|
read | GET/HEAD | 1200/min |
write | POST/PATCH/DELETE | 300/min |
document | PDF/document generation routes | 120/min |
Every response carries RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset; a 429
adds Retry-After (seconds). Back off exponentially and respect Retry-After.
Idempotency
Section titled “Idempotency”Every POST/PATCH/PUT/DELETE requires an Idempotency-Key header (8–128 chars,
A-Za-z0-9._:-). Generate one key per logical operation (e.g. your order id), not per
HTTP attempt. The contract:
- Retrying with the same key + same payload replays the original response unchanged
(marked with the
Idempotency-Replay: trueheader) — safe against network failures. - The same key with a different payload returns
409 idempotency_conflict. - Two concurrent requests with the same key: one wins, the other gets
409 idempotency_in_progress— waitRetry-Afterseconds and retry. - Responses are stored for 24 hours. Server errors (5xx) are never stored — retry immediately with the same key.