Skip to content

API routes (planned)

The v1 demo has no API; data is fixtures and Zustand. This page documents the planned production API. JSON over HTTPS, session-cookie auth, tenant scoping implicit through row-level security.

All non-public routes require a session cookie. The session row carries userId, tenantId, role, arId. RLS scopes every query to tenantId. Handlers add role-and-arId enforcement on top.

Step-up auth (re-enter password and TOTP) is required on terminal actions, signalled by an X-Step-Up-Token header carrying a short-lived (10-minute) bearer.

Write routes (POST, PATCH, DELETE) require an X-CSRF-Token header that matches the session’s csrfToken (returned by GET /api/me). Double-submit pattern.

Write routes that update a row require If-Match: <prior updatedAt>. A mismatch returns 412 with the current row in error.details.current.

Write routes accept an Idempotency-Key: <uuid> header. The handler caches (tenantId, idempotencyKey, response) for 24 hours; a duplicate request returns the cached response.

Token bucket per session and per IP at the edge. Limits in the table below. 429 responses include Retry-After (seconds).

List routes return Page<T>:

interface Page<T> {
items: T[];
nextCursor: string | null; // Opaque cursor; pass back as ?cursor=
total: number;
}
interface ApiError {
error: {
code: string; // Stable enum, see below
message: string; // Human-readable, may be localised in v2
details?: Record<string, unknown>;
};
}

Error codes:

CodeHTTPMeaning
unauthenticated401No or expired session
forbidden403Role or arId scope mismatch
step_up_required403Step-up token required or expired
not_found404Subject does not exist or is invisible to caller
conflict409State-machine transition disallowed or duplicate write
precondition_failed412If-Match mismatch
validation_failed422Zod or business-rule validation failed; details carries field errors
rate_limited429Token bucket exhausted; Retry-After set
internal_error500Unhandled server error; correlation id in details.requestId
service_unavailable503Dependency unhealthy (database, queue, object store)
MethodPathAuthRate limitNotes
POST/api/auth/sessionnone5/min/IPIssues session cookie. Lockout at 10 password fails per email per hour.
DELETE/api/auth/sessionsession30/minRevokes current session.
POST/api/auth/session/refreshsession60/minRotates session id, slides expiry.
POST/api/auth/step-upsession10/minIssues 10-minute step-up token. Requires password + TOTP.
GET/api/mesession60/minCurrent user. Returns csrfToken.
GET/api/arssession, principal120/minPaged, filterable by status, riskBand, vertical, free-text q.
POST/api/arssession, principal-admin30/minOnboard new AR.
GET/api/ars/:idsession, principal or own240/minAR-user reads only own.
PATCH/api/ars/:idsession, principal-admin30/minOptimistic concurrency. Step-up required to set terminated.
GET/api/ars/:id/risksession, principal120/minRisk trajectory data.
GET/api/ars/:id/breachessession120/minScoped to AR.
GET/api/ars/:id/reviewssession120/minScoped to AR.
GET/api/ars/:id/mi-returnssession120/minScoped to AR.
GET/api/ars/:id/conduct-eventssession120/minScoped to AR.
POST/api/breachessession30/minAR-user files own; principal can file on behalf. Triggers SUP 15 clock.
GET/api/breachessession, principal120/minTriage queue. Sorts by notifyByAt asc nulls last.
GET/api/breaches/:idsession240/minDetail.
PATCH/api/breaches/:idsession, compliance30/minStatus transitions, root-cause taxonomy.
POST/api/breaches/:id/notify-fcasession, principal-admin10/minRecords SUP 15 notification. Step-up required.
GET/api/reviewssession, principal120/minPaged, filterable.
POST/api/reviewssession, compliance30/minSchedules a review.
GET/api/reviews/:idsession240/minDetail. AR-user reads own.
PATCH/api/reviews/:idsession, compliance60/minInline saving of findings, notes.
POST/api/reviews/:id/completesession, compliance10/minTerminal. Locks findings, recomputes AR score.
POST/api/reviews/:id/challengesession, ar-user10/minAR challenges within 10 working days of complete.
GET/api/mi-returnssession120/minPaged, filterable.
POST/api/mi-returnssession, ar-user10/minIdempotent on (arId, period). Creates draft.
GET/api/mi-returns/:idsession240/minDetail. AR-user reads own.
PATCH/api/mi-returns/:idsession, ar-user60/minEdit draft.
POST/api/mi-returns/:id/submitsession, ar-user10/minTerminal. Recomputes anomaly score and AR risk.
PATCH/api/mi-returns/:id/statussession, compliance30/minMark queried or accepted.
GET/api/annual-reviewssession, principal60/minPaged.
GET/api/annual-reviews/:idsession240/minDetail. AR-user reads own.
PATCH/api/annual-reviews/:idsession, compliance60/minEdit draft.
POST/api/annual-reviews/:id/submit-for-reviewsession, compliance10/minTransitions draft to in-review.
POST/api/annual-reviews/:id/sign-offsession, principal-admin10/minTerminal. Step-up required.
POST/api/annual-reviews/:id/rejectsession, principal-admin10/minReturns to draft with notes.
GET/api/auditsession, compliance/auditor60/minRead-only chain. Cursor-paginated.
POST/api/audit/exportsession, compliance/auditor5/minGenerates CSV bundle, signed URL in response.
GET/api/conduct-eventssession, principal120/minPaged.
POST/api/conduct-eventssession, compliance30/minLogs an event.
POST/api/attachmentssession30/minReturns signed PUT URL for upload.
GET/api/attachments/:idsession120/minReturns signed GET URL. 5-minute TTL.
GET/api/tenants/mesession, principal60/minCurrent tenant settings.
PATCH/api/tenants/mesession, principal-admin10/minEdit settings.
PATCH/api/tenants/me/risk-weightssession, principal-admin5/minStep-up required. Triggers tenant-wide risk recompute.
GET/api/userssession, principal-admin60/minList tenant users.
POST/api/userssession, principal-admin10/minInvite user. Sends email.
PATCH/api/users/:idsession, principal-admin30/minEdit role, status.
DELETE/api/users/:idsession, principal-admin10/minOff-boards (tombstones PII).
GET/api/healthnone600/minDatabase connectivity check.
GET/api/versionnone600/minDeployed commit SHA.
POST /api/auth/session
Content-Type: application/json
{
"email": "compliance@heritage.example",
"password": "...",
"totp": "123456"
}
HTTP/1.1 200 OK
Set-Cookie: session=01HW...; HttpOnly; Secure; SameSite=Lax; Path=/
Content-Type: application/json
{
"userId": "01HW6V8K...",
"role": "principal-compliance-officer",
"tenantId": "01HW6V0A..."
}
POST /api/breaches
Cookie: session=01HW...
Content-Type: application/json
X-CSRF-Token: <csrf>
Idempotency-Key: 8f4f2e8a-7e1f-4f3b-8a02-7a6e1c1c8f3b
{
"arId": "01HW6V9B...",
"title": "Affordability check missed for case 24817",
"description": "On 2026-05-06 the broker submitted ...",
"category": "advice-suitability",
"severity": "moderate",
"customerImpact": "potential",
"awareAt": "2026-05-08T09:30:00Z",
"rootCauseTaxonomy": ["process", "training"]
}
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/breaches/01HW6VA1...
{
"id": "01HW6VA1...",
"arId": "01HW6V9B...",
"status": "open",
"notifyByAt": null,
"createdAt": "2026-05-08T09:31:42Z",
"updatedAt": "2026-05-08T09:31:42Z"
}
POST /api/auth/step-up
Cookie: session=01HW...
Content-Type: application/json
{ "password": "...", "totp": "654321" }
HTTP/1.1 200 OK
{ "stepUpToken": "su_01HW...", "expiresAt": "2026-05-08T09:42:00Z" }
POST /api/breaches/01HW6VA1/notify-fca
Cookie: session=01HW...
X-CSRF-Token: <csrf>
X-Step-Up-Token: su_01HW...
If-Match: 2026-05-08T09:31:42Z
{
"narrative": "Notification submitted under SUP 15.3 ..."
}
HTTP/1.1 200 OK
{
"id": "01HW6VA1...",
"notifiedFcaAt": "2026-05-08T09:43:18Z",
"auditEventId": "01HW6VAH...",
"bundleUrl": "/api/attachments/01HW6VAJ..."
}
GET /api/ars?riskBand=critical,high&cursor=eyJpZCI6IjAxSF...&limit=50
Cookie: session=01HW...
HTTP/1.1 200 OK
Content-Type: application/json
{
"items": [
{
"id": "01HW6V9B...",
"tradingName": "Pemberton Mortgages",
"frn": "412803",
"status": "under-investigation",
"riskScore": 84.2,
"riskBand": "critical",
"lastAnnualReviewAt": "2025-04-12T00:00:00Z",
"nextReviewDueAt": "2026-04-12T00:00:00Z"
}
],
"nextCursor": "eyJpZCI6IjAxSFc2VjFBLi4uIn0=",
"total": 32
}
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"error": {
"code": "validation_failed",
"message": "One or more fields failed validation.",
"details": {
"fields": {
"metrics.complaintsUpheld": "complaintsUpheld cannot exceed complaintsReceived"
}
}
}
}
HTTP/1.1 412 Precondition Failed
Content-Type: application/json
{
"error": {
"code": "precondition_failed",
"message": "The record was modified by another user.",
"details": {
"current": { "updatedAt": "2026-05-08T09:35:11Z", "status": "in-remediation" }
}
}
}

The client opens a merge dialog showing the diff and lets the user resubmit.

HTTP/1.1 409 Conflict
Content-Type: application/json
{
"error": {
"code": "conflict",
"message": "Cannot complete a review that is in 'challenged' status.",
"details": { "currentStatus": "challenged", "requiredStatus": "in-progress" }
}
}

Out of scope for v1. v2 adds outbound webhooks for breach.notify-fca, annual-review.sign-off, and ar.terminated, with HMAC-SHA256 signing of the payload using a per-tenant key.