Skip to main content

regulator-portal-service — API Contracts

Version: 1.0 Status: Draft Owner: Regulator-facing + Legal Last Updated: 2026-04-21 Companion: APPLICATION_LOGIC · SECURITY_MODEL · SYNC_CONTRACT · EVENT_SCHEMAS

The regulator-portal-service exposes three REST surfaces:

  1. Regulator REST :3082 — mTLS with national PKI, fronted by Envoy (no Kong — Kong does not carry the client-cert chain cleanly for regulators).
  2. Auditor REST :3083 — mTLS with auditor PKI, separate trust anchor and trust store.
  3. Internal admin REST :3084 — Kong JWT (platform.regulator.admin); used by admin-dashboard and cron pods.

Plus:

  • Web BFF :3081 — Next.js SSR frontend, delegates to REST plane on the same pod group.
  • Metrics / health :9464 — Prometheus.

Base path for all REST: /v1. All responses JSON unless noted.


1. Regulator REST — /v1/regulator/*

1.1 Session & Identity

MethodPathRolePurpose
POST/v1/regulator/auth/mtls/verify— (cert only)Handshake verify + issue session cookie; performed by Envoy + callback to service
GET/v1/regulator/meany regulator roleReturn resolved { userId, orgName, role, allowedRegions, sessionExpiresAt }
POST/v1/regulator/auth/logoutany regulator roleInvalidate session

1.2 Lawful Intercept

MethodPathRolePurpose
POST/v1/regulator/li/requestsregulator-liSubmit LI request with warrant (multipart)
GET/v1/regulator/li/requestsregulator-liList own LI requests (filters: state, createdAfter, createdBefore)
GET/v1/regulator/li/requests/{liRequestId}regulator-liSingle LI request + audit chain
POST/v1/regulator/li/requests/{liRequestId}/transitioninternal actor (Legal+Security — enforced by dual-control)Advance state; see below
GET/v1/regulator/li/requests/{liRequestId}/deliveryregulator-liFetch pre-signed SFTP delivery manifest (read-only)

Submit request body (multipart/form-data):

POST /v1/regulator/li/requests HTTP/1.1
Content-Type: multipart/form-data; boundary=----Boundary

------Boundary
Content-Disposition: form-data; name="metadata"
Content-Type: application/json

{
"targetMsisdn": "+93701234567",
"dateRange": { "from": "2026-04-01T00:00:00Z", "to": "2026-04-21T00:00:00Z" },
"scope": "FULL",
"legalRef": "ATRA-WAR-2026-00127",
"signedWarrantHashSha256": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b"
}
------Boundary
Content-Disposition: form-data; name="warrant"; filename="warrant.pdf"
Content-Type: application/pdf

<binary PDF>
------Boundary--

Response 201:

{
"liRequestId": "li_01HV9K8XYZ...",
"state": "RECEIVED",
"ackBy": "2026-04-21T11:00:00Z",
"inProgressBy": "2026-04-21T14:00:00Z",
"deliverBy": "2026-04-22T04:00:00Z"
}

Errors:

  • 401 MTLS_HANDSHAKE_REQUIRED — no client cert
  • 403 INSUFFICIENT_SCOPE — role lacks regulator-li
  • 422 WARRANT_HASH_MISMATCH — uploaded PDF SHA-256 differs from signedWarrantHashSha256
  • 422 INVALID_MSISDN — not E.164
  • 429 RATE_LIMITED — per-user 30 requests/hour on this endpoint

Transition body:

{
"action": "ACK",
"rationale": "Legal review confirms warrant authenticity",
"approverSignature": "base64(ed25519-signature-over-liRequestId||action||ts)"
}

1.3 Complaints

MethodPathRolePurpose
POST/v1/regulator/complaintsregulator-readForward a citizen complaint to Ghasi
GET/v1/regulator/complaintsregulator-readList (own orgName scope)
GET/v1/regulator/complaints/{complaintId}regulator-readSingle complaint
GET/v1/regulator/complaints/{complaintId}/resolutionregulator-readRead back resolution when state ≥ RESOLVED

Submit body:

{
"citizenMsisdn": "+93705551234",
"complaintType": "UNSOLICITED_SMS",
"summary": "Customer received 20 marketing SMS in one day from sender ID ACMEBANK.",
"receivedAt": "2026-04-20T08:15:00Z",
"regulatorRef": "ATRA-CMP-2026-045612"
}

Response 201:

{
"complaintId": "comp_01HV9K...",
"state": "RECEIVED",
"slaDueAt": "2026-04-27T08:15:00Z"
}

1.4 Reports

MethodPathRolePurpose
POST/v1/regulator/reportsregulator-readRequest an ad-hoc report
GET/v1/regulator/reportsregulator-readList (own orgName scope; cursor pagination)
GET/v1/regulator/reports/{reportJobId}regulator-readStatus + metadata
GET/v1/regulator/reports/{reportJobId}/downloadregulator-read302 → pre-signed S3 URL (15-min TTL)
GET/v1/regulator/reports/{reportJobId}/signatureregulator-readFetch detached .p7s
POST/v1/regulator/reports/{reportJobId}/ackregulator-readAcknowledge download (emits regulator.report.acked.v1)
POST/v1/regulator/reports/{reportJobId}/rejectregulator-readRaise integrity concern

Request body (ad-hoc):

{
"reportType": "AD_HOC",
"filters": {
"dateRange": { "from": "2026-03-01", "to": "2026-03-31" },
"tenantIds": ["t_01H..."],
"senderIds": ["ACMEBANK"],
"region": "AF-KAB"
},
"outputFormat": "PDF"
}

1.5 Attestations (shared with auditor)

MethodPathRolePurpose
GET/v1/regulator/attestationsregulator-read | regulator-auditorControl catalog + evidence status
GET/v1/regulator/attestations/{framework}/{controlId}sameDetail + evidence links
POST/v1/regulator/attestations/bundleregulator-auditor or platform.compliance.adminRequest annual bundle generation
GET/v1/regulator/attestations/bundle/{bundleId}sameStatus + download link

1.6 Rate limits (Envoy local rate-limit filter)

SurfaceLimit
LI submit30 req / h per user
LI transition120 req / h per user
Complaint ingest600 req / h per orgName
Report generation10 req / h per user
Report download120 req / h per user
Attestation read300 req / min per orgName

2. Auditor REST — /v1/auditor/*

Read-only. Surfaces only resources in grantedFrameworks.

MethodPathPurpose
GET/v1/auditor/meResolved auditor identity + access window
GET/v1/auditor/evidenceList accessible evidence (pagination, filter by framework/control)
GET/v1/auditor/evidence/{evidenceId}/download302 → pre-signed URL (5-min TTL)
GET/v1/auditor/attestations/bundleList bundles visible to this auditor
GET/v1/auditor/attestations/bundle/{bundleId}/download302 → pre-signed bundle URL

Every read writes a row to regulator.auditor_access_audit with { auditorId, resourceRef, ip, ua, occurredAt }.

Access-denied response on expired grant:

{
"error": {
"code": "AUDITOR_ACCESS_EXPIRED",
"message": "Access grant expired at 2026-04-15T00:00:00Z",
"accessExpiresAt": "2026-04-15T00:00:00Z",
"traceId": "00-abc-..."
}
}

3. Internal Admin REST — /v1/admin/*

Kong-fronted JWT. Role platform.regulator.admin.

MethodPathPurpose
GET/v1/admin/siem/destinationsList SIEM destinations
POST/v1/admin/siem/destinationsCreate destination
PUT/v1/admin/siem/destinations/{id}Update
DELETE/v1/admin/siem/destinations/{id}Soft-delete
POST/v1/admin/siem/destinations/{id}/testSend test event; returns ACK latency
GET/v1/admin/siem/delivery-logQuery delivery log
GET/v1/admin/evidence/collection-statusLast-run per control, failure counts
POST/v1/admin/evidence/collect-nowTrigger one-off collection cycle
POST/v1/admin/auditor/grantsCreate time-boxed auditor grant
POST/v1/admin/auditor/grants/{auditorId}/revokeImmediate revoke
POST/v1/admin/li/requests/{id}/escalateEscalate to CISO+CTO dual-sign for SLA bypass

Create auditor-grant body:

{
"firmName": "BigFour LLP",
"certSubjectDn": "CN=John Doe,O=BigFour LLP,C=US",
"issuerDn": "CN=BigFour Auditor CA",
"grantedFrameworks": ["ISO_27001", "SOC2_TYPE_II"],
"accessDurationDays": 30
}

Returns { auditorId, accessExpiresAt }.


4. Web BFF — /bff/*

Next.js server components. Routes (all server-side, no client-exposed secrets):

PathPurpose
/Landing + login status
/liLI workbench (regulator side)
/complaintsComplaints submission + history
/reportsReport listing + generation
/attestationsAttestation catalog
/auditor/*Auditor portal subset

The BFF calls the REST plane with the regulator's mTLS session cookie exchanged for a short-lived internal JWT (HS256, 5-min TTL, regulator-portal-only claim). This prevents the BFF from holding regulator certs directly while preserving identity end-to-end.


5. Pagination

All list endpoints use cursor pagination:

{
"items": [ ... ],
"nextCursor": "eyJsYXN0SWQiOiIuLi4iLCJsYXN0VHMiOiIuLi4ifQ==",
"total": 1234
}

Max limit = 100. nextCursor encodes (lastId, lastTimestamp) to avoid offset drift.


6. Error Envelope

Uniform across all REST surfaces (aligned with docs/standards/ERROR_CODES.md):

{
"error": {
"code": "WARRANT_HASH_MISMATCH",
"message": "Uploaded warrant SHA-256 does not match provided hash",
"details": {
"expected": "3a7bd3e2...",
"actual": "9f1c8b4a..."
},
"traceId": "00-abc-..."
}
}

Error-code catalogue

HTTPCodeWhen
400VALIDATION_FAILEDBody or query param invalid
401MTLS_HANDSHAKE_REQUIREDNo / invalid client cert
401OCSP_VERIFICATION_FAILEDStapled OCSP absent or negative
401CRL_REVOKEDCert is on CRL
401UNKNOWN_CERT_SUBJECTCert valid, subject not provisioned
403INSUFFICIENT_SCOPERole lacks capability
403AUDITOR_ACCESS_EXPIREDAccess window ended
403REGION_NOT_ALLOWEDUser's allowedRegions does not include request's region
404NOT_FOUNDResource missing
409CONFLICTLI transition conflict, duplicate complaint
409ILLEGAL_TRANSITIONState-machine edge invalid
409DUAL_CONTROL_VIOLATIONSame user attempting both initiator + approver
422WARRANT_HASH_MISMATCHWarrant PDF integrity failure
422INVALID_MSISDNNot E.164
422REPORT_FILTER_INVALIDFilter combination unsupported
429RATE_LIMITEDEnvoy/Kong rate-limit
500INTERNALUnhandled
502UPSTREAM_UNAVAILABLERead-through to compliance-engine / consent-ledger / CDR failed
503HSM_UNAVAILABLECannot sign report / bundle
503SFTP_UNAVAILABLELI delivery drop-box unreachable

7. Versioning

  • REST: /v1 top-level. Additive changes non-breaking; breaking → /v2 with 180-day deprecation window (ATRA-coordinated).
  • Accept: application/vnd.ghasi.regulator.v1+json reserved for future content-negotiation if ATRA requires.
  • Event schema versioning per EVENT_SCHEMAS §6.

8. Operational

MethodPathPurpose
GET/health/liveLiveness (K8s)
GET/health/readyReadiness (DB + Redis + NATS + Vault reachable)
GET/metricsPrometheus
GET/v1/openapi.jsonOpenAPI 3.1 spec (regulator + auditor + admin planes merged)

9. Contract Artefacts

  • OpenAPI 3.1 published to internal API registry on every deploy; PR contract-tests run against sms-orchestrator-agnostic contract (regulator-portal is a leaf, not a peer).
  • Pact verification between admin-dashboard and /v1/admin/*.
  • Manual cross-check against ATRA-provided OpenAPI for their SFTP manifest conventions (out-of-band).