← Docs / Guides
API conventions
Request shape, auth scopes, list queries, money, errors, idempotency, rate limits.
Request shape
Every operation is an RPC-style POST:
POST /v1/{module}/{resource}/{action}
Modules: reference, partners, catalog, sales, purchases, ledger, bank, declarations, files, webhooks, audit. Actions are verbs: create, get, update, delete, list, plus domain actions like issue, register, match, apply-advance, generate.
Authentication and scopes
Authorization: Bearer <api key>. Keys carry scopes checked per action:
{module}:read— get/list actions{module}:write— mutations{module}:*or*— wildcards
Missing scope → 403 forbidden. Missing/invalid key → 401 unauthorized.
List queries
All list actions share one contract:
{
"page": 1,
"pageSize": 50,
"sort": [{ "field": "createdAt", "dir": "desc" }],
"filter": [{ "field": "paymentStatus", "op": "eq", "value": "unpaid" }]
}
Filter operators: eq (equal), ne (not equal), contains (case-insensitive text match, text fields only), gte (greater than or equal), lte (less than or equal), in (any of a list of up to 100 values). Sortable and filterable fields are whitelisted per endpoint (see the OpenAPI spec); unknown fields return 400. Add totals: ["field", …] to the request to sum numeric fields over every matching row. Responses: { rows, total, page, pageSize, totals? }.
Money and dates
Monetary values are decimal strings with up to 4 decimal places ("121.0000") — never floats. VAT is computed per line with half-up rounding. Dates are YYYY-MM-DD; timestamps are ISO 8601 UTC.
Errors
One envelope everywhere:
{
"error": {
"code": "validation",
"message": "Request validation failed",
"requestId": "k1x…",
"key": "request_invalid",
"fieldErrors": { "lines.0.unitPriceExclVat": ["Invalid value"] }
}
}
Codes: validation (400/422, and 413 when an uploaded file or request body is too large), unauthorized (401), payment_required (402, the trial or prepaid credit has ended; the message says how to continue), forbidden (403), not_found (404), conflict (409), idempotency_key_reuse (422), idempotency_in_progress (409), rate_limited (429), internal (5xx). When the message is one of the fixed messages, key names it so a client can show its own translation, and params holds the values that go into it (for example { "period": "2026-09" } for a locked accounting period). Include the requestId when reporting issues.
Idempotency
Add Idempotency-Key: <any string ≤255 chars> to any mutating call:
- Retry with the same key and payload → the stored response is replayed byte-for-byte with
x-idempotent-replay: true. - Same key, different payload → 422
idempotency_key_reuse. - Concurrent duplicate while the first is in flight → 409
idempotency_in_progress. - Keys belong to the API key or user that sends them, within the company the call acts for, so two integrations of one company can use the same key. Calls made before a company exists, and
account/*calls, keep keys per user, so a repeatedaccount/companies/createcreates one company. - A stored response is returned only after the caller passes the same permission checks again (scope, role, company and billing status); otherwise the call gets the current error.
- Keys expire after 24 hours. Error responses (4xx) are replayed too; 5xx responses are not stored so retries re-execute.
Rate limits
300 requests/minute per API key (per presented credential; unauthenticated requests are limited per IP). On 429, honor the retry-after header. x-ratelimit-* headers report your budget.
Webhooks
Events are written to a transactional outbox in the same database transaction as the change — a rolled-back document never emits an event, and no event is lost. Deliveries:
POSTto your URL with the event JSONx-nordlet-timestamp: <unix seconds>, the time the delivery was signedx-nordlet-signature-v2: t=<timestamp>,v1=<hex HMAC-SHA256 of "<timestamp>.<raw body>">using your subscription secretx-nordlet-signature: sha256=<hex HMAC-SHA256 of the raw body>, kept unchanged for existing receiversx-nordlet-deliverycarries the delivery id, the same on every retry of one delivery- A non-2xx answer or a redirect is retried after 30 seconds, 2 minutes, 10 minutes, 30 minutes, 1 hour, 3 hours, 6 hours and 12 hours, about 24 hours in all, before the delivery is marked failed
- Each delivery is sent by one server at a time, so running several API instances does not send it twice
Verify x-nordlet-signature-v2 with a constant-time comparison before trusting the payload, and reject a delivery whose timestamp is more than 5 minutes away from your clock. The timestamp is part of the signed text, so a captured delivery cannot be replayed later with a fresh timestamp. Store the x-nordlet-delivery ids you have processed to ignore a repeat within that window. A failed delivery can be sent again with webhooks/deliveries/redeliver.
Audit
Every mutation is recorded (actor, action, entity, diff) and queryable via audit/list.
Accounting guarantees
- Journal postings are balanced — enforced by a deferred database trigger at commit time, not just application code.
- Document numbers are gapless per series/year; allocation is concurrency-safe.
- Locked accounting periods reject any posting dated inside them (409).
- Soft-deleted documents keep their audit trail; issued/registered documents cannot be deleted.