Skip to main content

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:

  1. gRPC on :50052 (mTLS required) — invoked by sms-orchestrator's NATS consumer. This is the hot path.
  2. HTTPS REST on :3013 — fronted by Kong; JWT + RBAC — for admin-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 statusConditionOrchestrator behaviour
OKVerdict returnedAct per verdict
INVALID_ARGUMENTMissing/malformed fieldAck message, update to BLOCKED with reason bad_input, raise alert
RESOURCE_EXHAUSTEDPer-pod concurrency cap hitDo not ack — redeliver via NATS
UNAVAILABLEConnect or deadlineDo not ack — redeliver
INTERNALHandler exceptionDo 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)

MethodPathRolePurpose
GET/v1/compliance/hold-queueplatform.compliance.reviewer or higherList PENDING/REVIEWING holds with pagination + filters
GET/v1/compliance/hold-queue/{holdId}platform.compliance.reviewer or higherSingle hold (body redacted unless role ≥ platform.compliance.admin)
POST/v1/compliance/hold-queue/{holdId}/reviewplatform.compliance.reviewerRelease or reject. Body: { action: 'RELEASE' | 'REJECT', notes?: string }. Idempotent on holdId.
POST/v1/compliance/hold-queue/bulk-reviewplatform.compliance.adminBulk action with filter (tenantId, ruleId, heldBefore). Hard cap: 10,000 rows. Returns job receipt.
GET/v1/compliance/hold-queue/metricsplatform.compliance.reviewer or higherQueue 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)

MethodPathRolePurpose
GET/v1/portal/compliance/held-messagesTenant user with sms:compliance:readList their own held/blocked messages — RLS-scoped to X-Tenant-Id, body never revealed
POST/v1/portal/compliance/held-messages/{holdId}/appealTenant adminSubmit an appeal — creates a support ticket tagged compliance-appeal

2.2 Rules

MethodPathRolePurpose
GET/v1/compliance/rulesplatform.compliance.adminPaginated list
GET/v1/compliance/rules/{ruleId}platform.compliance.adminCurrent rule
GET/v1/compliance/rules/{ruleId}/versionsplatform.compliance.adminVersion history
GET/v1/compliance/rules/{ruleId}/versions/{version}platform.compliance.adminRollback target
POST/v1/compliance/rulesplatform.compliance.adminCreate
PUT/v1/compliance/rules/{ruleId}platform.compliance.adminUpdate (bumps version)
POST/v1/compliance/rules/{ruleId}/enableplatform.compliance.adminEnable (idempotent)
POST/v1/compliance/rules/{ruleId}/disableplatform.compliance.adminDisable (idempotent)
DELETE/v1/compliance/rules/{ruleId}platform.compliance.adminSoft-delete (rejected if referenced by active COMPOSITE)
POST/v1/compliance/rules/{ruleId}/testplatform.compliance.adminDry-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

MethodPathRolePurpose
GET/v1/compliance/rule-setsplatform.compliance.admin
POST/v1/compliance/rule-setsplatform.compliance.adminCreate (status starts draft)
PUT/v1/compliance/rule-sets/{id}platform.compliance.adminUpdate
POST/v1/compliance/rule-sets/{id}/activateplatform.compliance.admindraft → active
POST/v1/compliance/rule-sets/{id}/retireplatform.compliance.adminactive → retired
POST/v1/compliance/rule-sets/{id}/set-defaultplatform.compliance.adminSwap the platform default (transactional; exactly one default at any moment)
GET/v1/compliance/tenants/{tenantId}/assignmentsplatform.compliance.admin
PUT/v1/compliance/tenants/{tenantId}/assignmentsplatform.compliance.adminUpsert list of assignments

2.4 Blocklists & keyword lists

MethodPathPurpose
GET/POST /v1/compliance/blocklistsCRUD on blocklists
GET/PUT/DELETE /v1/compliance/blocklists/{id}CRUD
GET /v1/compliance/blocklists/{id}/entriesPaginated entries
POST /v1/compliance/blocklists/{id}/entriesAdd one
POST /v1/compliance/blocklists/{id}/entries:bulkAdd ≤ 10,000
DELETE /v1/compliance/blocklists/{id}/entries/{entryId}Remove one
GET/POST /v1/compliance/keyword-listsCRUD
POST /v1/compliance/keyword-lists/{id}/importCSV/JSON bulk import
GET /v1/compliance/keyword-lists/{id}/exportCSV/JSON export

All endpoints: platform.compliance.admin.

2.5 Tenant scoring

MethodPathRolePurpose
GET/v1/compliance/tenantsplatform.compliance.admin or platform.auditorRanked list (sort by overallScore asc/desc, filter by tier)
GET/v1/compliance/tenants/{tenantId}/scoresameCurrent score breakdown
GET/v1/compliance/tenants/{tenantId}/score/historysameTime series (last N days)
GET/v1/compliance/tenants/{tenantId}/violationssameViolation log derived from evaluation_log
POST/v1/compliance/tenants/{tenantId}/tier-overrideplatform.compliance.adminBody: { tier, reason, expiresAt? }. Emits compliance.tenant.tier.changed.v1.
DELETE/v1/compliance/tenants/{tenantId}/tier-overrideplatform.compliance.adminClear override

Tenant (portal):

MethodPathRolePurpose
GET/v1/portal/compliance/scoreTenant userCurrent score + tier + guidance copy (own tenant only)
GET/v1/portal/compliance/score/historyTenant user30-day history

2.6 Reports

MethodPathRolePurpose
POST/v1/compliance/reportsplatform.auditor or platform.compliance.adminRequest an async report
GET/v1/compliance/reportssameList
GET/v1/compliance/reports/{id}sameStatus + output URL when READY
GET/v1/compliance/reports/{id}/downloadsamePre-signed URL redirect to object storage

2.7 Audit log query

MethodPathRolePurpose
GET/v1/compliance/audit-logplatform.auditor, platform.compliance.adminFilter by entityType, entityId, actorUserId, date range. Cursor pagination. Read-only.

2.8 Internal / operational

MethodPathCallerPurpose
GET/health/live, /health/readyKubernetesLiveness / readiness
GET/metricsPrometheusScrape
GET/v1/compliance/schemas/{ruleType}Any authenticated callerPer-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-…"
}
}
HTTPCodeWhen
400COMPLIANCE_VALIDATION_FAILEDPayload or type-specific config invalid
401UNAUTHENTICATEDMissing/invalid JWT (Kong returned it upstream)
403INSUFFICIENT_SCOPECaller role lacks required compliance scope
404NOT_FOUNDRule/rule-set/hold/report not found
409CONFLICTVersion mismatch (rule updated concurrently); hold already reviewed; rule referenced by COMPOSITE; default rule set collision
422COMPOSITE_CYCLE_DETECTEDCreating/updating a COMPOSITE rule would form a cycle
422REGEX_REDOS_RISKRegex failed ReDoS screen
429RATE_LIMITEDKong rate-limit
500INTERNALUnhandled internal error
503DEPENDENCY_UNAVAILABLEPostgres / Redis / NATS unavailable

4. Versioning

  • gRPC: ghasi.sms.compliance.v1. Breaking change → v2 in parallel with a ≥ 90-day deprecation window.
  • REST: /v1/compliance/*. Additive changes are non-breaking. Breaking change → /v2/compliance/*.
  • schemaVersion field on events per EVENT_SCHEMAS §7.

5. Rate limits & quotas (Kong)

SurfaceLimit
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 reads120 req/min per tenant
Report generation10 requests/hour per user
gRPC EvaluateComplianceNo 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.json and published to the internal API registry on each deploy.
  • .proto file 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.