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:
- Regulator REST
:3082— mTLS with national PKI, fronted by Envoy (no Kong — Kong does not carry the client-cert chain cleanly for regulators). - Auditor REST
:3083— mTLS with auditor PKI, separate trust anchor and trust store. - Internal admin REST
:3084— Kong JWT (platform.regulator.admin); used byadmin-dashboardand 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
| Method | Path | Role | Purpose |
|---|---|---|---|
| POST | /v1/regulator/auth/mtls/verify | — (cert only) | Handshake verify + issue session cookie; performed by Envoy + callback to service |
| GET | /v1/regulator/me | any regulator role | Return resolved { userId, orgName, role, allowedRegions, sessionExpiresAt } |
| POST | /v1/regulator/auth/logout | any regulator role | Invalidate session |
1.2 Lawful Intercept
| Method | Path | Role | Purpose |
|---|---|---|---|
| POST | /v1/regulator/li/requests | regulator-li | Submit LI request with warrant (multipart) |
| GET | /v1/regulator/li/requests | regulator-li | List own LI requests (filters: state, createdAfter, createdBefore) |
| GET | /v1/regulator/li/requests/{liRequestId} | regulator-li | Single LI request + audit chain |
| POST | /v1/regulator/li/requests/{liRequestId}/transition | internal actor (Legal+Security — enforced by dual-control) | Advance state; see below |
| GET | /v1/regulator/li/requests/{liRequestId}/delivery | regulator-li | Fetch 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 cert403 INSUFFICIENT_SCOPE— role lacksregulator-li422 WARRANT_HASH_MISMATCH— uploaded PDF SHA-256 differs fromsignedWarrantHashSha256422 INVALID_MSISDN— not E.164429 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
| Method | Path | Role | Purpose |
|---|---|---|---|
| POST | /v1/regulator/complaints | regulator-read | Forward a citizen complaint to Ghasi |
| GET | /v1/regulator/complaints | regulator-read | List (own orgName scope) |
| GET | /v1/regulator/complaints/{complaintId} | regulator-read | Single complaint |
| GET | /v1/regulator/complaints/{complaintId}/resolution | regulator-read | Read 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
| Method | Path | Role | Purpose |
|---|---|---|---|
| POST | /v1/regulator/reports | regulator-read | Request an ad-hoc report |
| GET | /v1/regulator/reports | regulator-read | List (own orgName scope; cursor pagination) |
| GET | /v1/regulator/reports/{reportJobId} | regulator-read | Status + metadata |
| GET | /v1/regulator/reports/{reportJobId}/download | regulator-read | 302 → pre-signed S3 URL (15-min TTL) |
| GET | /v1/regulator/reports/{reportJobId}/signature | regulator-read | Fetch detached .p7s |
| POST | /v1/regulator/reports/{reportJobId}/ack | regulator-read | Acknowledge download (emits regulator.report.acked.v1) |
| POST | /v1/regulator/reports/{reportJobId}/reject | regulator-read | Raise 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)
| Method | Path | Role | Purpose |
|---|---|---|---|
| GET | /v1/regulator/attestations | regulator-read | regulator-auditor | Control catalog + evidence status |
| GET | /v1/regulator/attestations/{framework}/{controlId} | same | Detail + evidence links |
| POST | /v1/regulator/attestations/bundle | regulator-auditor or platform.compliance.admin | Request annual bundle generation |
| GET | /v1/regulator/attestations/bundle/{bundleId} | same | Status + download link |
1.6 Rate limits (Envoy local rate-limit filter)
| Surface | Limit |
|---|---|
| LI submit | 30 req / h per user |
| LI transition | 120 req / h per user |
| Complaint ingest | 600 req / h per orgName |
| Report generation | 10 req / h per user |
| Report download | 120 req / h per user |
| Attestation read | 300 req / min per orgName |
2. Auditor REST — /v1/auditor/*
Read-only. Surfaces only resources in grantedFrameworks.
| Method | Path | Purpose |
|---|---|---|
| GET | /v1/auditor/me | Resolved auditor identity + access window |
| GET | /v1/auditor/evidence | List accessible evidence (pagination, filter by framework/control) |
| GET | /v1/auditor/evidence/{evidenceId}/download | 302 → pre-signed URL (5-min TTL) |
| GET | /v1/auditor/attestations/bundle | List bundles visible to this auditor |
| GET | /v1/auditor/attestations/bundle/{bundleId}/download | 302 → 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.
| Method | Path | Purpose |
|---|---|---|
| GET | /v1/admin/siem/destinations | List SIEM destinations |
| POST | /v1/admin/siem/destinations | Create destination |
| PUT | /v1/admin/siem/destinations/{id} | Update |
| DELETE | /v1/admin/siem/destinations/{id} | Soft-delete |
| POST | /v1/admin/siem/destinations/{id}/test | Send test event; returns ACK latency |
| GET | /v1/admin/siem/delivery-log | Query delivery log |
| GET | /v1/admin/evidence/collection-status | Last-run per control, failure counts |
| POST | /v1/admin/evidence/collect-now | Trigger one-off collection cycle |
| POST | /v1/admin/auditor/grants | Create time-boxed auditor grant |
| POST | /v1/admin/auditor/grants/{auditorId}/revoke | Immediate revoke |
| POST | /v1/admin/li/requests/{id}/escalate | Escalate 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):
| Path | Purpose |
|---|---|
/ | Landing + login status |
/li | LI workbench (regulator side) |
/complaints | Complaints submission + history |
/reports | Report listing + generation |
/attestations | Attestation 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
| HTTP | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Body or query param invalid |
| 401 | MTLS_HANDSHAKE_REQUIRED | No / invalid client cert |
| 401 | OCSP_VERIFICATION_FAILED | Stapled OCSP absent or negative |
| 401 | CRL_REVOKED | Cert is on CRL |
| 401 | UNKNOWN_CERT_SUBJECT | Cert valid, subject not provisioned |
| 403 | INSUFFICIENT_SCOPE | Role lacks capability |
| 403 | AUDITOR_ACCESS_EXPIRED | Access window ended |
| 403 | REGION_NOT_ALLOWED | User's allowedRegions does not include request's region |
| 404 | NOT_FOUND | Resource missing |
| 409 | CONFLICT | LI transition conflict, duplicate complaint |
| 409 | ILLEGAL_TRANSITION | State-machine edge invalid |
| 409 | DUAL_CONTROL_VIOLATION | Same user attempting both initiator + approver |
| 422 | WARRANT_HASH_MISMATCH | Warrant PDF integrity failure |
| 422 | INVALID_MSISDN | Not E.164 |
| 422 | REPORT_FILTER_INVALID | Filter combination unsupported |
| 429 | RATE_LIMITED | Envoy/Kong rate-limit |
| 500 | INTERNAL | Unhandled |
| 502 | UPSTREAM_UNAVAILABLE | Read-through to compliance-engine / consent-ledger / CDR failed |
| 503 | HSM_UNAVAILABLE | Cannot sign report / bundle |
| 503 | SFTP_UNAVAILABLE | LI delivery drop-box unreachable |
7. Versioning
- REST:
/v1top-level. Additive changes non-breaking; breaking →/v2with 180-day deprecation window (ATRA-coordinated). Accept: application/vnd.ghasi.regulator.v1+jsonreserved for future content-negotiation if ATRA requires.- Event schema versioning per EVENT_SCHEMAS §6.
8. Operational
| Method | Path | Purpose |
|---|---|---|
| GET | /health/live | Liveness (K8s) |
| GET | /health/ready | Readiness (DB + Redis + NATS + Vault reachable) |
| GET | /metrics | Prometheus |
| GET | /v1/openapi.json | OpenAPI 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-dashboardand/v1/admin/*. - Manual cross-check against ATRA-provided OpenAPI for their SFTP manifest conventions (out-of-band).