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.
Conventions
Section titled “Conventions”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.
Optimistic concurrency
Section titled “Optimistic concurrency”Write routes that update a row require If-Match: <prior updatedAt>. A mismatch returns 412 with the current row in error.details.current.
Idempotency
Section titled “Idempotency”Write routes accept an Idempotency-Key: <uuid> header. The handler caches (tenantId, idempotencyKey, response) for 24 hours; a duplicate request returns the cached response.
Rate limiting
Section titled “Rate limiting”Token bucket per session and per IP at the edge. Limits in the table below. 429 responses include Retry-After (seconds).
Pagination
Section titled “Pagination”List routes return Page<T>:
interface Page<T> { items: T[]; nextCursor: string | null; // Opaque cursor; pass back as ?cursor= total: number;}Error envelope
Section titled “Error envelope”interface ApiError { error: { code: string; // Stable enum, see below message: string; // Human-readable, may be localised in v2 details?: Record<string, unknown>; };}Error codes:
| Code | HTTP | Meaning |
|---|---|---|
unauthenticated | 401 | No or expired session |
forbidden | 403 | Role or arId scope mismatch |
step_up_required | 403 | Step-up token required or expired |
not_found | 404 | Subject does not exist or is invisible to caller |
conflict | 409 | State-machine transition disallowed or duplicate write |
precondition_failed | 412 | If-Match mismatch |
validation_failed | 422 | Zod or business-rule validation failed; details carries field errors |
rate_limited | 429 | Token bucket exhausted; Retry-After set |
internal_error | 500 | Unhandled server error; correlation id in details.requestId |
service_unavailable | 503 | Dependency unhealthy (database, queue, object store) |
Route table
Section titled “Route table”| Method | Path | Auth | Rate limit | Notes |
|---|---|---|---|---|
POST | /api/auth/session | none | 5/min/IP | Issues session cookie. Lockout at 10 password fails per email per hour. |
DELETE | /api/auth/session | session | 30/min | Revokes current session. |
POST | /api/auth/session/refresh | session | 60/min | Rotates session id, slides expiry. |
POST | /api/auth/step-up | session | 10/min | Issues 10-minute step-up token. Requires password + TOTP. |
GET | /api/me | session | 60/min | Current user. Returns csrfToken. |
GET | /api/ars | session, principal | 120/min | Paged, filterable by status, riskBand, vertical, free-text q. |
POST | /api/ars | session, principal-admin | 30/min | Onboard new AR. |
GET | /api/ars/:id | session, principal or own | 240/min | AR-user reads only own. |
PATCH | /api/ars/:id | session, principal-admin | 30/min | Optimistic concurrency. Step-up required to set terminated. |
GET | /api/ars/:id/risk | session, principal | 120/min | Risk trajectory data. |
GET | /api/ars/:id/breaches | session | 120/min | Scoped to AR. |
GET | /api/ars/:id/reviews | session | 120/min | Scoped to AR. |
GET | /api/ars/:id/mi-returns | session | 120/min | Scoped to AR. |
GET | /api/ars/:id/conduct-events | session | 120/min | Scoped to AR. |
POST | /api/breaches | session | 30/min | AR-user files own; principal can file on behalf. Triggers SUP 15 clock. |
GET | /api/breaches | session, principal | 120/min | Triage queue. Sorts by notifyByAt asc nulls last. |
GET | /api/breaches/:id | session | 240/min | Detail. |
PATCH | /api/breaches/:id | session, compliance | 30/min | Status transitions, root-cause taxonomy. |
POST | /api/breaches/:id/notify-fca | session, principal-admin | 10/min | Records SUP 15 notification. Step-up required. |
GET | /api/reviews | session, principal | 120/min | Paged, filterable. |
POST | /api/reviews | session, compliance | 30/min | Schedules a review. |
GET | /api/reviews/:id | session | 240/min | Detail. AR-user reads own. |
PATCH | /api/reviews/:id | session, compliance | 60/min | Inline saving of findings, notes. |
POST | /api/reviews/:id/complete | session, compliance | 10/min | Terminal. Locks findings, recomputes AR score. |
POST | /api/reviews/:id/challenge | session, ar-user | 10/min | AR challenges within 10 working days of complete. |
GET | /api/mi-returns | session | 120/min | Paged, filterable. |
POST | /api/mi-returns | session, ar-user | 10/min | Idempotent on (arId, period). Creates draft. |
GET | /api/mi-returns/:id | session | 240/min | Detail. AR-user reads own. |
PATCH | /api/mi-returns/:id | session, ar-user | 60/min | Edit draft. |
POST | /api/mi-returns/:id/submit | session, ar-user | 10/min | Terminal. Recomputes anomaly score and AR risk. |
PATCH | /api/mi-returns/:id/status | session, compliance | 30/min | Mark queried or accepted. |
GET | /api/annual-reviews | session, principal | 60/min | Paged. |
GET | /api/annual-reviews/:id | session | 240/min | Detail. AR-user reads own. |
PATCH | /api/annual-reviews/:id | session, compliance | 60/min | Edit draft. |
POST | /api/annual-reviews/:id/submit-for-review | session, compliance | 10/min | Transitions draft to in-review. |
POST | /api/annual-reviews/:id/sign-off | session, principal-admin | 10/min | Terminal. Step-up required. |
POST | /api/annual-reviews/:id/reject | session, principal-admin | 10/min | Returns to draft with notes. |
GET | /api/audit | session, compliance/auditor | 60/min | Read-only chain. Cursor-paginated. |
POST | /api/audit/export | session, compliance/auditor | 5/min | Generates CSV bundle, signed URL in response. |
GET | /api/conduct-events | session, principal | 120/min | Paged. |
POST | /api/conduct-events | session, compliance | 30/min | Logs an event. |
POST | /api/attachments | session | 30/min | Returns signed PUT URL for upload. |
GET | /api/attachments/:id | session | 120/min | Returns signed GET URL. 5-minute TTL. |
GET | /api/tenants/me | session, principal | 60/min | Current tenant settings. |
PATCH | /api/tenants/me | session, principal-admin | 10/min | Edit settings. |
PATCH | /api/tenants/me/risk-weights | session, principal-admin | 5/min | Step-up required. Triggers tenant-wide risk recompute. |
GET | /api/users | session, principal-admin | 60/min | List tenant users. |
POST | /api/users | session, principal-admin | 10/min | Invite user. Sends email. |
PATCH | /api/users/:id | session, principal-admin | 30/min | Edit role, status. |
DELETE | /api/users/:id | session, principal-admin | 10/min | Off-boards (tombstones PII). |
GET | /api/health | none | 600/min | Database connectivity check. |
GET | /api/version | none | 600/min | Deployed commit SHA. |
Common request and response patterns
Section titled “Common request and response patterns”Sign in
Section titled “Sign in”POST /api/auth/sessionContent-Type: application/json
{ "email": "compliance@heritage.example", "password": "...", "totp": "123456"}HTTP/1.1 200 OKSet-Cookie: session=01HW...; HttpOnly; Secure; SameSite=Lax; Path=/Content-Type: application/json
{ "userId": "01HW6V8K...", "role": "principal-compliance-officer", "tenantId": "01HW6V0A..."}File a breach (AR-user)
Section titled “File a breach (AR-user)”POST /api/breachesCookie: session=01HW...Content-Type: application/jsonX-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 CreatedContent-Type: application/jsonLocation: /api/breaches/01HW6VA1...
{ "id": "01HW6VA1...", "arId": "01HW6V9B...", "status": "open", "notifyByAt": null, "createdAt": "2026-05-08T09:31:42Z", "updatedAt": "2026-05-08T09:31:42Z"}Notify FCA (step-up)
Section titled “Notify FCA (step-up)”POST /api/auth/step-upCookie: 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-fcaCookie: 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..."}List the AR register
Section titled “List the AR register”GET /api/ars?riskBand=critical,high&cursor=eyJpZCI6IjAxSF...&limit=50Cookie: session=01HW...HTTP/1.1 200 OKContent-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}Validation error
Section titled “Validation error”HTTP/1.1 422 Unprocessable EntityContent-Type: application/json
{ "error": { "code": "validation_failed", "message": "One or more fields failed validation.", "details": { "fields": { "metrics.complaintsUpheld": "complaintsUpheld cannot exceed complaintsReceived" } } }}Optimistic-concurrency conflict
Section titled “Optimistic-concurrency conflict”HTTP/1.1 412 Precondition FailedContent-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.
State-machine conflict
Section titled “State-machine conflict”HTTP/1.1 409 ConflictContent-Type: application/json
{ "error": { "code": "conflict", "message": "Cannot complete a review that is in 'challenged' status.", "details": { "currentStatus": "challenged", "requiredStatus": "in-progress" } }}Webhooks (planned, v2)
Section titled “Webhooks (planned, v2)”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.