Compliance Engine — API Contracts
Status: populated Owner: Platform Engineering / Trust & Safety Last updated: 2026-04-19 Companion: SYNC_CONTRACT · APPLICATION_LOGIC · SECURITY_MODEL
The compliance-engine exposes two interface planes:
- gRPC on
:50052(mTLS required) — invoked bysms-orchestrator's NATS consumer. This is the hot path. - HTTPS REST on
:3013— fronted by Kong; JWT + RBAC — foradmin-dashboard(rule authoring, hold-queue review, tenant scoring) and a narrow tenant-portal subset.
Both surfaces share the same domain aggregates but serve different actors.
1. gRPC service — ComplianceService.v1
Full .proto lives in SYNC_CONTRACT §3. Reproduced here for reference with field-level stability notes.
syntax = "proto3";
package ghasi.sms.compliance.v1;
service ComplianceService {
// Hot path. P95 ≤ 500 ms. mTLS required.
rpc EvaluateCompliance(EvaluateComplianceRequest)
returns (EvaluateComplianceResponse);
}
message EvaluateComplianceRequest {
string message_id = 1;
string tenant_id = 2;
string account_id = 3;
string to = 4;
string from_id = 5;
string body = 6;
string message_type = 7;
int32 segments = 8;
string encoding = 9;
string idempotency_key = 10;
map<string, string> metadata = 11;
}
message EvaluateComplianceResponse {
string evaluation_id = 1;
ComplianceVerdict verdict = 2;
repeated Finding findings = 3;
string rule_set_id = 4;
int64 evaluation_latency_ms = 5;
string hold_id = 6;
}
enum ComplianceVerdict {
COMPLIANCE_VERDICT_UNSPECIFIED = 0;
ALLOW = 1;
BLOCK = 2;
HOLD = 3;
FLAG = 4;
}
message Finding {
string rule_id = 1;
string rule_name = 2;
string rule_type = 3;
ComplianceVerdict action = 4;
string evidence = 5;
float confidence = 6;
}
Error mapping
| gRPC status | Condition | Orchestrator behaviour |
|---|---|---|
OK | Verdict returned | Act per verdict |
INVALID_ARGUMENT | Missing/malformed field | Ack message, update to BLOCKED with reason bad_input, raise alert |
RESOURCE_EXHAUSTED | Per-pod concurrency cap hit | Do not ack — redeliver via NATS |
UNAVAILABLE | Connect or deadline | Do not ack — redeliver |
INTERNAL | Handler exception | Do not ack — redeliver |
DEADLINE_EXCEEDED | > 1 s (orchestrator timeout) | Do not ack — redeliver |
After maxRedeliveries = 3, messages move to sms.outbound.deadletter with reason compliance_unavailable. They are never dispatched without an explicit ALLOW.
Streaming variants (future, not in v1)
BatchEvaluate(unary) for bulk admin reruns — deferred to v2.WatchHoldQueue(server-streaming) for admin-dashboard live view — currently served via SSE on the REST plane.
2. REST — public surface
Base path: /v1/compliance. All endpoints fronted by Kong (jwt plugin + rate-limiting-advanced). All responses JSON. All state-changing endpoints are idempotent via Idempotency-Key header where noted.
2.1 Hold queue
Admin (platform roles)
| Method | Path | Role | Purpose |
|---|---|---|---|
| GET | /v1/compliance/hold-queue | platform.compliance.reviewer or higher | List PENDING/REVIEWING holds with pagination + filters |
| GET | /v1/compliance/hold-queue/{holdId} | platform.compliance.reviewer or higher | Single hold (body redacted unless role ≥ platform.compliance.admin) |
| POST | /v1/compliance/hold-queue/{holdId}/review | platform.compliance.reviewer | Release or reject. Body: { action: 'RELEASE' | 'REJECT', notes?: string }. Idempotent on holdId. |
| POST | /v1/compliance/hold-queue/bulk-review | platform.compliance.admin | Bulk action with filter (tenantId, ruleId, heldBefore). Hard cap: 10,000 rows. Returns job receipt. |
| GET | /v1/compliance/hold-queue/metrics | platform.compliance.reviewer or higher | Queue depth by tenant, oldest pending, auto-expire rate |
Query parameters for list: tenantId, accountId, ruleId, status, minPriority, heldAfter, heldBefore, cursor, limit (≤ 100).
Response envelope (list):
{
"items": [
{
"holdId": "hq_01H...",
"messageId": "msg_01H...",
"tenantId": "t_...",
"accountId": "a_...",
"reviewPriority": 87,
"status": "PENDING",
"heldAt": "2026-04-19T10:00:00Z",
"autoExpiresAt": "2026-04-20T10:00:00Z",
"triggerRuleIds": ["rl_...", "rl_..."],
"toMasked": "+93701***",
"senderId": "ACME",
"payloadPreview": "<redacted — role-gated>"
}
],
"nextCursor": "eyJ...",
"total": 1234
}
Tenant (portal)
| Method | Path | Role | Purpose |
|---|---|---|---|
| GET | /v1/portal/compliance/held-messages | Tenant user with sms:compliance:read | List their own held/blocked messages — RLS-scoped to X-Tenant-Id, body never revealed |
| POST | /v1/portal/compliance/held-messages/{holdId}/appeal | Tenant admin | Submit an appeal — creates a support ticket tagged compliance-appeal |
2.2 Rules
| Method | Path | Role | Purpose |
|---|---|---|---|
| GET | /v1/compliance/rules | platform.compliance.admin | Paginated list |
| GET | /v1/compliance/rules/{ruleId} | platform.compliance.admin | Current rule |
| GET | /v1/compliance/rules/{ruleId}/versions | platform.compliance.admin | Version history |
| GET | /v1/compliance/rules/{ruleId}/versions/{version} | platform.compliance.admin | Rollback target |
| POST | /v1/compliance/rules | platform.compliance.admin | Create |
| PUT | /v1/compliance/rules/{ruleId} | platform.compliance.admin | Update (bumps version) |
| POST | /v1/compliance/rules/{ruleId}/enable | platform.compliance.admin | Enable (idempotent) |
| POST | /v1/compliance/rules/{ruleId}/disable | platform.compliance.admin | Disable (idempotent) |
| DELETE | /v1/compliance/rules/{ruleId} | platform.compliance.admin | Soft-delete (rejected if referenced by active COMPOSITE) |
| POST | /v1/compliance/rules/{ruleId}/test | platform.compliance.admin | Dry-run against a supplied MessageContext; does not write evaluation_log |
Create body (type-polymorphic config):
{
"name": "Block financial fraud keywords (EN)",
"description": "Canonical fraud word list",
"type": "KEYWORD",
"action": "BLOCK",
"priority": 100,
"isActive": true,
"config": {
"keywordListId": "kw_01H...",
"matchAll": false,
"caseSensitive": false
}
}
Per-type config schemas are Zod-validated and published under /v1/compliance/schemas/{type}.
2.3 Rule sets + assignments
| Method | Path | Role | Purpose |
|---|---|---|---|
| GET | /v1/compliance/rule-sets | platform.compliance.admin | — |
| POST | /v1/compliance/rule-sets | platform.compliance.admin | Create (status starts draft) |
| PUT | /v1/compliance/rule-sets/{id} | platform.compliance.admin | Update |
| POST | /v1/compliance/rule-sets/{id}/activate | platform.compliance.admin | draft → active |
| POST | /v1/compliance/rule-sets/{id}/retire | platform.compliance.admin | active → retired |
| POST | /v1/compliance/rule-sets/{id}/set-default | platform.compliance.admin | Swap the platform default (transactional; exactly one default at any moment) |
| GET | /v1/compliance/tenants/{tenantId}/assignments | platform.compliance.admin | — |
| PUT | /v1/compliance/tenants/{tenantId}/assignments | platform.compliance.admin | Upsert list of assignments |
2.4 Blocklists & keyword lists
| Method | Path | Purpose |
|---|---|---|
GET/POST /v1/compliance/blocklists | CRUD on blocklists | |
GET/PUT/DELETE /v1/compliance/blocklists/{id} | CRUD | |
GET /v1/compliance/blocklists/{id}/entries | Paginated entries | |
POST /v1/compliance/blocklists/{id}/entries | Add one | |
POST /v1/compliance/blocklists/{id}/entries:bulk | Add ≤ 10,000 | |
DELETE /v1/compliance/blocklists/{id}/entries/{entryId} | Remove one | |
GET/POST /v1/compliance/keyword-lists | CRUD | |
POST /v1/compliance/keyword-lists/{id}/import | CSV/JSON bulk import | |
GET /v1/compliance/keyword-lists/{id}/export | CSV/JSON export |
All endpoints: platform.compliance.admin.
2.5 Tenant scoring
| Method | Path | Role | Purpose |
|---|---|---|---|
| GET | /v1/compliance/tenants | platform.compliance.admin or platform.auditor | Ranked list (sort by overallScore asc/desc, filter by tier) |
| GET | /v1/compliance/tenants/{tenantId}/score | same | Current score breakdown |
| GET | /v1/compliance/tenants/{tenantId}/score/history | same | Time series (last N days) |
| GET | /v1/compliance/tenants/{tenantId}/violations | same | Violation log derived from evaluation_log |
| POST | /v1/compliance/tenants/{tenantId}/tier-override | platform.compliance.admin | Body: { tier, reason, expiresAt? }. Emits compliance.tenant.tier.changed.v1. |
| DELETE | /v1/compliance/tenants/{tenantId}/tier-override | platform.compliance.admin | Clear override |
Tenant (portal):
| Method | Path | Role | Purpose |
|---|---|---|---|
| GET | /v1/portal/compliance/score | Tenant user | Current score + tier + guidance copy (own tenant only) |
| GET | /v1/portal/compliance/score/history | Tenant user | 30-day history |
2.6 Reports
| Method | Path | Role | Purpose |
|---|---|---|---|
| POST | /v1/compliance/reports | platform.auditor or platform.compliance.admin | Request an async report |
| GET | /v1/compliance/reports | same | List |
| GET | /v1/compliance/reports/{id} | same | Status + output URL when READY |
| GET | /v1/compliance/reports/{id}/download | same | Pre-signed URL redirect to object storage |
2.7 Audit log query
| Method | Path | Role | Purpose |
|---|---|---|---|
| GET | /v1/compliance/audit-log | platform.auditor, platform.compliance.admin | Filter by entityType, entityId, actorUserId, date range. Cursor pagination. Read-only. |
2.8 Internal / operational
| Method | Path | Caller | Purpose |
|---|---|---|---|
| GET | /health/live, /health/ready | Kubernetes | Liveness / readiness |
| GET | /metrics | Prometheus | Scrape |
| GET | /v1/compliance/schemas/{ruleType} | Any authenticated caller | Per-type config JSON schema (useful for admin-dashboard form rendering) |
3. Error shape (REST)
Uniform envelope across all REST endpoints:
{
"error": {
"code": "COMPLIANCE_VALIDATION_FAILED",
"message": "Regex pattern exceeds 500 chars",
"details": {
"field": "config.pattern",
"max": 500
},
"traceId": "00-abc-…"
}
}
| HTTP | Code | When |
|---|---|---|
| 400 | COMPLIANCE_VALIDATION_FAILED | Payload or type-specific config invalid |
| 401 | UNAUTHENTICATED | Missing/invalid JWT (Kong returned it upstream) |
| 403 | INSUFFICIENT_SCOPE | Caller role lacks required compliance scope |
| 404 | NOT_FOUND | Rule/rule-set/hold/report not found |
| 409 | CONFLICT | Version mismatch (rule updated concurrently); hold already reviewed; rule referenced by COMPOSITE; default rule set collision |
| 422 | COMPOSITE_CYCLE_DETECTED | Creating/updating a COMPOSITE rule would form a cycle |
| 422 | REGEX_REDOS_RISK | Regex failed ReDoS screen |
| 429 | RATE_LIMITED | Kong rate-limit |
| 500 | INTERNAL | Unhandled internal error |
| 503 | DEPENDENCY_UNAVAILABLE | Postgres / Redis / NATS unavailable |
4. Versioning
- gRPC:
ghasi.sms.compliance.v1. Breaking change →v2in parallel with a ≥ 90-day deprecation window. - REST:
/v1/compliance/*. Additive changes are non-breaking. Breaking change →/v2/compliance/*. schemaVersionfield on events per EVENT_SCHEMAS §7.
5. Rate limits & quotas (Kong)
| Surface | Limit |
|---|---|
| Admin REST (platform role) | 600 req/min per user |
Admin bulk endpoints (/hold-queue/bulk-review, blocklist bulk) | 10 req/min per user |
| Tenant portal compliance reads | 120 req/min per tenant |
| Report generation | 10 requests/hour per user |
gRPC EvaluateCompliance | No Kong limit (internal, mTLS); per-pod concurrency governed by the gRPC server config (default 1000 in-flight) |
6. OpenAPI / Proto artefacts
- Generated OpenAPI 3.1 document served at
GET /v1/compliance/openapi.jsonand published to the internal API registry on each deploy. .protofile versioned in this repository; generated TypeScript + Go clients published to the internal package registry.- Contract tests (Pact) verify the orchestrator ↔ compliance-engine gRPC contract and admin-dashboard ↔ compliance-engine REST contract on every CI run.