Number Intelligence Service — Event Schemas
Version: 1.0 Status: Draft Owner: Messaging Core Last Updated: 2026-04-21 Companion: DOMAIN_MODEL · SYNC_CONTRACT · SECURITY_MODEL
All events are published to NATS JetStream via the transactional outbox pattern — every emitted event corresponds to a row in numint.outbox written in the same Postgres transaction as the source state change (per SYNC_CONTRACT §4). Every event carries schemaVersion, eventId (UUIDv4), traceId (W3C), and at (RFC 3339 UTC).
Payloads are PII-minimal. MSISDN appears on NATS only as msisdnHash (sha256(msisdn || pepper)) and, where policy permits, msisdnMasked (e.g. +93701***). Raw MSISDN never traverses NATS. IMEIs likewise appear only as imeiHash.
1. Streams, subjects, retention
| Stream | Subjects | Retention | Replicas | Dedup window |
|---|---|---|---|---|
NUMBER_INTELLIGENCE_EVENTS | numint.attribution.changed.v1, numint.mnp.changed.v1, numint.mnp.divergence.v1, numint.hlr_probe.completed.v1, numint.cache.refreshed.v1 | 13 months | 3 | 2 min |
NUMINT_RECONCILIATION | numint.reconciliation.completed.v1, numint.reconciliation.conflict.v1 | 90 days | 3 | 5 min |
NUMINT_EIR | numint.eir.flagged.v1, numint.eir.cleared.v1 | 365 days | 3 | 5 min |
NUMINT_AUDIT_OPS | numint.audit.v1, numint.audit.chain_verified.v1, numint.audit.chain_broken.v1 | 13 months hot + 7 y cold (regulator) | 3 | 2 min |
NUMINT_BILLING | numint.lookup.billed.v1 | 13 months | 3 | 2 min |
Each stream has a sibling *.deadletter subject. Consumers are durable with explicit ack per PLT-REQ-008/009. JetStream cluster topology follows ADR-0004 §3 (active-active Kabul / Mazar for this control-plane-adjacent service).
2. Produced events
2.1 numint.attribution.changed.v1
Emitted when NumberRecord.mnoId or mnpStatus changes relative to the previous persisted row. Not emitted on VLR/IMSI-only refreshes.
{
"schemaVersion": "1",
"eventId": "0193abcd-…",
"msisdnHash": "8d4f…e91a",
"msisdnMasked": "+93701***",
"country": "AF",
"previous": { "mno": "afghan-wireless", "mnpStatus": "NATIVE" },
"current": { "mno": "mtn-afghanistan", "mnpStatus": "PORTED_IN", "originalMno": "afghan-wireless" },
"source": "MNP_RECON",
"confidence": "HIGH",
"version": 42,
"traceId": "00-abc-def-01",
"at": "2026-04-21T03:01:27.812Z"
}
Consumers: routing-engine (warms its per-MNO connector routing cache), sms-firewall-service (invalidates origin-MNO rules), fraud-intel-service (feeds churn detector).
JSON Schema (draft-2020-12):
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ghasi.platform/schemas/numint/attribution.changed.v1.json",
"type": "object",
"required": ["schemaVersion","eventId","msisdnHash","current","source","at"],
"properties": {
"schemaVersion": { "const": "1" },
"eventId": { "type": "string", "format": "uuid" },
"msisdnHash": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
"msisdnMasked": { "type": "string" },
"country": { "type": "string", "pattern": "^[A-Z]{2}$" },
"previous": { "$ref": "#/$defs/MnoSnapshot" },
"current": { "$ref": "#/$defs/MnoSnapshot" },
"source": { "enum": ["LIVE_HLR_MAP","LIVE_HLR_REST","MNP_RECON","MNO_HLR_DUMP","ADMIN_OVERRIDE"] },
"confidence": { "enum": ["HIGH","MEDIUM","LOW","UNKNOWN"] },
"version": { "type": "integer" },
"traceId": { "type": "string" },
"at": { "type": "string", "format": "date-time" }
},
"$defs": {
"MnoSnapshot": {
"type": "object",
"required": ["mno","mnpStatus"],
"properties": {
"mno": { "type": "string" },
"originalMno": { "type": ["string","null"] },
"mnpStatus": { "enum": ["NATIVE","PORTED_IN","PORTED_OUT","UNKNOWN"] }
}
}
}
}
2.2 numint.mnp.changed.v1
Per-port-transition event. Fires once per inserted PortabilityRecord.
{
"schemaVersion": "1",
"eventId": "…",
"portId": "ni_01HZX7…",
"msisdnHash": "8d4f…",
"msisdnMasked": "+93701***",
"donorMno": "afghan-wireless",
"recipientMno": "mtn-afghanistan",
"direction": "IN",
"portDate": "2026-04-19",
"sourceFeed": "mtn-afghanistan-mnp-2026-04-20.csv",
"reconRunId": "rcn_01HZX7…",
"fileSha256": "bd…",
"traceId": "…",
"at": "2026-04-21T03:01:27Z"
}
2.3 numint.mnp.divergence.v1
Fires when LookupPorting or ResolveMsisdn detects the MNP registry vs a recent live-HLR observation disagree.
{
"schemaVersion": "1",
"eventId": "…",
"msisdnHash": "8d4f…",
"mnpMno": "mtn-afghanistan",
"hlrMno": "afghan-wireless",
"lastSeenHlrAt": "2026-04-19T11:22:33Z",
"portDate": "2026-04-10",
"severity": "HIGH",
"traceId": "…",
"at": "2026-04-21T09:15:00Z"
}
Consumed by fraud-intel-service as a SIM-swap / MNP-laundering signal.
2.4 numint.reconciliation.completed.v1
Per-MNO daily (or on-demand) run summary.
{
"schemaVersion": "1",
"eventId": "…",
"runId": "rcn_01HZX7…",
"kind": "MNP",
"mnoId": "mtn-afghanistan",
"fileSha256": "bd…",
"totalRecords": 47322,
"accepted": 47298,
"rejected": 24,
"conflictsCount": 5,
"durationMs": 287340,
"prevChainHash": "ab…",
"recordHash": "cd…",
"status": "COMPLETED",
"at": "2026-04-21T03:01:27Z"
}
2.5 numint.reconciliation.conflict.v1
One event per new unresolved conflict.
{
"schemaVersion": "1",
"eventId": "…",
"conflictId": "cfl_01HZX7…",
"msisdnHash": "8d4f…",
"candidateA": { "mno": "afghan-wireless", "portDate": "2026-04-18", "sourceFeed": "afghan-wireless-mnp-2026-04-19.csv" },
"candidateB": { "mno": "mtn-afghanistan", "portDate": "2026-04-19", "sourceFeed": "mtn-afghanistan-mnp-2026-04-20.csv" },
"severity": "HIGH",
"at": "2026-04-21T03:01:28Z"
}
2.6 numint.hlr_probe.completed.v1
One per live HLR probe — supports fraud-intel VLR-change correlation.
{
"schemaVersion": "1",
"eventId": "…",
"probeId": "prb_01HZX7…",
"msisdnHash": "8d4f…",
"mnoId": "afghan-wireless",
"transport": "MAP_SRI_SM",
"status": "OK",
"vlrChanged": true,
"previousVlr": "93-KBL-VLR-002",
"currentVlr": "93-MZR-VLR-007",
"durationMs": 284,
"at": "2026-04-21T09:15:00Z"
}
2.7 numint.eir.flagged.v1 / numint.eir.cleared.v1
Fires on EirRecord.effectiveStatus transition into / out of BLACKLIST.
{
"schemaVersion": "1",
"eventId": "…",
"imeiHash": "f1…",
"previousStatus": "WHITELIST",
"currentStatus": "BLACKLIST",
"reasonCode": "STOLEN",
"reportedBy": ["ATRA","mtn-afghanistan"],
"at": "2026-04-21T04:00:00Z"
}
Consumed by sms-firewall-service for MT recipient-device policy and by fraud-intel-service.
2.8 numint.lookup.billed.v1
Per-chargeable-call metering event. Consumed by billing-service.
{
"schemaVersion": "1",
"eventId": "0193abcd-…",
"tenantId": "11111111-2222-3333-4444-555555555555",
"sku": "lookup.fresh.v1",
"quantity": 1,
"requestId": "00-abc-def-01",
"msisdnHashTenantSalted": "9a…",
"tier": "LIVE",
"source": "LIVE_HLR_MAP",
"occurredAt": "2026-04-21T10:14:22.812Z",
"at": "2026-04-21T10:14:22.900Z"
}
NATS Msg-Id: eventId ensures idempotent consumption in billing-service.
2.9 numint.cache.refreshed.v1
Ops marker; useful for dashboard annotations.
{
"schemaVersion": "1", "eventId": "…",
"kind": "warm_on_deploy",
"region": "af-kabul-1",
"keys": 500000,
"durationMs": 18732,
"at": "2026-04-21T06:05:00Z"
}
2.10 numint.audit.v1
Administrative state-change mirror of AuditLog.
{
"schemaVersion": "1", "eventId": "…",
"entityType": "MNP_CONFLICT",
"entityId": "cfl_01HZX7…",
"action": "RESOLVE",
"actorUserId": "aa…",
"before": { "resolution": null },
"after": { "resolution": "B_WINS", "note": "MTN file dated later; HLR probe confirms recipient" },
"at": "2026-04-21T14:30:00Z"
}
2.11 numint.audit.chain_verified.v1 / numint.audit.chain_broken.v1
chain_verified.v1:
{
"schemaVersion": "1", "eventId": "…",
"verifierRunId": "cvr_01HZX7…",
"chainKind": "LOOKUP_AUDIT",
"windowFrom": "2026-04-20T00:00:00Z",
"windowTo": "2026-04-21T00:00:00Z",
"rowsChecked": 47829143,
"at": "2026-04-21T04:30:00Z"
}
chain_broken.v1 adds firstBadSeq, auditId, expectedPrevHash, actualPrevHash, partition, severity: CRITICAL.
3. Consumed events (minimal)
| Stream / subject | Purpose |
|---|---|
operator.config.changed.v1 | Refresh MnoSnapshot cache (endpoints, TPS quotas) |
billing.tenant.plan.changed.v1 | Refresh TenantLookupQuota caps |
auth.tenant.lifecycle.v1 | Reject writes for SUSPENDED tenants |
MNO MNP files arrive via SFTP (not NATS) — see APPLICATION_LOGIC UC-MnpReconciliationDaily.
4. Schema evolution
- New fields → additive, backwards-compatible; same subject.
- Breaking changes → new subject
numint.<topic>.v2; both published during 90-day deprecation window; schema registry holds both. - Enum additions → permitted; consumers MUST ignore unknown enum values (fail-open on enum).
- Payload field removal → BREAKING; requires
v2.
All schemas registered in the platform schema registry (docs/standards/EVENT_NAMING_AND_VERSIONING.md). Producer and consumer SDKs generated from the registry on each CI run.
5. Dead-letter handling
Per-subject *.deadletter catches messages that NAK'd 3 times. An SRE runbook numint-dlq.md covers inspection, replay, and discard. The NumIntEventsDlqGrowing alert fires when any *.deadletter subject grows > 100 messages in 10 min.
6. Retention summary
| Subject | Hot (JetStream) | Cold (S3) | Rationale |
|---|---|---|---|
numint.mnp.changed.v1 | 13 months | — | MNP PortabilityRecord is the durable source; event is delivery |
numint.attribution.changed.v1 | 13 months | — | Tenant / routing consumers replay ≤ 13 m |
numint.reconciliation.completed.v1 | 90 days | — | Operational |
numint.reconciliation.conflict.v1 | 90 days | — | Operational; conflicts themselves retained in PG indefinitely |
numint.hlr_probe.completed.v1 | 30 days | — | Fraud correlation window |
numint.eir.flagged.v1 | 365 days | — | Regulator inquiries |
numint.audit.v1 | 13 months | 7 y Object Lock | Regulator requirement |
numint.lookup.billed.v1 | 13 months | — | Billing dispute window |
numint.cache.refreshed.v1 | 30 days | — | Ops marker |
Cold archive flow for audit: daily cron exports partitions > 13 months to s3://ghasi-numint-audit-cold/ with SSE-KMS + Object Lock governance mode for 7 y, then drops the partition.