Skip to main content

cbc-bridge-service — API Contracts

Version: 1.0 Status: Draft Owner: Government / Emergency Last Updated: 2026-04-21

Companion: SYNC_CONTRACT · APPLICATION_LOGIC · SECURITY_MODEL · EVENT_SCHEMAS · docs/standards/ERROR_CODES.md

The service exposes two interface planes:

  1. gRPC on :50061 (mTLS with national-PKI chain required) — invoked by authorised government callers (civil-defence, NDMA, police) and by regulator-portal-service. This is the hot path.
  2. HTTPS REST on :3061 — fronted by Kong with JWT + RBAC — for admin-dashboard (regulator workbench, caller-registry CRUD, drill scheduling, audit query, cell-DB admin).

Both surfaces share the same domain aggregates but serve different actors.


1. gRPC service — CbcBridgeService.v1

Full .proto lives in SYNC_CONTRACT §3. Reproduced here for reference with field-level stability notes.

syntax = "proto3";
package ghasi.sms.cbc.v1;
option go_package = "github.com/ghasi/sms-gateway/cbc/v1";

service CbcBridgeService {
// Hot path. Authorised government caller only (mTLS + detached PKCS#7 signature).
rpc BroadcastEmergency(BroadcastEmergencyRequest)
returns (BroadcastEmergencyResponse);

// Poll live broadcast status (caller-of-record only).
rpc GetBroadcastStatus(GetBroadcastStatusRequest)
returns (BroadcastStatus);

// Dual-control cancel within the dispatch window.
rpc CancelBroadcast(CancelBroadcastRequest)
returns (CancelBroadcastResponse);

// Schedule a drill (admin caller; typically invoked by DrillSchedulerWorker).
rpc ScheduleDrill(ScheduleDrillRequest) returns (ScheduleDrillResponse);

// Diagnostic: verify a candidate caller cert + sample signature without persisting
// a broadcast. Used during onboarding (UC-10).
rpc VerifyAuthorisedCaller(VerifyAuthorisedCallerRequest)
returns (VerifyAuthorisedCallerResponse);
}

message BroadcastEmergencyRequest {
string request_id = 1; // UUIDv4 idempotency key
string headline = 2; // <= 60 chars
repeated LanguageVariant body_variants = 3;
GeoTarget geo_target = 4;
Severity severity = 5;
int32 expiry_minutes = 6;
bool is_drill = 7;
string nonce = 8; // 16-byte base64 random (anti-replay)
string requested_at = 9; // RFC 3339 UTC
}

message LanguageVariant {
string lang = 1; // 'en' | 'fa' | 'ps' | 'ar'
string body = 2; // UTF-8 body content
}

message GeoTarget {
oneof kind {
CellIds cell_ids = 1;
string polygon = 2; // GeoJSON Polygon/MultiPolygon as string
string region = 3; // ISO-3166-2 e.g. 'AF-KAB'
bool country = 4; // true = full national coverage
}
}
message CellIds { repeated string values = 1; }

enum Severity {
SEVERITY_UNSPECIFIED = 0;
P0_EXTREME = 1;
P1_MAJOR = 2;
P2_ADVISORY = 3;
}

message BroadcastEmergencyResponse {
string broadcast_id = 1;
string accepted_at = 2;
string expected_dispatch_by = 3;
string signature_audit_id = 4;
}

message BroadcastStatus {
string broadcast_id = 1;
string state = 2; // ACCEPTED|DISPATCHING|ACKED|PARTIAL|FAILED|CANCELLED
repeated MnoDispatchStatus per_mno = 3;
string accepted_at = 4;
string finalised_at = 5;
float coverage_pct = 6; // fraction of MNOs that ACKED
}
message MnoDispatchStatus {
string mno_id = 1;
string adapter_kind = 2;
string status = 3; // PENDING|DISPATCHED|ACKED|FAILED|TIMEOUT|REJECTED
int32 cell_count = 4;
int32 latency_ms = 5;
string error_code = 6;
string cbe_ack_reference = 7;
}

message CancelBroadcastRequest {
string broadcast_id = 1;
string approval_token = 2; // returned by first approver
string nonce = 3;
string requested_at = 4;
}
message CancelBroadcastResponse {
string state = 1; // 'AWAITING_APPROVAL' or 'CANCELLED'
repeated MnoDispatchStatus per_mno = 2;
}

Required headers (all gRPC methods)

HeaderValueNotes
content-typeapplication/grpc+protoStandard
x-gov-signaturebase64(PKCS#7 detached signature over canonical JSON of request)Mandatory; HSM-verified per UC-01 step 4
x-trace-idW3C TraceContext traceparent propagationPropagated through audit

Error mapping

gRPC statusConditionCaller action
OKSuccessAct on response
INVALID_ARGUMENTLanguage coverage, page limit, polygon oversized, unknown cellsFix payload and resubmit
UNAUTHENTICATEDSignature invalid / cert expired / clock skew / replayRegenerate signature; rotate cert if expired
PERMISSION_DENIEDCaller not registered or scope violationContact Ghasi to update registry (requires mouRef amendment)
FAILED_PRECONDITIONHSM unavailableRetry after 30 s; if persists, emergency runbook (manual call to MNO NOCs)
RESOURCE_EXHAUSTEDPer-caller rate limit exceededBack off per retry_after_ms
UNAVAILABLEService unhealthyRetry with full jitter; after 3 attempts, escalate
DEADLINE_EXCEEDEDHandler > caller deadlineRetry; audit row persisted regardless

Rate limits. Enforced via Redis sliding-window sorted sets (cbc:rl:{callerId}:{minuteBucket}):

SeverityPer-caller default
P0_EXTREME10 reqs/min
P1_MAJOR60 reqs/min
P2_ADVISORY120 reqs/min

P0_EXTREME limit cannot be raised without CISO + CTO sign-off.


2. REST — administrative surface

Base path: /v1/cbc. All endpoints fronted by Kong (jwt plugin + rate-limiting-advanced + cors with admin-only origins). Responses JSON; state-changing endpoints idempotent via Idempotency-Key header where noted.

2.1 Broadcasts

MethodPathRolePurpose
GET/v1/cbc/broadcastsplatform.regulator.read or platform.cbc.adminList with cursor pagination + filters (callerId, severity, isDrill, from, to, state)
GET/v1/cbc/broadcasts/{broadcastId}sameSingle broadcast with per-MNO breakdown
GET/v1/cbc/broadcasts/{broadcastId}/auditplatform.auditorFull hash-chain back to genesis of partition
GET/v1/cbc/broadcasts/{broadcastId}/pdusplatform.cbc.adminCBS PDUs as hex per MNO (regulator evidence export)

Query parameters for list: callerId, severity (comma-separated enum), state, isDrill (bool), from (RFC 3339), to (RFC 3339), cursor (opaque), limit (≤ 100). Sort: acceptedAt DESC by default.

Response envelope (list):

{
"items": [
{
"broadcastId": "bc_01HRB...",
"callerId": "caller_01HNK...",
"orgName": "NDMA",
"severity": "P0_EXTREME",
"isDrill": false,
"state": "ACKED",
"coveragePct": 1.0,
"geoTarget": { "kind": "REGION", "payload": "AF-KAB" },
"cbsMessageIdentifier": 4370,
"acceptedAt": "2026-04-21T09:12:00Z",
"finalisedAt": "2026-04-21T09:12:47Z",
"perMnoCounts": { "ACKED": 5, "FAILED": 0, "TIMEOUT": 0 }
}
],
"nextCursor": "eyJsYXN0QWNjZXB0ZWRBdCI6Ii4uLiJ9",
"total": 137
}

2.2 Authorised-Caller Registry

MethodPathRolePurpose
GET/v1/cbc/callersplatform.cbc.adminList registered callers
GET/v1/cbc/callers/{callerId}sameSingle caller (with history pointer)
GET/v1/cbc/callers/{callerId}/historysameAppend-only change log
POST/v1/cbc/callersplatform.cbc.admin + Legal counter-approvalCreate caller (requires mouRef; see §2.2.1)
PUT/v1/cbc/callers/{callerId}sameUpdate; new history row appended
POST/v1/cbc/callers/{callerId}/deactivateplatform.cbc.adminSoft-disable (idempotent)
POST/v1/cbc/callers/{callerId}/verifyplatform.cbc.adminUC-10 dry-run verify with sample signature

2.2.1 Create payload

{
"orgName": "National Disaster Management Authority",
"certSubject": "CN=ndma.gov.af,O=NDMA,C=AF",
"certFingerprintSha256": "aa11bb22cc33...",
"allowedSeverities": ["P0_EXTREME", "P1_MAJOR", "P2_ADVISORY"],
"allowedRegions": ["AF"],
"mouRef": "MOU-NDMA-GHASI-2026-007",
"notBefore": "2026-05-01T00:00:00Z",
"notAfter": "2027-05-01T00:00:00Z",
"dualControlPartners": ["caller_01HNK..."]
}

Validated against the national-PKI trust anchor (the certSubject's issuer chain MUST terminate at the configured root). Legal sign-off is recorded in the audit log via POST /v1/cbc/callers/{callerId}/legal-approve with signed body from Legal counsel (RS256 JWT in the approval chain).

2.3 Drills

MethodPathRolePurpose
POST/v1/cbc/drillsplatform.cbc.adminSchedule a drill (scheduledAt, geoTarget, bodyVariants)
GET/v1/cbc/drillsplatform.cbc.admin or platform.regulator.readList scheduled + executed drills
GET/v1/cbc/drills/{drillId}sameSingle drill with reportRef
GET/v1/cbc/drills/{drillId}/reportplatform.regulator.read or platform.cbc.adminPre-signed URL to after-action PDF
DELETE/v1/cbc/drills/{drillId}platform.cbc.adminCancel a scheduled drill (only before execution)

2.4 Cell-Database Admin

MethodPathRolePurpose
GET/v1/cbc/cell-db/statusplatform.cbc.adminPer-MNO snapshot version, row count, age, coverage %
POST/v1/cbc/cell-db/{mnoId}/refreshplatform.cbc.adminTrigger refresh cron immediately (idempotent per day)
POST/v1/cbc/cell-db/{mnoId}/uploadplatform.cbc.adminManual upload fallback (multipart/form-data CSV/JSON)
POST/v1/cbc/cell-db/{mnoId}/rollbackplatform.cbc.admin + CISO approvalRoll back to prior snapshotVersion (last 90 d)

2.5 Signature-Audit Queries

MethodPathRolePurpose
GET/v1/cbc/signature-auditplatform.auditorFilter by result, date range, presentedCertSubject
GET/v1/cbc/signature-audit/metricsplatform.cbc.adminSuccess/fail rate per hour, top failing cert subjects (CBC-US-010)

2.6 Public Drill Feed

MethodPathAuthPurpose
GET/v1/cbc/public/drillsNone (rate-limited by Kong anonymous quota)Last 12 months of drill records, redacted (no caller cert subject)
GET/v1/cbc/public/drills/feedNoneJSON feed consumed by status.ghasi.io/cbc-drills (CBC-US-017)
POST/v1/cbc/public/drills/webhook-subscribeAdmin-issued subscriber tokenRegister HMAC-signed webhook for media partners (CBC-US-017)

2.7 Internal / operational

MethodPathCallerPurpose
GET/health/live, /health/readyKubernetesLiveness / readiness
GET/metricsPrometheusScrape on :3061/metrics
GET/v1/cbc/openapi.jsonAny authenticatedOpenAPI 3.1 document, regenerated on each deploy

3. Error shape (REST)

Uniform envelope (platform standard per docs/standards/ERROR_CODES.md):

{
"error": {
"code": "CBC_CALLER_NOT_REGISTERED",
"message": "Certificate subject is not present in the authorised-caller registry.",
"details": {
"presentedCertSubject": "CN=example.gov.af,O=Example,C=AF",
"registryUpdatedAt": "2026-04-18T10:22:00Z"
},
"traceId": "00-abc-..."
}
}
HTTPCodeWhen
400CBC_VALIDATION_FAILEDPayload missing/invalid
400CBC_LANGUAGE_COVERAGE_INSUFFICIENTseverity requires more languages than supplied
400CBC_PAGE_LIMIT_EXCEEDEDAny language encodes to > 15 CBS pages
400CBC_GEO_POLYGON_OVERSIZEDPolygon area > national area
400CBC_GEO_UNKNOWN_CELLSCell-ID list contains unknown cells
401CBC_SIGNATURE_INVALIDHSM C_Verify = SIGNATURE_INVALID
401CBC_CLOCK_SKEW`
401CBC_REPLAY_DETECTEDNonce reused
401CBC_CERT_REVOKEDOCSP or CRL indicates revoked
403CBC_CALLER_NOT_REGISTEREDCert subject not in registry
403CBC_CALLER_SCOPE_VIOLATIONSeverity / region outside caller scope
403CBC_DUAL_APPROVAL_REQUIREDCancel / severity escalate pending second approver
404CBC_NOT_FOUNDBroadcast / caller / drill / PDU not found
409CBC_IDEMPOTENCY_CONFLICTSame Idempotency-Key with different body
409CBC_STATE_CONFLICTCancel requested after ACKED reached for any MNO
422CBC_MOU_MISSINGCaller create without mouRef
429CBC_RATE_LIMITEDPer-caller or Kong edge limit hit
500CBC_INTERNALUnhandled internal error
503CBC_HSM_UNAVAILABLEHSM session unreachable
503CBC_DEPENDENCY_UNAVAILABLEPostgres / Redis / NATS unavailable

4. Pagination

All list endpoints use cursor-based pagination (platform standard):

  • Request: ?cursor=<opaque>&limit=<≤100>.
  • Response: nextCursor (string, base64-encoded JSON) when more results exist.
  • Cursors encode (acceptedAt, broadcastId) — stable across inserts since we sort DESC.

5. Versioning

  • gRPC: ghasi.sms.cbc.v1. Breaking change → v2 in parallel with ≥ 90-day deprecation window.
  • REST: /v1/cbc/*. Additive changes (new optional fields) non-breaking. Breaking change → /v2/cbc/* with deprecation notice in Sunset HTTP header.
  • Event schemaVersion per EVENT_SCHEMAS §9.

6. OpenAPI / Proto artefacts

  • Generated OpenAPI 3.1 document served at GET /v1/cbc/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 regulator-portal-servicecbc-bridge-service gRPC contract and admin-dashboardcbc-bridge-service REST contract on every CI run.

7. Rate limits & quotas

SurfaceLimitRationale
gRPC BroadcastEmergency (P0_EXTREME)10 req/min per callerCISO approval required to raise
gRPC BroadcastEmergency (P1_MAJOR)60 req/min per caller
gRPC BroadcastEmergency (P2_ADVISORY)120 req/min per caller
gRPC CancelBroadcast30 req/min per callerWindow is always short
REST admin (read)600 req/min per user
REST admin (bulk / cell-db refresh)10 req/min per userLong-running
REST public drill feed60 req/min per source IP (Kong anonymous)
Webhook subscribe (public)5 req/min per source IP

All limits emit X-RateLimit-Limit / X-RateLimit-Remaining / Retry-After headers per platform standard.


8. Cross-service caller identities

CallerAuth planeIdentityExample
Government client (NDMA / civil defence / police)gRPC mTLS + national-PKI signatureCN=ndma.gov.af,O=NDMA,C=AFUC-01
regulator-portal-servicegRPC mTLS (platform SPIRE SVID) + relayed caller signatureSPIFFE ID spiffe://ghasi/regulator-portal-serviceUC-01 (on behalf of regulator)
admin-dashboardREST JWT (role platform.cbc.admin)User subject from Keycloak§2
notification-serviceNATS consumerStream CBC_EVENTS§2.6 drill webhook fan-out
analytics-serviceNATS consumerStream CBC_EVENTSLong-term analytics