sender-id-registry-service — Event Schemas
Version: 1.0 Status: Draft Owner: Trust & Safety + Regulator-facing 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 event carries schemaVersion, eventId (UUID), traceId, and at (RFC 3339 timestamp). Event bodies are PII-safe — KYC document content is never emitted; only metadata pointers (with role-gated retrieval).
Per ADR-0004 §13, sender.id.* streams are mirrored across kbl ↔ mzr (sender-ID is control-plane data) and the audit subset is leaf-mirrored to dxb for immutable WORM archival.
1. Streams, subjects, retention
| Stream | Subjects | Retention | Replicas | Mirroring | Deduplication window |
|---|---|---|---|---|---|
SENDER_ID_EVENTS | sender.id.submitted.v1, sender.id.kyc_approved.v1, sender.id.kyc_rejected.v1, sender.id.info_requested.v1, sender.id.verified.v1, sender.id.activated.v1, sender.id.suspended.v1, sender.id.reactivated.v1, sender.id.revoked.v1 | 13 months (regulatory evidence) | 3 | kbl ↔ mzr mirror; dxb leaf | 5 min |
SENDER_ID_REPUTATION | sender.id.reputation.changed.v1 | 90 days | 3 | kbl ↔ mzr | 5 min |
SENDER_ID_REGULATOR | sender.id.regulator.exported.v1 | 7 years (regulator archival) | 3 | kbl ↔ mzr ↔ dxb (WORM) | — |
SENDER_ID_CACHE_INVALIDATE | sender.id.cache.invalidate | 1 hour | 3 | region-local | 5 s |
All streams have a corresponding .deadletter suffix. Consumers are durable with explicit ack per platform standard.
2. Produced events
2.1 sender.id.submitted.v1
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["schemaVersion","eventId","senderIdInternalId","value","type","tenantId","registrantOrgName","traceId","at"],
"properties": {
"schemaVersion": { "const": "1" },
"eventId": { "type": "string", "format": "uuid" },
"senderIdInternalId": { "type": "string", "format": "uuid" },
"value": { "type": "string" },
"type": { "enum": ["ALPHA","SHORT","LONG"] },
"category": { "enum": ["BANKING","GOVERNMENT","HEALTHCARE","UTILITIES","MNO_INTERNAL","RETAIL","TRANSPORT","EDUCATION","OTHER"] },
"tenantId": { "type": "string", "format": "uuid" },
"registrantOrgName": { "type": "string" },
"restrictedPatternId": { "type": ["string","null"], "format": "uuid" },
"requiredVerificationLevel": { "enum": ["NONE","OTP","DOCUMENT","NOTARISED"] },
"kycDocCount": { "type": "integer", "minimum": 0 },
"submittedBy": { "type": "string", "format": "uuid" },
"traceId": { "type": "string" },
"at": { "type": "string", "format": "date-time" }
}
}
2.2 sender.id.kyc_approved.v1
{
"schemaVersion": "1",
"eventId": "...",
"senderIdInternalId": "sid_...",
"value": "BANK-XYZ",
"type": "ALPHA",
"tenantId": "t_...",
"reviewerUserId": "u_...",
"decisionNotes": "Verified DAB regulator letter; notarised authority valid",
"kycApprovedAt": "2026-04-22T13:14:00Z",
"traceId": "...",
"at": "2026-04-22T13:14:00Z"
}
2.3 sender.id.kyc_rejected.v1
Identical shape to kyc_approved.v1 but adds reasonCode (enum: IDENTITY_UNVERIFIED, DOCUMENT_FORGED, MISSING_REGULATOR_LETTER, IMPERSONATION_RISK, OTHER) and reasonDetail (free text, max 500 chars). Subscribers (notification-service) translate reasonCode to user-facing copy.
2.4 sender.id.info_requested.v1
Adds missingDocTypes: ["REGULATOR_LETTER", ...] and reviewerChecklist: string[] so customer-portal can render an itemised "what to submit next" panel.
2.5 sender.id.verified.v1
{
"schemaVersion": "1",
"eventId": "...",
"senderIdInternalId": "sid_...",
"value": "BANK-XYZ",
"type": "ALPHA",
"tenantId": "t_...",
"verificationId": "vrf_...",
"method": "DOMAIN_DNS",
"previousLevel": "NOTARISED",
"newLevel": "NOTARISED",
"newDomainDnsFlag": true,
"verifiedAt": "2026-04-23T08:01:42Z",
"traceId": "...",
"at": "2026-04-23T08:01:42Z"
}
2.6 sender.id.activated.v1
{
"schemaVersion": "1",
"eventId": "...",
"senderIdInternalId": "sid_...",
"value": "BANK-XYZ",
"type": "ALPHA",
"tenantId": "t_...",
"activatedBy": "u_...",
"currentVerificationLevel": "NOTARISED",
"hasDomainDns": true,
"category": "BANKING",
"activatedAt": "2026-04-23T08:30:00Z",
"traceId": "...",
"at": "2026-04-23T08:30:00Z"
}
2.7 sender.id.suspended.v1
{
"schemaVersion": "1",
"eventId": "...",
"senderIdInternalId": "sid_...",
"value": "BANK-XYZ",
"type": "ALPHA",
"tenantId": "t_...",
"trigger": "MANUAL",
"actorUserId": "u_...",
"reasonCode": "ABUSE_REPORTED",
"reasonDetail": "Multiple confirmed phishing complaints from regulator-portal-service",
"reputationAtSuspension": 22,
"suspendedAt": "2026-04-25T15:00:00Z",
"traceId": "...",
"at": "2026-04-25T15:00:00Z"
}
trigger ∈ {MANUAL, AUTO_REPUTATION, AUTO_FRAUD_HOOK}. On AUTO_REPUTATION, actorUserId = "system".
2.8 sender.id.reactivated.v1
{
"schemaVersion": "1",
"eventId": "...",
"senderIdInternalId": "sid_...",
"value": "BANK-XYZ",
"type": "ALPHA",
"tenantId": "t_...",
"reactivatedBy": "u_...",
"remediationEvidenceUrl": "s3://ghasi-evidence/...",
"probationUntil": "2026-05-25T15:00:00Z",
"reputationResetTo": 50,
"reactivatedAt": "2026-04-25T17:30:00Z",
"traceId": "...",
"at": "2026-04-25T17:30:00Z"
}
2.9 sender.id.revoked.v1
{
"schemaVersion": "1",
"eventId": "...",
"senderIdInternalId": "sid_...",
"value": "BANK-XYZ",
"type": "ALPHA",
"tenantId": "t_...",
"revokedBy": "u_...",
"reasonCode": "SEVERE_FRAUD",
"reasonDetail": "Confirmed coordinated phishing attack impersonating Da Afghanistan Bank",
"reservedUntil": "2027-04-25T15:00:00Z",
"revokedAt": "2026-04-25T15:30:00Z",
"traceId": "...",
"at": "2026-04-25T15:30:00Z"
}
2.10 sender.id.reputation.changed.v1
{
"schemaVersion": "1",
"eventId": "...",
"senderIdInternalId": "sid_...",
"value": "BANK-XYZ",
"type": "ALPHA",
"tenantId": "t_...",
"previousScore": 72,
"newScore": 28,
"previousBand": "GOOD",
"newBand": "AUTO_SUSPEND",
"trigger": "FRAUD_EVENT",
"triggerEventId": "fraud_...",
"inputs": {
"complianceHits7d": 18,
"complaints7d": 7,
"fraudHits7d": 4,
"deliveryRate7d": 0.91
},
"computedAt": "2026-04-25T11:00:00Z",
"traceId": "...",
"at": "2026-04-25T11:00:00Z"
}
Emitted only on band-boundary crossing (30/50/70/90). previousBand and newBand ∈ {AUTO_SUSPEND, POOR, NEUTRAL, GOOD, EXCELLENT}.
2.11 sender.id.regulator.exported.v1
{
"schemaVersion": "1",
"eventId": "...",
"exportId": "exp_...",
"windowFrom": "2026-04-20T00:00:00Z",
"windowTo": "2026-04-21T00:00:00Z",
"format": "JSONL",
"rowCount": 12482,
"s3Ref": "s3://ghasi-sid-regulator-export-kbl/exp_....jsonl",
"s3RefSignature": "s3://ghasi-sid-regulator-export-kbl/exp_....jsonl.sig",
"transmittedTo": "sftp://atra.gov.af/incoming/",
"acknowledgmentRef": "ATRA-RX-2026-04-21-0001",
"triggeredBy": "CRON",
"exportedAt": "2026-04-21T04:02:11Z",
"traceId": "...",
"at": "2026-04-21T04:02:11Z"
}
2.12 sender.id.cache.invalidate (operational broadcast)
Lightweight, not in main stream — short retention. Consumed by every sender-id-registry-service pod and by hot consumers (compliance-engine, routing-engine, sms-firewall-service) for fast in-process cache eviction.
{
"schemaVersion": "1",
"eventId": "...",
"senderIdInternalId": "sid_...",
"value": "BANK-XYZ",
"type": "ALPHA",
"tenantId": "t_...",
"reason": "STATE_CHANGED",
"newState": "SUSPENDED",
"at": "2026-04-25T15:00:00Z"
}
3. Consumed events
| Subject | Producer | Action |
|---|---|---|
compliance.message.blocked.v1 | compliance-engine | Reputation delta (per UC-15); inc complianceHits window counter |
compliance.message.held.v1 | compliance-engine | Reputation delta (lighter weight than blocked) |
fraud.detected.ait.v1 | fraud-intel-service | Reputation delta × 1 |
fraud.detected.simbox.v1 | fraud-intel-service | Reputation delta × 2 |
fraud.detected.otp_harvesting.v1 | fraud-intel-service | Reputation delta × 3 |
firewall.alert.v1 | sms-firewall-service | Reputation delta if alert references a sender-ID |
regulator.complaint.received.v1 | regulator-portal-service | inc complaints counter; reputation delta × 5 weighting |
dlr.aggregate.v1 | dlr-processor (hourly roll-up) | Update deliveryRate window |
auth.user.erased.v1 | auth-service | Tombstone affected KycDocument rows; redact PII fields per SECURITY_MODEL §9 |
numint.mnp.changed.v1 | number-intelligence-service | Refresh LONG-type sender-ID port-status cache (cosmetic only — does not change registry state) |
4. Consumer contracts (by subscribing service)
| Service | Subjects | Action |
|---|---|---|
compliance-engine | sender.id.activated.v1, .suspended.v1, .reactivated.v1, .revoked.v1, sender.id.cache.invalidate | Local cache invalidation; SENDER_ID rule re-eval flag |
routing-engine | same as compliance-engine | Local cache invalidation; last-mile veto |
sms-firewall-service | same | Local cache invalidation; inbound MT rules |
notification-service | sender.id.kyc_approved.v1, .kyc_rejected.v1, .info_requested.v1, .activated.v1, .suspended.v1, .reactivated.v1, .revoked.v1, .reputation.changed.v1 | Tenant compliance officer notifications |
admin-dashboard (SSE) | all sender.id.* | Live reviewer queue + state-change banners (per EP-ADMDASH-11) |
analytics-service | all sender.id.* | Long-term archival, registry growth dashboards |
regulator-portal-service | sender.id.regulator.exported.v1 | Mirror to regulator audit log |
customer-portal (SSE proxy) | tenant-scoped subset of own sender-IDs | Tenant-side live status |
5. PII & security rules for events
- No KYC content. Events carry
kycDocIdreferences at most. Fetching the artefact requires authenticated REST call with audit logging. - No registrant PII beyond
registrantOrgName(legal entity name, public registry data). registrantContactEmailandregistrantContactMsisdnare never in events — they live in DB only with role-gated read.- Event payloads are signed by the producer's RS256 key (HSM-held per ADR-0004 §11) and verified opportunistically by critical consumers (notification-service, regulator-portal-service).
- Audit-class events (
SENDER_ID_EVENTS,SENDER_ID_REGULATOR) are mirrored to immutable WORM indxbper ADR-0004.
6. Outbox pattern
All sender-ID state changes write an event row to sender_id_registry.outbox in the same Postgres transaction as the aggregate change. A relay polls outbox every 200 ms with FOR UPDATE SKIP LOCKED, publishes to NATS with retry, marks published_at, and emits sid_outbox_publish_total{result}.
This guarantees:
- No emitted events without a persisted state change.
- No unpublished state change for more than ~1 s under normal conditions.
- During NATS outage, outbox accumulates; relay drains when NATS returns; alert
SidOutboxLagfires ifcount(outbox WHERE published_at IS NULL) > 1000for > 5 min.
7. Schema evolution
- Additive fields with non-required defaults are non-breaking within the same
schemaVersion. - Breaking changes bump to
sender.<topic>.v2. Subjects coexist during a deprecation window (≥ 90 days); subscribers migrate offv1before it is removed. - Enum values are additive. Consumers MUST treat unknown enum values as
UNKNOWNand not fail. - New event subjects (e.g.
sender.id.transferred.v1if cross-tenant transfers are ever introduced) are non-breaking; consumers opt in by subscribing.