Skip to main content

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 kblmzr (sender-ID is control-plane data) and the audit subset is leaf-mirrored to dxb for immutable WORM archival.


1. Streams, subjects, retention

StreamSubjectsRetentionReplicasMirroringDeduplication window
SENDER_ID_EVENTSsender.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.v113 months (regulatory evidence)3kbl ↔ mzr mirror; dxb leaf5 min
SENDER_ID_REPUTATIONsender.id.reputation.changed.v190 days3kbl ↔ mzr5 min
SENDER_ID_REGULATORsender.id.regulator.exported.v17 years (regulator archival)3kbl ↔ mzr ↔ dxb (WORM)
SENDER_ID_CACHE_INVALIDATEsender.id.cache.invalidate1 hour3region-local5 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

SubjectProducerAction
compliance.message.blocked.v1compliance-engineReputation delta (per UC-15); inc complianceHits window counter
compliance.message.held.v1compliance-engineReputation delta (lighter weight than blocked)
fraud.detected.ait.v1fraud-intel-serviceReputation delta × 1
fraud.detected.simbox.v1fraud-intel-serviceReputation delta × 2
fraud.detected.otp_harvesting.v1fraud-intel-serviceReputation delta × 3
firewall.alert.v1sms-firewall-serviceReputation delta if alert references a sender-ID
regulator.complaint.received.v1regulator-portal-serviceinc complaints counter; reputation delta × 5 weighting
dlr.aggregate.v1dlr-processor (hourly roll-up)Update deliveryRate window
auth.user.erased.v1auth-serviceTombstone affected KycDocument rows; redact PII fields per SECURITY_MODEL §9
numint.mnp.changed.v1number-intelligence-serviceRefresh LONG-type sender-ID port-status cache (cosmetic only — does not change registry state)

4. Consumer contracts (by subscribing service)

ServiceSubjectsAction
compliance-enginesender.id.activated.v1, .suspended.v1, .reactivated.v1, .revoked.v1, sender.id.cache.invalidateLocal cache invalidation; SENDER_ID rule re-eval flag
routing-enginesame as compliance-engineLocal cache invalidation; last-mile veto
sms-firewall-servicesameLocal cache invalidation; inbound MT rules
notification-servicesender.id.kyc_approved.v1, .kyc_rejected.v1, .info_requested.v1, .activated.v1, .suspended.v1, .reactivated.v1, .revoked.v1, .reputation.changed.v1Tenant compliance officer notifications
admin-dashboard (SSE)all sender.id.*Live reviewer queue + state-change banners (per EP-ADMDASH-11)
analytics-serviceall sender.id.*Long-term archival, registry growth dashboards
regulator-portal-servicesender.id.regulator.exported.v1Mirror to regulator audit log
customer-portal (SSE proxy)tenant-scoped subset of own sender-IDsTenant-side live status

5. PII & security rules for events

  • No KYC content. Events carry kycDocId references at most. Fetching the artefact requires authenticated REST call with audit logging.
  • No registrant PII beyond registrantOrgName (legal entity name, public registry data).
  • registrantContactEmail and registrantContactMsisdn are 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 in dxb per 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 SidOutboxLag fires if count(outbox WHERE published_at IS NULL) > 1000 for > 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 off v1 before it is removed.
  • Enum values are additive. Consumers MUST treat unknown enum values as UNKNOWN and not fail.
  • New event subjects (e.g. sender.id.transferred.v1 if cross-tenant transfers are ever introduced) are non-breaking; consumers opt in by subscribing.