This document describes how clients should use the Saldra HTTP API and how we extend it without surprise breaking changes. Product routes live under /v1; a future /v2 would preserve /v1 for integrators.
The normative contract for implemented routes is openapi/openapi.yaml. When you change a handler or response shape, update OpenAPI in the same change.
Internal web apps (SPA, mobile) and external integrators (ERP, payroll, partner services) should use the same URLs, versions, and JSON shapes. Differences are only in how the caller authenticates and which org they act within.
- Today: interactive flows send
X-Principal-ID(UUID) after auth middleware in dev; production will replace this with JWT/OIDC or similar without changing path structure. - Tomorrow: machine clients will use
api_clients+X-API-Key(or mTLS) mapped to aprincipalsrow withkind = integrationand memberships in an org. The same/v1/organizations/{orgID}/…routes apply; RBAC checks scopes the same way.
Keeping one API avoids "internal vs integration" drift and makes OpenAPI the single contract.
- URL prefix: All authenticated product APIs are under
/v1. - Breaking changes: Add
/v2, keep/v1stable for a deprecation window. Do not silently change field types or meanings on/v1. - OpenAPI
info.version: Tracks the published API contract (e.g.1.0.0), not the Git tag of the server binary.
| Mechanism | Header | Status |
|---|---|---|
| Dev / legacy header | X-Principal-ID |
Implemented |
| Bearer / OIDC | Authorization |
Planned |
| Integration client | X-API-Key |
Reserved |
Constants live in internal/authn. principals.kind may be user or integration (migration 000011); users remains only for human principals.
- Routes under
/v1/organizations/{orgID}/…require org membership; the resolved org (role + effective scopes) is attached in context by middleware. - Services enforce
internal/authzchecks (accounting:read,accounting:manage, etc.). Do not branch in handlers on role strings.
Structured errors use internal/apperror and map to JSON via internal/httpapi/respond. Clients should rely on error.code, error.message, and optional error.details.fields, not on free-text parsing.
Convention (offset / limit):
- Query parameters:
limit(positive, capped per endpoint),offset(non‑negative). - Collection lists (journal entries, import batches and rows, etc.) include a
metaobject:{ "limit": <n>, "offset": <n> }so clients can page without guessing defaults. - The general ledger report keeps pagination fields at the top level of the JSON body (
limit,offset,has_more,total_count) for backward compatibility; it does not use ametawrapper. See OpenAPI for each route.
Future: Keyset (cursor) pagination may be added for large tables; new query params would be additive (e.g. after_id).
- Use query parameters with clear names (
status,date_from, …). - Prefer enumerated values documented in OpenAPI (
enumin the spec). - Default sort order is documented per list endpoint (e.g. journal entries by posting date descending).
For mutating POST endpoints where duplicate submits are costly or dangerous (journal draft creation, post, reverse, add line, import batch commit, link attachments), clients may send:
Idempotency-Key: <opaque string, max 128 chars>Behavior:
- If the header is omitted, the request behaves as before.
- If sent, the server keys by
(organization_id, route pattern, idempotency key)and a fingerprint ofmethod + path + body. - Same key + same request after a successful (2xx) response replays the stored status and JSON body.
- Same key + different body/path → 409 Conflict.
- Failures (non-2xx) do not persist the replay cache for that key, so the client can fix the request and retry.
Implementation: api_idempotency_records + middleware in internal/httpapi/middleware/idempotency.go.
- Prefer explicit keys over wrapping everything in generic
data. - Nullable fields are
nullin JSON when absent (document in OpenAPI). - Money amounts are minor units (e.g. øre) as integers (
*_minor). - Dates are ISO
YYYY-MM-DDwhere calendar dates are meant; timestampsdate-time(RFC3339) for audit fields.
Async work is persisted in background_jobs and processed by cmd/worker. The API enqueues (e.g. import validation); clients poll GET .../accounting/jobs/{jobID} for status, last_error, and result.
- Register route under
/v1/...with existing middleware (authn → org → handler). - Enforce authorization in services, not only in HTTP.
- Add or update
openapi/openapi.yaml(paths, schemas, enums,Idempotency-Keyif POST is retried). - For new list endpoints, include
metawithlimit/offset(andhas_more/total_countwhen available). - If POST must be safe to retry, wire
With(deps.Idempotency)and document the header.