Nordlet

← 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 repeated account/companies/create creates 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:

  • POST to your URL with the event JSON
  • x-nordlet-timestamp: <unix seconds>, the time the delivery was signed
  • x-nordlet-signature-v2: t=<timestamp>,v1=<hex HMAC-SHA256 of "<timestamp>.<raw body>"> using your subscription secret
  • x-nordlet-signature: sha256=<hex HMAC-SHA256 of the raw body>, kept unchanged for existing receivers
  • x-nordlet-delivery carries 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.