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:
- gRPC on
:50061(mTLS with national-PKI chain required) — invoked by authorised government callers (civil-defence, NDMA, police) and byregulator-portal-service. This is the hot path. - HTTPS REST on
:3061— fronted by Kong with JWT + RBAC — foradmin-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)
| Header | Value | Notes |
|---|---|---|
content-type | application/grpc+proto | Standard |
x-gov-signature | base64(PKCS#7 detached signature over canonical JSON of request) | Mandatory; HSM-verified per UC-01 step 4 |
x-trace-id | W3C TraceContext traceparent propagation | Propagated through audit |
Error mapping
| gRPC status | Condition | Caller action |
|---|---|---|
OK | Success | Act on response |
INVALID_ARGUMENT | Language coverage, page limit, polygon oversized, unknown cells | Fix payload and resubmit |
UNAUTHENTICATED | Signature invalid / cert expired / clock skew / replay | Regenerate signature; rotate cert if expired |
PERMISSION_DENIED | Caller not registered or scope violation | Contact Ghasi to update registry (requires mouRef amendment) |
FAILED_PRECONDITION | HSM unavailable | Retry after 30 s; if persists, emergency runbook (manual call to MNO NOCs) |
RESOURCE_EXHAUSTED | Per-caller rate limit exceeded | Back off per retry_after_ms |
UNAVAILABLE | Service unhealthy | Retry with full jitter; after 3 attempts, escalate |
DEADLINE_EXCEEDED | Handler > caller deadline | Retry; audit row persisted regardless |
Rate limits. Enforced via Redis sliding-window sorted sets (cbc:rl:{callerId}:{minuteBucket}):
| Severity | Per-caller default |
|---|---|
P0_EXTREME | 10 reqs/min |
P1_MAJOR | 60 reqs/min |
P2_ADVISORY | 120 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
| Method | Path | Role | Purpose |
|---|---|---|---|
| GET | /v1/cbc/broadcasts | platform.regulator.read or platform.cbc.admin | List with cursor pagination + filters (callerId, severity, isDrill, from, to, state) |
| GET | /v1/cbc/broadcasts/{broadcastId} | same | Single broadcast with per-MNO breakdown |
| GET | /v1/cbc/broadcasts/{broadcastId}/audit | platform.auditor | Full hash-chain back to genesis of partition |
| GET | /v1/cbc/broadcasts/{broadcastId}/pdus | platform.cbc.admin | CBS 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
| Method | Path | Role | Purpose |
|---|---|---|---|
| GET | /v1/cbc/callers | platform.cbc.admin | List registered callers |
| GET | /v1/cbc/callers/{callerId} | same | Single caller (with history pointer) |
| GET | /v1/cbc/callers/{callerId}/history | same | Append-only change log |
| POST | /v1/cbc/callers | platform.cbc.admin + Legal counter-approval | Create caller (requires mouRef; see §2.2.1) |
| PUT | /v1/cbc/callers/{callerId} | same | Update; new history row appended |
| POST | /v1/cbc/callers/{callerId}/deactivate | platform.cbc.admin | Soft-disable (idempotent) |
| POST | /v1/cbc/callers/{callerId}/verify | platform.cbc.admin | UC-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
| Method | Path | Role | Purpose |
|---|---|---|---|
| POST | /v1/cbc/drills | platform.cbc.admin | Schedule a drill (scheduledAt, geoTarget, bodyVariants) |
| GET | /v1/cbc/drills | platform.cbc.admin or platform.regulator.read | List scheduled + executed drills |
| GET | /v1/cbc/drills/{drillId} | same | Single drill with reportRef |
| GET | /v1/cbc/drills/{drillId}/report | platform.regulator.read or platform.cbc.admin | Pre-signed URL to after-action PDF |
| DELETE | /v1/cbc/drills/{drillId} | platform.cbc.admin | Cancel a scheduled drill (only before execution) |
2.4 Cell-Database Admin
| Method | Path | Role | Purpose |
|---|---|---|---|
| GET | /v1/cbc/cell-db/status | platform.cbc.admin | Per-MNO snapshot version, row count, age, coverage % |
| POST | /v1/cbc/cell-db/{mnoId}/refresh | platform.cbc.admin | Trigger refresh cron immediately (idempotent per day) |
| POST | /v1/cbc/cell-db/{mnoId}/upload | platform.cbc.admin | Manual upload fallback (multipart/form-data CSV/JSON) |
| POST | /v1/cbc/cell-db/{mnoId}/rollback | platform.cbc.admin + CISO approval | Roll back to prior snapshotVersion (last 90 d) |
2.5 Signature-Audit Queries
| Method | Path | Role | Purpose |
|---|---|---|---|
| GET | /v1/cbc/signature-audit | platform.auditor | Filter by result, date range, presentedCertSubject |
| GET | /v1/cbc/signature-audit/metrics | platform.cbc.admin | Success/fail rate per hour, top failing cert subjects (CBC-US-010) |
2.6 Public Drill Feed
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /v1/cbc/public/drills | None (rate-limited by Kong anonymous quota) | Last 12 months of drill records, redacted (no caller cert subject) |
| GET | /v1/cbc/public/drills/feed | None | JSON feed consumed by status.ghasi.io/cbc-drills (CBC-US-017) |
| POST | /v1/cbc/public/drills/webhook-subscribe | Admin-issued subscriber token | Register HMAC-signed webhook for media partners (CBC-US-017) |
2.7 Internal / operational
| Method | Path | Caller | Purpose |
|---|---|---|---|
| GET | /health/live, /health/ready | Kubernetes | Liveness / readiness |
| GET | /metrics | Prometheus | Scrape on :3061/metrics |
| GET | /v1/cbc/openapi.json | Any authenticated | OpenAPI 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-..."
}
}
| HTTP | Code | When |
|---|---|---|
| 400 | CBC_VALIDATION_FAILED | Payload missing/invalid |
| 400 | CBC_LANGUAGE_COVERAGE_INSUFFICIENT | severity requires more languages than supplied |
| 400 | CBC_PAGE_LIMIT_EXCEEDED | Any language encodes to > 15 CBS pages |
| 400 | CBC_GEO_POLYGON_OVERSIZED | Polygon area > national area |
| 400 | CBC_GEO_UNKNOWN_CELLS | Cell-ID list contains unknown cells |
| 401 | CBC_SIGNATURE_INVALID | HSM C_Verify = SIGNATURE_INVALID |
| 401 | CBC_CLOCK_SKEW | ` |
| 401 | CBC_REPLAY_DETECTED | Nonce reused |
| 401 | CBC_CERT_REVOKED | OCSP or CRL indicates revoked |
| 403 | CBC_CALLER_NOT_REGISTERED | Cert subject not in registry |
| 403 | CBC_CALLER_SCOPE_VIOLATION | Severity / region outside caller scope |
| 403 | CBC_DUAL_APPROVAL_REQUIRED | Cancel / severity escalate pending second approver |
| 404 | CBC_NOT_FOUND | Broadcast / caller / drill / PDU not found |
| 409 | CBC_IDEMPOTENCY_CONFLICT | Same Idempotency-Key with different body |
| 409 | CBC_STATE_CONFLICT | Cancel requested after ACKED reached for any MNO |
| 422 | CBC_MOU_MISSING | Caller create without mouRef |
| 429 | CBC_RATE_LIMITED | Per-caller or Kong edge limit hit |
| 500 | CBC_INTERNAL | Unhandled internal error |
| 503 | CBC_HSM_UNAVAILABLE | HSM session unreachable |
| 503 | CBC_DEPENDENCY_UNAVAILABLE | Postgres / 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 →v2in parallel with ≥ 90-day deprecation window. - REST:
/v1/cbc/*. Additive changes (new optional fields) non-breaking. Breaking change →/v2/cbc/*with deprecation notice inSunsetHTTP header. - Event
schemaVersionper EVENT_SCHEMAS §9.
6. OpenAPI / Proto artefacts
- Generated OpenAPI 3.1 document served at
GET /v1/cbc/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
regulator-portal-service↔cbc-bridge-servicegRPC contract andadmin-dashboard↔cbc-bridge-serviceREST contract on every CI run.
7. Rate limits & quotas
| Surface | Limit | Rationale |
|---|---|---|
gRPC BroadcastEmergency (P0_EXTREME) | 10 req/min per caller | CISO approval required to raise |
gRPC BroadcastEmergency (P1_MAJOR) | 60 req/min per caller | |
gRPC BroadcastEmergency (P2_ADVISORY) | 120 req/min per caller | |
gRPC CancelBroadcast | 30 req/min per caller | Window is always short |
| REST admin (read) | 600 req/min per user | |
| REST admin (bulk / cell-db refresh) | 10 req/min per user | Long-running |
| REST public drill feed | 60 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
| Caller | Auth plane | Identity | Example |
|---|---|---|---|
| Government client (NDMA / civil defence / police) | gRPC mTLS + national-PKI signature | CN=ndma.gov.af,O=NDMA,C=AF | UC-01 |
regulator-portal-service | gRPC mTLS (platform SPIRE SVID) + relayed caller signature | SPIFFE ID spiffe://ghasi/regulator-portal-service | UC-01 (on behalf of regulator) |
admin-dashboard | REST JWT (role platform.cbc.admin) | User subject from Keycloak | §2 |
notification-service | NATS consumer | Stream CBC_EVENTS | §2.6 drill webhook fan-out |
analytics-service | NATS consumer | Stream CBC_EVENTS | Long-term analytics |