Event Registry
The registry (packages/go/domain/eventregistry) is the single source of truth for every event contract on the bus: one directory per eventType holding its payload JSON Schema, state subjects, lineage contract, contract version (with retired versions kept in priorVersions for processing old events), the producers and consumers as code refs, and golden examples that are its test cases.
It runs at three levels: CI (the generic golden test), the e2e compliance gate (every wire event of a priced quote must validate), and runtime (the debugger attaches a contract verdict to every ingested event, so drift shows on live walks).
164 registered event types, grouped by topic.
billing.events
account.suspended
Emitted in the same tx as invoice.lapsed, keyed on the billing account: with no dedicated account.status column yet, the lapse escalation is the canonical suspension-for-non-payment transition, and reason is always non_payment. org_locator is joined from billing.accounts and can be empty for D2C accounts. No state: only the joined query row is in scope.
| Key | billing account locator (ACC-YYYY-NNNNNN) |
| Producers | billing (services/billing/internal/job/delinquency.go:processRow) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)document-service (services/document-service/internal/kafka/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType account_locator org_locator invoice_locator reason |
adjustment.applied
ApplyAdjustment moved a PENDING adjustment to APPLIED and appended the matching ledger entry (ADJUSTMENT_CREDIT with direction CREDIT, or ADJUSTMENT_DEBIT with direction DEBIT) in the same tx. No state: the adjustment struct in scope still holds its pre-transition PENDING status.
| Key | adjustment locator (ADJ-<uuid>) |
| Producers | billing (services/billing/internal/service/billing.go:ApplyAdjustment) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)document-service (services/document-service/internal/kafka/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType adjustment_locator type amount |
adjustment.created
CreateAdjustment wrote a PENDING account adjustment, keyed on the ADJ locator so created/applied/reversed share a partition. Adjustment locators are ADJ- plus a uuid, not the year-sequence shape other billing locators use. state freezes the adjustment row as created (type CREDIT or DEBIT, status PENDING).
| Key | adjustment locator (ADJ-<uuid>) |
| Producers | billing (services/billing/internal/service/billing.go:CreateAdjustment) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)document-service (services/document-service/internal/kafka/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["adjustment"] |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType adjustment_locator type amount |
adjustment.reversed
ReverseAdjustment moved an APPLIED adjustment to REVERSED and appended a REVERSAL ledger entry in the opposite direction, in the same tx. Minimal payload, no state: the adjustment struct in scope holds its pre-transition APPLIED status.
| Key | adjustment locator (ADJ-<uuid>) |
| Producers | billing (services/billing/internal/service/billing.go:ReverseAdjustment) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)document-service (services/document-service/internal/kafka/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType adjustment_locator |
charge.created
Billing wrote the charge row for a settled premium, enqueued in the same tx as the row create so the event can never exist without the charge it describes. Two flavours of one shape: the B2B funnel settle carries scheme_locator, the D2C direct purchase carries party_locator and quote_locator. Both flavours are PREMIUM charges already PAID at creation (paid-on-raise); amount is decimal major units serialized as a string, minor units live on the checkout events.
| Key | charge locator (CHG-YYYY-NNNNNN) |
| Producers | billing (services/billing/internal/service/onboarding.go:SettleFunnelPayment)billing (services/billing/internal/service/direct_purchase.go:SettleDirectPurchase) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)document-service (services/document-service/internal/kafka/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["charge"] |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType charge_locator account_id category amount currency status |
charge.voided
VoidCharge marked a charge VOID, with the outbox insert in the same tx as the status write. Minimal payload, no state: the charge struct in scope was loaded before the void and holds pre-transition fields. The notifications template key for this shape is charge.void, so the dispatcher logs SKIPPED on live events.
| Key | charge locator (CHG-YYYY-NNNNNN) |
| Producers | billing (services/billing/internal/service/billing.go:VoidCharge) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)document-service (services/document-service/internal/kafka/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType charge_locator |
installment.cancelled
CancelSchedule moved an installment schedule to CANCELLED, with the outbox insert in the same tx as the status write. Minimal payload, no state: the schedule struct in scope holds its pre-cancel fields.
| Key | installment schedule locator (ISC-YYYY-NNNNNN) |
| Producers | billing (services/billing/internal/service/installment.go:CancelSchedule) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)document-service (services/document-service/internal/kafka/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType schedule_locator |
installment.created
CreateSchedule wrote an ACTIVE installment schedule, keyed on the ISC locator so a schedule's created/cancelled events share a partition. installments is derived from frequency: monthly 12, quarterly 4, annually 1. state freezes the schedule row as created.
| Key | installment schedule locator (ISC-YYYY-NNNNNN) |
| Producers | billing (services/billing/internal/service/installment.go:CreateSchedule) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)document-service (services/document-service/internal/kafka/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["schedule"] |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType schedule_locator frequency installments total_amount currency |
invoice.delinquent
The delinquency job moved a FINALISED invoice past its grace period (grace_period_days, default 30) to DELINQUENT, with the outbox insert in the same tx as the status write. Job-driven, no client session. No state: only a query-row projection of the invoice is in scope.
| Key | invoice locator (INV-YYYY-NNNNNN) |
| Producers | billing (services/billing/internal/job/delinquency.go:processRow) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)document-service (services/document-service/internal/kafka/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType invoice_locator |
invoice.finalised
A DRAFT invoice moved to FINALISED: line-item amounts are summed into total_amount, every underlying charge is locked to INVOICED, and a due date 30 days out is stamped, all in one tx with the outbox insert. Plain payload event with no state snapshot: the invoice struct in scope at the emit site still holds its pre-transition DRAFT fields.
| Key | invoice locator (INV-YYYY-NNNNNN) |
| Producers | billing (services/billing/internal/service/billing.go:FinaliseInvoice) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)document-service (services/document-service/internal/kafka/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType invoice_locator total_amount due_date |
invoice.lapsed
The delinquency job moved a DELINQUENT invoice past twice its grace period to LAPSED, with the outbox insert in the same tx as the status write. account.suspended rides the same tx keyed on the account, since the lapse escalation is the canonical suspension transition. No state: only a query-row projection of the invoice is in scope.
| Key | invoice locator (INV-YYYY-NNNNNN) |
| Producers | billing (services/billing/internal/job/delinquency.go:processRow) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)document-service (services/document-service/internal/kafka/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType invoice_locator |
invoice.overdue
The lapse job (background tick, or the single-invoice TickInvoice ops path) found a FINALISED invoice past its due date with a live charge, lapsed the associated policy through enrollment's HTTP API, then emitted this event. The enqueue runs with tx nil because a local transaction cannot span the cross-service HTTP call; worst case the event is missed while the policy is already lapsed. No state: only a query-row projection of the invoice is in scope.
| Key | invoice locator (INV-YYYY-NNNNNN) |
| Producers | billing (services/billing/internal/job/lapse.go:processRow) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)document-service (services/document-service/internal/kafka/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType invoice_locator policy_locator |
invoice.paid
An invoice reached PAID. Three emit sites: the B2B funnel and D2C direct-purchase settles raise-and-pay in one tx (rich payload, state freezes the invoice), and RecordPayment marks an existing invoice paid once settled payments cover its total (payload is only invoice_locator, no state snapshot). That third site is why only eventType and invoice_locator are required and why state cannot be required. Consumed to render the INVOICE PDF receipt and the Payment Received email.
| Key | invoice locator (INV-YYYY-NNNNNN) |
| Producers | billing (services/billing/internal/service/onboarding.go:SettleFunnelPayment)billing (services/billing/internal/service/direct_purchase.go:SettleDirectPurchase)billing (services/billing/internal/service/billing.go:RecordPayment) |
| Consumers | document-service (services/document-service/internal/kafka/consumer.go)notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType invoice_locator |
invoice.void
VoidInvoice marked an invoice VOID, with the outbox insert in the same tx as the status write. The type is invoice.void, not invoice.voided: the emit-site literal is the contract. Minimal payload, no state: the invoice struct in scope holds pre-transition fields.
| Key | invoice locator (INV-YYYY-NNNNNN) |
| Producers | billing (services/billing/internal/service/billing.go:VoidInvoice) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)document-service (services/document-service/internal/kafka/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType invoice_locator |
payment.received
RecordPayment wrote a SETTLED payment against a FINALISED invoice through the manual/API path, with a CREDIT ledger entry in the same tx. When the settled sum covers the invoice total, invoice.paid rides the same transaction. Distinct from payment.settled, which the Stripe webhook settle paths emit. state freezes the payment row as constructed at the emit site.
| Key | payment locator (PAY-YYYY-NNNNNN) |
| Producers | billing (services/billing/internal/service/billing.go:RecordPayment) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)document-service (services/document-service/internal/kafka/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["payment"] |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType payment_locator invoice_locator amount |
payment.refunded
The reversal ledger entry written by VoidPayment returned the money to the payer. Billing has no separate refund method, so the void-with-reversal is the refund transition: this event is emitted in the same tx as payment.voided, keyed on the same payment, carrying the reversed amount and currency. No state: the payment struct in scope is pre-transition.
| Key | payment locator (PAY-YYYY-NNNNNN) |
| Producers | billing (services/billing/internal/service/billing.go:VoidPayment) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)document-service (services/document-service/internal/kafka/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType payment_locator amount currency |
payment.settled
The money actually moved: a first-class payment-journey event keyed on the PAYMENT (invoice.paid is keyed on the invoice), enqueued in the same tx as the payment row so it can never exist without the payment it describes. Emitted by both settle paths: B2B funnel with org_locator + scheme_locator, D2C direct purchase with party_locator + quote_locator. method is always CARD at both emit sites today; stripe_payment_intent carries the Stripe reference. Distinct from payment.received, which RecordPayment emits for manually recorded payments.
| Key | payment locator (PAY-YYYY-NNNNNN) |
| Producers | billing (services/billing/internal/service/onboarding.go:SettleFunnelPayment)billing (services/billing/internal/service/direct_purchase.go:SettleDirectPurchase) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)document-service (services/document-service/internal/kafka/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["payment"] |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType payment_locator invoice_locator method amount currency stripe_payment_intent |
payment.voided
VoidPayment moved a SETTLED payment to VOID and appended a DEBIT REVERSAL ledger entry in the same tx; a PAID invoice covered by the payment drops back to FINALISED. payment.refunded rides the same transaction, since the void-with-reversal is the refund transition. Minimal payload, no state: the payment struct in scope still holds its pre-void SETTLED fields. The notifications template key for this shape is payment.void, so the dispatcher logs SKIPPED on live events.
| Key | payment locator (PAY-YYYY-NNNNNN) |
| Producers | billing (services/billing/internal/service/billing.go:VoidPayment) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)document-service (services/document-service/internal/kafka/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType payment_locator |
quote.payment_failed
A D2C payment attempt failed: the quote_payments row was upserted to FAILED with the Stripe decline reason, so the checkout UI can show declined-and-retry and a payment-failed notification can react. The upsert never downgrades an already SETTLED row (ON CONFLICT WHERE guard), so a late failure event after a successful retry does not un-pay the quote. Issuance stays gated; the quote is not marked paid.
| Key | quote locator (QTE-YYYY-NNNNNN) |
| Producers | billing (services/billing/internal/repository/gorm_quote_payments.go:MarkQuoteFailed) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)document-service (services/document-service/internal/kafka/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["quotePayment"] |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType quoteLocator paymentIntentId partyLocator reason occurredAt |
quote.payment_initiated
A member started D2C checkout for a priced quote: billing created the Stripe PaymentIntent (mode one_off) or the Subscription whose first invoice carries the PaymentIntent (mode recurring) and announced it, closing the funnel's dark window between consent and the settle webhook (#1783). Enqueued with tx=nil since the Stripe object already exists; this is an announcement, not the settle, which stays webhook-driven. State freezes the just-created Stripe record under a mode-dependent subject (paymentIntent for one_off, subscription for recurring) with client_secret stripped, so the bus never carries payment authorisation material.
| Key | quote locator (QTE-YYYY-NNNNNN) |
| Producers | billing (services/billing/internal/handler/payment_intents.go:createD2CPaymentIntentHandler)billing (services/billing/internal/handler/subscriptions.go:createD2CSubscriptionHandler) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)document-service (services/document-service/internal/kafka/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects [] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType quoteLocator partyLocator mode amountPence currency stripePaymentIntentId |
quote.payment_settled
A D2C quote's premium settled: the quote_payments row was upserted to SETTLED (idempotent on quote_locator, so a redelivered Stripe webhook re-emits rather than duplicates) and policy issuance is unblocked; enrollment's issuance gate reads the row over GET /quotes/{quoteLocator}/payment. amountPence is Stripe minor units; currency arrives lowercase from Stripe (gbp) and defaults to GBP on the internal mark-paid path. partyLocator can be empty when the PaymentIntent metadata lacked it. The billing artefacts (charge.created / invoice.paid / payment.settled) follow separately from the settleDirect step.
| Key | quote locator (QTE-YYYY-NNNNNN) |
| Producers | billing (services/billing/internal/repository/gorm_quote_payments.go:MarkQuotePaid) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)document-service (services/document-service/internal/kafka/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["quotePayment"] |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType quoteLocator paymentIntentId partyLocator amountPence currency occurredAt |
broker.events
broker.appointed
A broker gained delegated authority for the first time: PUT /authority found no broker_configs row and created one with the authority limit and contracted commission rate. The same endpoint on an existing row emits broker.authority.updated instead, so appointed fires at most once per broker (unless the row is deleted out of band). state freezes the created BrokerConfig.
| Key | brokerId (the broker's Keycloak brokerId claim) |
| Producers | broker-api (services/broker-api/internal/service/authority.go:Upsert) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["brokerConfig"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | brokerConfigLocator brokerId delegatedAuthorityLimit defaultCommissionRate |
broker.authority.breached
A policy was issued whose premium exceeds the broker's delegated authority limit (or the broker has no config row at all, which is deny-by-default). Detect-only: the policy is already bound when the policy.issued projector runs the check, so this surfaces the breach for follow-up rather than blocking or reversing anything, and the commission is still recorded. state freezes the issued policy that breached; consumer-driven, so no client session baggage.
| Key | brokerId (the issued policy's broker_locator) |
| Producers | broker-api (services/broker-api/internal/consumer/projector.go:ProcessPolicyIssued) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["policy"] |
| Lineage | correlation optional · not client-interactive |
| Payload required | brokerId policyLocator premium |
broker.authority.updated
An existing broker's delegated authority terms were amended: PUT /authority updated the broker_configs row in place with a new authority limit and/or contracted commission rate. Mirrors broker.appointed (same payload shape) on the update branch. state freezes the config as re-upserted; the payload carries only the new values, not the prior ones.
| Key | brokerId (the broker's Keycloak brokerId claim) |
| Producers | broker-api (services/broker-api/internal/service/authority.go:Upsert) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["brokerConfig"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | brokerConfigLocator brokerId delegatedAuthorityLimit defaultCommissionRate |
commission.earned
A commission was recorded for a broker on an issued policy, always at status PENDING with the amount derived server-side as round(premium x rate). Fires from CommissionService.Create on both paths: the policy.issued projector (the primary, idempotent path via EnsureForPolicy) and the manual POST /commissions override, which is NOT idempotent and can emit duplicates for the same (broker, policy) pair. state freezes the commission row as inserted; the row is mutable (status later flips to PAID).
| Key | brokerId (the broker's Keycloak brokerId claim / policy broker_locator) |
| Producers | broker-api (services/broker-api/internal/service/commission.go:Create) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["commission"] |
| Lineage | correlation optional · not client-interactive |
| Payload required | commissionLocator brokerId policyLocator amount rate status |
commission.paid
A PENDING commission was settled: PATCH /commissions/{locator}/pay flipped the row to PAID, the only status transition the service allows (a repeat pay returns 422 and emits nothing). state freezes the commission as paid; the payload's status is always PAID. Born from an interactive broker portal request, so client session baggage rides the envelope when present.
| Key | brokerId (the broker's Keycloak brokerId claim) |
| Producers | broker-api (services/broker-api/internal/service/commission.go:MarkPaid) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["commission"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | commissionLocator brokerId policyLocator amount status |
care.events
care.appointment.attended
An appointment was marked ATTENDED via the attend action. state.appointment freezes the attended row.
| Key | appointment locator (APT-xxxxxxxx) |
| Producers | care (services/care/internal/service/service.go:UpdateAppointmentStatus) |
| Consumers | notifications (services/notifications/internal/dispatch/dispatcher.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["appointment"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType appointmentLocator episodeLocator occurredAt |
care.appointment.cancelled
An appointment was cancelled. The same transaction releases an OWN_SLOT slot back to AVAILABLE, and a Kry-linked booking is cancelled upstream first. CancellationReason/CancellationComment live on state.appointment, not in the payload.
| Key | appointment locator (APT-xxxxxxxx) |
| Producers | care (services/care/internal/service/service.go:UpdateAppointmentStatus) |
| Consumers | notifications (services/notifications/internal/dispatch/dispatcher.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["appointment"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType appointmentLocator episodeLocator occurredAt |
care.appointment.confirmed
An appointment moved to CONFIRMED via the confirm action. The payload carries its own eventId plus partyLocator (resolved best-effort from the episode) and scheduledAt/appointmentType, added for the notifications consumer's appointment-booked copy (#1630); partyLocator and scheduledAt are absent when resolution fails or no time is set. state.appointment freezes the confirmed row.
| Key | appointment locator (APT-xxxxxxxx) |
| Producers | care (services/care/internal/service/service.go:UpdateAppointmentStatus) |
| Consumers | notifications (services/notifications/internal/dispatch/dispatcher.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["appointment"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventId eventType appointmentLocator episodeLocator appointmentType occurredAt |
care.appointment.no_show
An appointment was marked NO_SHOW via the no-show action. state.appointment freezes the row.
| Key | appointment locator (APT-xxxxxxxx) |
| Producers | care (services/care/internal/service/service.go:UpdateAppointmentStatus) |
| Consumers | notifications (services/notifications/internal/dispatch/dispatcher.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["appointment"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType appointmentLocator episodeLocator occurredAt |
care.appointment.reminder
T-24h reminder for a CONFIRMED appointment, emitted by the in-process reminder sweep (#1630), at most once per appointment (the appointment_reminders_sent ledger is claimed in the same transaction as the outbox row). Job-driven: no client session, and correlation depends on the sweep's own root span rather than an originating request. Skipped entirely when the episode's party cannot be resolved.
| Key | appointment locator (APT-xxxxxxxx) |
| Producers | care (services/care/internal/service/reminders.go:SweepReminders) |
| Consumers | notifications (services/notifications/internal/dispatch/dispatcher.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["appointment"] |
| Lineage | correlation optional · not client-interactive |
| Payload required | eventId eventType appointmentLocator episodeLocator appointmentType partyLocator scheduledAt occurredAt |
care.episode.cancelled
An OPEN care episode was cancelled rather than completed; terminal, like closed but without a summary. state.episode freezes the cancelled row.
| Key | episode locator (EP-xxxxxxxx) |
| Producers | care (services/care/internal/service/service.go:CancelEpisode) |
| Consumers | notifications (services/notifications/internal/dispatch/dispatcher.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["episode"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType episodeLocator occurredAt |
care.episode.closed
An OPEN care episode was closed with a summary; terminal, there is no reopen. The payload is minimal by design (locator + occurredAt); state.episode freezes the closed row including Summary and ClosedAt.
| Key | episode locator (EP-xxxxxxxx) |
| Producers | care (services/care/internal/service/service.go:CloseEpisode) |
| Consumers | notifications (services/notifications/internal/dispatch/dispatcher.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["episode"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType episodeLocator occurredAt |
care.episode.opened
A member's care episode was created (POST /episodes), usually out of a triage chat booking a GP or physio. triageSessionLocator carries the chat -> care join and is null when no chat produced the episode; creation is idempotent per (party, careType, triage session, OPEN), so a double-tap reuses the open episode and emits nothing. state.episode freezes the episode row as created.
| Key | episode locator (EP-xxxxxxxx) |
| Producers | care (services/care/internal/service/service.go:CreateEpisode) |
| Consumers | notifications (services/notifications/internal/dispatch/dispatcher.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["episode"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType episodeLocator partyLocator careType triageSessionLocator occurredAt |
care.prescription.filled
A prescription moved to FILLED via the fill action (pharmacy dispensed). state.prescription freezes the row including the medication blob.
| Key | prescription locator (RX-xxxxxxxx) |
| Producers | care (services/care/internal/service/service.go:UpdatePrescriptionStatus) |
| Consumers | notifications (services/notifications/internal/dispatch/dispatcher.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["prescription"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType prescriptionLocator episodeLocator providerLocator occurredAt |
care.prescription.issued
A provider issued a prescription inside a care episode; the medication JSONB blob (name, dosage, frequency, instructions) rides on state.prescription, never in the payload. Row insert and outbox row commit in one transaction.
| Key | prescription locator (RX-xxxxxxxx) |
| Producers | care (services/care/internal/service/service.go:IssuePrescription) |
| Consumers | notifications (services/notifications/internal/dispatch/dispatcher.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["prescription"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType prescriptionLocator episodeLocator providerLocator occurredAt |
care.prescription.voided
A prescription was voided via the void action. state.prescription freezes the voided row.
| Key | prescription locator (RX-xxxxxxxx) |
| Producers | care (services/care/internal/service/service.go:UpdatePrescriptionStatus) |
| Consumers | notifications (services/notifications/internal/dispatch/dispatcher.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["prescription"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType prescriptionLocator episodeLocator providerLocator occurredAt |
care.referral.cancelled
A referral was cancelled via the cancel action. state.referral freezes the cancelled row.
| Key | referral locator (REF-xxxxxxxx) |
| Producers | care (services/care/internal/service/service.go:UpdateReferralStatus) |
| Consumers | notifications (services/notifications/internal/dispatch/dispatcher.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["referral"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType referralLocator episodeLocator referralType occurredAt |
care.referral.completed
A referral moved to COMPLETED via the complete action (the diagnostic was performed). state.referral freezes the row.
| Key | referral locator (REF-xxxxxxxx) |
| Producers | care (services/care/internal/service/service.go:UpdateReferralStatus) |
| Consumers | notifications (services/notifications/internal/dispatch/dispatcher.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["referral"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType referralLocator episodeLocator referralType occurredAt |
care.referral.created
A diagnostics referral (MRI, X-ray, bloods, ...) was raised inside a care episode with status REFERRED. Booking it against a clinic (BookReferral) emits nothing; only complete and cancel do. state.referral freezes the created row.
| Key | referral locator (REF-xxxxxxxx) |
| Producers | care (services/care/internal/service/service.go:CreateReferral) |
| Consumers | notifications (services/notifications/internal/dispatch/dispatcher.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["referral"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType referralLocator episodeLocator referralType occurredAt |
claims.events
claim.approved
The claim reached APPROVED, which by invariant means the member's allowance has been burnt (ApplyAccumulators succeeded before or in the same flow as the transition). Three emit sites: ReviewClaim when adjudication auto-approves (payload carries policy_locator, and account_id when the policy resolved), ApproveClaim for the manual or repair approve (policy_locator and account_id only when enrollment resolved the policy), and autoApprove on line submission (policy_locator, no account_id). state.claim is the pre-transition row in scope at the emit site.
| Key | claim locator (CLM-YYYY-NNNNNN) |
| Producers | claims (services/claims/internal/service/claim.go:ReviewClaim)claims (services/claims/internal/service/claim.go:ApproveClaim)claims (services/claims/internal/service/claim.go:autoApprove) |
| Consumers | billing (services/billing/internal/projection/handlers.go:HandleClaimApproved)document-service (services/document-service/internal/kafka/consumer.go)notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["claim"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType claimLocator |
claim.closed
An APPROVED or REJECTED claim was closed, the terminal transition of the claim state machine. No downstream projection reacts today; the event exists for the audit spine and the debugger. state.claim is the pre-transition row in scope.
| Key | claim locator (CLM-YYYY-NNNNNN) |
| Producers | claims (services/claims/internal/service/claim.go:CloseClaim) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["claim"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType claimLocator |
claim.info_requested
A reviewer asked the member for more information: the claim moved SUBMITTED or UNDER_REVIEW to PENDING_INFO. The note travels on the claim_events audit row, not the bus. state.claim is the pre-transition row in scope.
| Key | claim locator (CLM-YYYY-NNNNNN) |
| Producers | claims (services/claims/internal/service/claim.go:RequestInfo) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["claim"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType claimLocator |
claim.paid
The claim reached PAID: line amount_paid was set to amount_allowed and the status flipped APPROVED to PAID, driven by billing through the internal pay endpoint. Emitted in the same transaction as claim.payment_initiated. Consumer/job driven, so no client session is expected. state.claim is the pre-transition APPROVED row in scope.
| Key | claim locator (CLM-YYYY-NNNNNN) |
| Producers | claims (services/claims/internal/service/claim.go:PayClaim) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["claim"] |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType claimLocator |
claim.payment_initiated
PayClaim is moving the claim out of APPROVED: this event signals that payment is being initiated, bridging APPROVED to PAID for downstream billing and ledger consumers. It rides the same transaction as the PAID transition and the claim.paid event that follows it. Emitted from the internal pay endpoint billing calls, so there is no client session. state.claim is the APPROVED row in scope.
| Key | claim locator (CLM-YYYY-NNNNNN) |
| Producers | claims (services/claims/internal/service/claim.go:PayClaim) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["claim"] |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType claimLocator |
claim.rejected
The claim moved to REJECTED, the only adverse claim status. Two emit sites: the manual RejectClaim (payload carries the durable reasonCode, #1626) and autoDenyOpenClaims, which rejects every open claim when a policy.cancelled event arrives (no reasonCode, which is why the field cannot be required). state.claim is the pre-transition row in scope.
| Key | claim locator (CLM-YYYY-NNNNNN) |
| Producers | claims (services/claims/internal/service/claim.go:RejectClaim)claims (services/claims/internal/kafka/handlers.go:autoDenyOpenClaims) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["claim"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType claimLocator |
claim.review_required
ReviewClaim adjudicated the claim and the decision came back MANUAL_REVIEW_REQUIRED, so the claim moved SUBMITTED (or PENDING_INFO) to UNDER_REVIEW instead of auto-approving. Today that only happens via the config-gated high-value threshold (MANUAL_REVIEW_THRESHOLD), since no adjudication ruleset is fetched. state.claim is the pre-transition row as fetched at review time.
| Key | claim locator (CLM-YYYY-NNNNNN) |
| Producers | claims (services/claims/internal/service/claim.go:ReviewClaim) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["claim"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType claimLocator policy_locator |
claim.submitted
A claim was created complete (there is no DRAFT) and entered the status machine at SUBMITTED. The claim row, its SUBMITTED audit event and this outbox row are written in one transaction; state freezes the freshly created claim, Document decoded.
| Key | claim locator (CLM-YYYY-NNNNNN) |
| Producers | claims (services/claims/internal/service/claim.go:SubmitClaim) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["claim"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType claimLocator |
prior_auth.escalated
The prior-auth moved SUBMITTED to PENDING_REVIEW: routing into specialist or clinical review is an escalation of the request. state.priorAuth is the pre-transition SUBMITTED row in scope.
| Key | prior-auth locator (PAU-YYYY-NNNNNN) |
| Producers | claims (services/claims/internal/service/priorauth.go:Review) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["priorAuth"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType priorAuthLocator |
prior_auth.submitted
A provider opened a prior-authorization request; the row is created in SUBMITTED in the same transaction as this outbox write. Prior auth is an independent workflow and does not gate claim adjudication today. state.priorAuth freezes the freshly created row, Document decoded. Note the underscore spelling; the decided sibling uses a hyphen (prior-auth.decided), an as-built inconsistency kept for wire compatibility.
| Key | prior-auth locator (PAU-YYYY-NNNNNN) |
| Producers | claims (services/claims/internal/service/priorauth.go:Submit) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["priorAuth"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType priorAuthLocator |
prior-auth.decided
A PENDING_REVIEW (or legacy PENDING) prior-auth was decided: the decision field carries APPROVED or DENIED and decidedAt the review timestamp. One eventType covers both outcomes; consumers branch on decision. state.priorAuth is the pre-transition row in scope. Note the hyphen spelling; the submitted and escalated siblings use underscores, an as-built inconsistency kept for wire compatibility.
| Key | prior-auth locator (PAU-YYYY-NNNNNN) |
| Producers | claims (services/claims/internal/service/priorauth.go:Approve)claims (services/claims/internal/service/priorauth.go:Deny) |
| Consumers | document-service (services/document-service/internal/kafka/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["priorAuth"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType priorAuthLocator decision decidedAt |
record.viewed
The ENG-406 NFR-04 audit trail: a 2xx read of a customer, policy, quote or claim record emits one of these from the shared audit.RecordView middleware, server-side so it cannot be omitted by a client. It bypasses the outbox (a read has no transaction to attach to) and goes out through each emitting service's direct producer to that service's own events topic (claims.events here; enrollment and policy-admin publish to theirs), which the analytics Kafka to BigQuery sink consumes by pattern. The direct path stamps client lineage from baggage but no correlationId, and there is no state subject: the payload IS the complete fact.
| Key | none (published unkeyed by the direct producers) |
| Producers | claims (services/claims/internal/handler/handler.go)enrollment (services/enrollment/internal/handler/quotes.go)enrollment (services/enrollment/internal/handler/policies.go)policy-admin (services/policy-admin/internal/handler/parties.go) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | record_type record_locator viewer_subject viewed_at path |
consent.events
consent.changed
The generic consent write event: fires on EVERY consent upsert (first grant, re-grant, withdrawal), carrying the new granted value. The semantic transition events consent.granted / consent.withdrawn ride the same outbox transaction on top of it. Caveat (#1782): a parallel STALE consent stack still publishes flat look-alike rows on this topic with no eventId, no payload wrapper, no state and no session; the registry verdict is how you spot them.
| Key | party locator (PTY-YYYY-NNNNNN) |
| Producers | consent (services/consent/internal/handler/consent.go:recordConsentChange) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["consent"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType partyLocator consentType granted changedByType occurredAt |
consent.deletion.requested
A right-to-erasure (GDPR Art. 17) request was accepted and persisted as a PENDING deletion_requests row; the in-process erasure job will execute it on its next tick. Rides the same outbox transaction as the row insert, so an event implies a durable request. The terminal outcome arrives later as deletion.completed or deletion.failed on their own topics.
| Key | party locator of the member to erase (PTY-YYYY-NNNNNN) |
| Producers | consent (services/consent/internal/handler/deletion.go:postDeletion) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["deletionRequest"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType id partyLocator deleteType occurredAt |
consent.granted
A party transitioned INTO granted for one consent type (no prior record or false, to true). Layered on top of the generic consent.changed, which rides the same outbox transaction; granted is therefore always true here. Caveat (#1782): a parallel STALE consent stack still publishes flat look-alike rows on this topic with no eventId, no payload wrapper, no state and no session; the registry verdict is how you spot them.
| Key | party locator (PTY-YYYY-NNNNNN) |
| Producers | consent (services/consent/internal/handler/consent.go:recordConsentChange) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["consent"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType partyLocator consentType granted changedByType occurredAt |
consent.withdrawn
A party transitioned OUT of granted for one consent type (true to false). Layered on top of the generic consent.changed in the same outbox transaction; granted is therefore always false here. Exercised end to end by tests/e2e/qnb_variants_test.go:TestEvent_ConsentWithdrawn (revoke a granted consent via the internal consent-update path). Golden example is synthetic: no wire capture existed at authoring time.
| Key | party locator (PTY-YYYY-NNNNNN) |
| Producers | consent (services/consent/internal/handler/consent.go:recordConsentChange) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["consent"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType partyLocator consentType granted changedByType occurredAt |
deletion.completed
deletion.completed
The erasure job executed a deletion request to completion (SOFT anonymised or HARD deleted the party's consent-side rows) and settled the request COMPLETED. Published best-effort by the job's DIRECT producer to its own topic, outside the outbox, so a Kafka outage can drop it even though the erasure itself committed. The envelope carries no correlationId (the direct producer has no trace-context store), so correlation cannot be required; state freezes the settled request row, which matters because the erasure destroys the party data the payload's locator points at.
| Key | party locator of the erased member (PTY-YYYY-NNNNNN) |
| Producers | consent (services/consent/internal/job/erasure.go:processRequest) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["deletionRequest"] |
| Lineage | correlation optional · not client-interactive |
| Payload required | eventType id partyLocator deleteType completedAt |
deletion.failed
deletion.failed
The erasure job exhausted its attempts (default 3) on a deletion request and settled it FAILED; attemptCount carries the final tally. Published best-effort by the job's DIRECT producer to its own topic, outside the outbox; no correlationId is available on this path, so correlation cannot be required. The only deterministic trigger today is the PTY-E2E-FAULT- sentinel prefix (fault injection for e2e coverage), because the real SOFT/HARD delete queries are no-ops on unknown parties and do not organically fail.
| Key | party locator of the member whose erasure failed (PTY-YYYY-NNNNNN) |
| Producers | consent (services/consent/internal/job/erasure.go:processRequest) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["deletionRequest"] |
| Lineage | correlation optional · not client-interactive |
| Payload required | eventType id partyLocator attemptCount failedAt |
document-events
document.downloaded
A stored document's PDF bytes were actually served to a caller (the JWT-gated GET /documents/{locator} or the signed GET /documents/{locator}/content), making access auditable; minting a download URL alone does NOT fire it. Emitted after the bytes left, so a failure to record it is traced but never fails the download. state.document freezes the row minus the PDF bytes, mirroring document.ready.
| Key | policy locator first, then party / invoice / document locator (domain.MessageKeyOf order in services/document-service/internal/outbox/producer.go:PublishState) |
| Producers | document-service (services/document-service/internal/service/document.go:RecordDownload) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["document"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | documentLocator partyLocator policyLocator schemeLocator invoiceLocator templateType marketCode |
document.generation_failed
A Generate call failed before a document became ready: stage names the transition that broke (fetch_template, render_template, render_pdf, or persist) and reason carries the error text. No document row exists at any failure stage, so there is no entity to freeze and the event is payload-only; it rides the same outbox as document.ready so a consumer watching for a schedule can observe the failure instead of a silent gap.
| Key | policy locator first, then party / invoice locator (domain.MessageKeyOf order in services/document-service/internal/outbox/producer.go:PublishState); no document locator exists on a failure |
| Producers | document-service (services/document-service/internal/service/document.go:emitGenerationFailed) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlationId required · not client-interactive |
| Payload required | partyLocator policyLocator schemeLocator invoiceLocator templateType marketCode stage reason |
document.ready
Document-service rendered a document (POLICY_SCHEDULE, INVOICE, ...) and persisted it in the same transaction as the outbox row; the artifact is now downloadable by documentLocator. state.document freezes the row MINUS the rendered PDF bytes (megabytes of base64 do not belong on the bus; the locator identifies the artifact). policyLocator, schemeLocator, invoiceLocator and partyLocator are always present but may be empty strings depending on template type. Note: the topic is document-events, not document.events.
| Key | policy locator first, then party / invoice / document locator (domain.MessageKeyOf order in services/document-service/internal/outbox/producer.go:PublishState) |
| Producers | document-service (services/document-service/internal/service/document.go:Generate) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["document"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | documentLocator partyLocator policyLocator schemeLocator templateType marketCode invoiceLocator |
document.reissued
An existing document was regenerated because the entity it describes changed: today, policy.endorsed re-rendering a member's POLICY_SCHEDULE after an upgrade or add-on (services/document-service/internal/kafka/consumer.go sets Reissue on that event). The payload is byte-identical to document.ready; only the event type differs, so consumers can tell a superseding schedule from a new first issue instead of treating it as a duplicate. Fired from event-driven regeneration, not directly from a client request. Golden example is synthetic (a document.ready capture retyped): no wire capture existed at authoring time.
| Key | policy locator first, then party / invoice / document locator (domain.MessageKeyOf order in services/document-service/internal/outbox/producer.go:PublishState) |
| Producers | document-service (services/document-service/internal/service/document.go:Generate) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["document"] |
| Lineage | correlationId required · not client-interactive |
| Payload required | documentLocator partyLocator policyLocator schemeLocator templateType marketCode invoiceLocator |
eligibility.events
eligibility.accumulators.applied
A claim line actually consumed an accumulator: the idempotent apply (PATCH /internal/members/{locator}/accumulators/{termLocator}/apply, called by claims) inserted a fresh ledger row and moved consumed_amount. Fires once per line that moved, never on an idempotent replay; fire-and-forget after the consumption already committed. state cannot be required honestly: the repository Apply returns only a bool, so the post-consumption accumulator row is not in scope at the emit site and no subject is frozen (recorded gap). Published DIRECTLY on the eligibility envelope, which has no correlation or session fields.
| Key | member's party locator (PTY-YYYY-NNNNNN) |
| Producers | eligibility (services/eligibility/internal/handler/internal.go:internalApplyAccumulatorsHandler) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · not client-interactive |
| Payload required | memberLocator claimLocator claimLineId accumulator accumulatorType amount |
eligibility.accumulators.reset
A policy renewal (policy.renewed consumed from enrollment) opened a new term and re-created the member's accumulators at zero consumption; one event per affected member per renewal, not per element. Consumer-driven and fire-and-forget, so a publish failure never fails the projection. state cannot be required honestly: the fresh accumulator rows are created inside createAccumulators, which returns only an error, so no subject entity is in scope at the emit site (recorded gap). Published DIRECTLY on the eligibility envelope, which has no correlation or session fields.
| Key | member's party locator (PTY-YYYY-NNNNNN) |
| Producers | eligibility (services/eligibility/internal/projection/handlers.go:publishAccumulatorsReset) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · not client-interactive |
| Payload required | memberLocator policyLocator termId |
eligibility.coverage.changed
A member's coverage status flipped between ACTIVE and INACTIVE via the policy-lifecycle projection: cancel/lapse flips to INACTIVE, reinstate and term activation flip back to ACTIVE. One event per affected member per flip. Published DIRECTLY by the projection consumer (services/eligibility/internal/kafka/producer.go:PublishWithKeyState), not via an outbox: the eligibility envelope carries no correlationId or sessionId fields at all, so lineage cannot be required. state.coverage is a LIST of the member's flipped coverage rows (employee plus dependents, several products), with CoverageTerms decoded to an object. Golden example is synthetic: no wire capture existed at authoring time.
| Key | member's party locator (PTY-YYYY-NNNNNN) |
| Producers | eligibility (services/eligibility/internal/projection/handlers.go:publishCoverageChanged) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["coverage"] |
| Lineage | correlation optional · not client-interactive |
| Payload required | memberLocator policyLocator oldStatus newStatus |
eligibility.coverage.not_found
A point-of-care eligibility check (GET/POST /check) found NO active coverage for the member on the service date; the check still returns 200 with status INACTIVE, this event is the bus-side record of the miss. Fire-and-forget and published DIRECTLY, and the eligibility envelope carries no correlationId or sessionId fields, so lineage cannot be required. Carries no state: there is no coverage entity in scope when the lookup finds nothing.
| Key | member's party locator as the caller supplied it |
| Producers | eligibility (services/eligibility/internal/handler/check.go:runCheck) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · not client-interactive |
| Payload required | memberLocator serviceDate status coverageCount serviceType eligible |
eligibility.coverage.verified
A point-of-care eligibility check (GET/POST /check) found ACTIVE coverage for the member on the service date. Fires on every check, so it is a high-frequency clinical-journey milestone, not a state transition; fire-and-forget, so a publish failure never fails the check. Published DIRECTLY (services/eligibility/internal/kafka/producer.go), and the eligibility envelope carries no correlationId or sessionId fields at all, so lineage cannot be required. state freezes the verified coverage rows (CoverageTerms decoded to an object) plus the member's accumulators as the check saw them.
| Key | member's party locator (PTY-YYYY-NNNNNN) |
| Producers | eligibility (services/eligibility/internal/handler/check.go:runCheck) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["coverage","accumulators"] |
| Lineage | correlation optional · not client-interactive |
| Payload required | memberLocator serviceDate status coverageCount serviceType eligible |
enrollment.events
affordability.checked
The affordability/credit enrichment check returned during D2C pricing (#1675), emitted by enrollment from the shared per-check loop in priceD2C. outcome is BAND_ plus the credit band (BAND_A ... today, from the mock-credit provider); affordability flags and thin-file detail stay on state.quote.Document.checks.affordability. Fires on every (re)price.
| Key | quote locator (QTE-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/quote.go:priceD2C) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["quote"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType quoteLocator accountId partyLocator outcome provider |
element.added
A coverage element became part of a policy: an add-on elected at D2C issue, a fresh element added through the internal element-change surface, or a re-versioned ACTIVE element written when a non-endorsement, non-renewal transaction is applied. Only elementLocator and elementType are common to all three emit sites: addonKey and effectiveFrom appear only at issue, staticID only on the transaction paths, and policyLocator is absent from the Apply re-version emit. State always freezes the element; the issue path also freezes the policy and the transaction paths freeze the transaction.
| Key | element locator (ELM-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/quote.go:Issue)enrollment (services/enrollment/internal/service/transaction.go:ApplyElementChange)enrollment (services/enrollment/internal/service/transaction.go:Apply) |
| Consumers | eligibility (services/eligibility/internal/projection/handlers.go:handleElementAdded)notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["element"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType elementLocator elementType |
element.removed
A policy element was removed from cover: ApplyElementChange (action remove) closes the active version and writes a REMOVED successor version carrying the same staticID, or TransactionService.Apply re-versions an already-REMOVED element when a transaction is applied. The event is keyed on the new REMOVED version's locator; policyLocator appears only on the ApplyElementChange path. Eligibility terminates the member's coverage rows for the element's staticID off it.
| Key | element locator (ELM-YYYY-NNNNNN) of the NEW element version |
| Producers | enrollment (services/enrollment/internal/service/transaction.go:ApplyElementChange)enrollment (services/enrollment/internal/service/transaction.go:Apply) |
| Consumers | eligibility (services/eligibility/internal/projection/handlers.go:handleElementRemoved)notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["element"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType elementLocator elementType staticID |
element.updated
A policy element was re-versioned with changed content: a coverage upgrade rewriting coverageTerms, or an applied ENDORSEMENT or RENEWAL transaction re-versioning the policy's active elements. Three emit sites share the type and only eventType and elementLocator are common: UpgradeCoverage emits the rich eligibility-projection shape (camelCase staticId, coverageTerms, policyId), while the two transaction paths emit the slim shape with staticID and elementType, adding policyLocator only on ApplyElementChange. Both staticId and staticID spellings are therefore legal; state always freezes the new element version.
| Key | element locator (ELM-YYYY-NNNNNN) of the NEW element version |
| Producers | enrollment (services/enrollment/internal/service/upgrade.go:UpgradeCoverage)enrollment (services/enrollment/internal/service/transaction.go:Apply)enrollment (services/enrollment/internal/service/transaction.go:ApplyElementChange) |
| Consumers | eligibility (services/eligibility/internal/projection/handlers.go:handleElementUpdated)notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["element"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType elementLocator |
fraud.assessed
The fraud check returned during D2C pricing (#1675). The vendor outcome (ALLOW/REVIEW/BLOCK from the mock) is overlaid with the platform's own re-rate velocity signal: every price stamps rateAttempts, and at 5 attempts the outcome escalates to REVIEW with reviewReason 're-rate velocity', which is what fraud analytics and the ops review queue act on. A REVIEW outcome does not block issuance until a human rejects it via ReviewCheck.
| Key | quote locator (QTE-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/quote.go:priceD2C) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["quote"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType quoteLocator accountId partyLocator outcome provider rateAttempts |
identity.verified
The KYC identity check returned during D2C pricing (#1675), emitted by enrollment (the orchestrator) from the shared per-check loop in priceD2C, not by the provider, so the contract survives the swap from the mock (provider mock-kyc) to a real vendor. outcome is the provider status (PASS today); reference and at are provider-issued and optional. Fires on every (re)price, so one quote can carry several.
| Key | quote locator (QTE-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/quote.go:priceD2C) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["quote"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType quoteLocator accountId partyLocator outcome provider |
policy.activated
The policy went on risk. Emitted inside the issue transaction when inception is at or before sale time (both issuance paths), and by the activation job tick when a PENDING policy reaches its future inception, so consumers get one uniform activation signal either way (ENG-405, #1783 P1). Job emissions carry no sessionId and freeze only a slim policy projection (Locator, PartyLocator, InceptionDate), so state.policy is thinner there than the issue-time snapshot.
| Key | policy locator (POL-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/quote.go:Issue)enrollment (services/enrollment/internal/service/policy.go:IssueInternal)enrollment (services/enrollment/internal/job/activation.go:Tick) |
| Consumers | eligibility (services/eligibility/internal/projection/handlers.go:handlePolicyActivated)notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["policy"] |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType policyLocator partyLocator effectiveDate |
policy.cancelled
The policy was cancelled: its status flipped to CANCELLED atomically with a CANCELLATION transaction. The direct path is PolicyService.Cancel, which also accepts PENDING policies so a cooling-off cancel before cover start works; TransactionService.Apply emits the same type when a CANCELLATION transaction is applied, adding transactionLocator and transactionCategory. Consumers end coverage, stop billing and auto-deny open claims off it; policyLocator is contractually a POL- locator, never the raw policy UUID (#1783 section 3).
| Key | policy locator (POL-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/policy.go:Cancel)enrollment (services/enrollment/internal/service/transaction.go:Apply) |
| Consumers | eligibility (services/eligibility/internal/projection/handlers.go:handlePolicyCancelledOrLapsed)billing (services/billing/internal/projection/consumer.go:dispatchEnrollment)claims (services/claims/internal/kafka/handlers.go:Handle)notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["policy","transaction"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType policyLocator partyLocator |
policy.endorsed
Mid-term changes to the policy were applied. Two shapes share the type: PolicyService.UpgradeCoverage (a tier upgrade) emits the rich form with the full endorsed element list, coverageTerms, planTier and schemeLocator, which is what eligibility's projection and document-service's schedule re-render read; TransactionService.Apply on an ENDORSEMENT transaction emits the slim form carrying transactionLocator and transactionCategory. Only eventType, policyLocator and partyLocator are common to both, so everything else is optional in the contract.
| Key | policy locator (POL-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/upgrade.go:UpgradeCoverage)enrollment (services/enrollment/internal/service/transaction.go:Apply) |
| Consumers | eligibility (services/eligibility/internal/projection/handlers.go:handlePolicyEndorsed)billing (services/billing/internal/projection/consumer.go:dispatchEnrollment)document-service (services/document-service/internal/kafka/consumer.go)notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["policy","transaction"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType policyLocator partyLocator |
policy.endorsement_drafted
PolicyService.Endorse created a DRAFT ENDORSEMENT transaction for an ACTIVE policy. Intent only, mirroring policy.renewal_drafted: nothing has been applied yet, and the fact event policy.endorsed fires from TransactionService.Apply or UpgradeCoverage (#1783 section 4). The requested changes travel in the draft transaction's Document, which state freezes alongside the policy.
| Key | policy locator (POL-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/policy.go:Endorse) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["policy","transaction"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType policyLocator partyLocator |
policy.issued
A policy row now exists: either a D2C member bound their priced quote (Issue) or the Flow-0 internal path issued scheme cover for a party (IssueInternal). The two paths emit different optional fields: the D2C emit carries accountID and inceptionDate, the Flow-0 emit carries scheme, plan, product, premium and coverageTerms fields. status says whether the policy is already on risk (ACTIVE) or awaiting a future cover start (PENDING); eligibility keys its coverage bootstrap off it. State always freezes the policy; only the D2C path additionally freezes the quote.
| Key | policy locator (POL-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/quote.go:Issue)enrollment (services/enrollment/internal/service/policy.go:IssueInternal) |
| Consumers | eligibility (services/eligibility/internal/projection/handlers.go:handlePolicyIssued)billing (services/billing/internal/projection/consumer.go:dispatchEnrollment)broker-api (services/broker-api/internal/consumer/consumer.go)document-service (services/document-service/internal/kafka/consumer.go)claims (services/claims/internal/kafka/handlers.go:Handle)notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["policy"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType policyLocator partyLocator status |
policy.lapsed
An ACTIVE policy lapsed, typically for non-payment: PolicyService.Lapse flips status to LAPSED with a conditional UPDATE so concurrent callers produce exactly one event. Fired from the internal ops surface (POST /internal/policies/{locator}/lapse), not a member action, so no session lineage is expected. Eligibility ends coverage off it and notifications tells the member how to reinstate; state freezes the policy as read before the transition, so Status there is still ACTIVE.
| Key | policy locator (POL-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/policy.go:Lapse) |
| Consumers | eligibility (services/eligibility/internal/projection/handlers.go:handlePolicyCancelledOrLapsed)notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["policy"] |
| Lineage | correlationId required · not client-interactive |
| Payload required | eventType policyLocator partyLocator |
policy.reinstated
A CANCELLED or LAPSED policy went back on risk: PolicyService.Reinstate flips status to ACTIVE atomically with an APPLIED REINSTATEMENT transaction. TransactionService.Apply emits the same type when a drafted REINSTATEMENT transaction is applied, adding transactionLocator and transactionCategory. Eligibility restores coverage and billing resumes the schedule off it; state freezes the policy as read before the transition (Status still CANCELLED or LAPSED) plus the reinstatement transaction.
| Key | policy locator (POL-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/policy.go:Reinstate)enrollment (services/enrollment/internal/service/transaction.go:Apply) |
| Consumers | eligibility (services/eligibility/internal/projection/handlers.go:handlePolicyReinstated)billing (services/billing/internal/projection/consumer.go:dispatchEnrollment)notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["policy","transaction"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType policyLocator partyLocator |
policy.renewal_drafted
PolicyService.Renew created the next policy term and a DRAFT RENEWAL transaction. This is intent, not fact: the draft can still be declined or discarded, and policy.renewed fires only when TransactionService.Apply applies it (#1783 section 4), so projections must not act on this type. Payload carries the drafted term's number and effective window; state freezes the policy and the draft transaction.
| Key | policy locator (POL-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/policy.go:Renew) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["policy","transaction"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType policyLocator partyLocator termNumber effectiveFrom effectiveTo |
policy.renewed
A drafted RENEWAL transaction was applied: the policy rolled into its next term. Emitted only by TransactionService.Apply, never at draft time; PolicyService.Renew emits policy.renewal_drafted instead, so consumers act exactly once per lifecycle (#1783 section 4). Keyed on the policy locator with the transaction locator in the payload (#1783 section 3); billing opens the new term's schedule and eligibility refreshes coverage windows off it.
| Key | policy locator (POL-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/transaction.go:Apply) |
| Consumers | eligibility (services/eligibility/internal/projection/handlers.go:handlePolicyRenewed)billing (services/billing/internal/projection/consumer.go:dispatchEnrollment)notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["policy","transaction"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType policyLocator partyLocator transactionLocator transactionCategory |
premium.rated
The rating engine computed a premium, the analytics-facing sibling of quote.calculated riding the same transaction. Emitted on BOTH pricing paths: D2C (priceD2C, sets accountId/partyLocator/premiumMonthly/channel=D2C and always totalPremium) and the scheme rate-table path (Price, where totalPremium is set only when a premium exists). The D2C leg was missing until #1768, so counts before that date cover group business only.
| Key | quote locator (QTE-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/quote.go:priceD2C)enrollment (services/enrollment/internal/service/quote.go:Price) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["quote"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType quoteLocator productVersionId premiumCurrency |
quote.accepted
The customer accepted the quote as part of issuance: Issue moved it to ACCEPTED in the same transaction that creates the policy, term, and issuance transaction. Keyed on the quote locator so quote-aggregate consumers can find it; policyLocator links forward to the policy.issued sibling. State carries BOTH the quote and the newborn policy.
| Key | quote locator (QTE-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/quote.go:Issue) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["quote","policy"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType quoteLocator accountId productVersionId policyLocator status |
quote.calculated
Pricing computed a premium for a D2C quote and moved it to PRICED. Fires on every (re)price, so a mutated selection emits a fresh one; premium.rated rides the same transaction as the analytics-facing sibling.
| Key | quote locator (QTE-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/quote.go:priceD2C) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["quote"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType quoteLocator status premiumCurrency |
quote.created
A new quote row opened in DRAFT (#1675), making the funnel entry observable downstream. partyLocator is empty for employer quotes (no party on the Document); consumers skip those. The declaration itself travels in state.quote.Document, not the payload.
| Key | quote locator (QTE-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/quote.go:Create) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["quote"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType quoteLocator accountId partyLocator status |
quote.declined
The quote was refused: either D2C underwriting declined the applicant during pricing (priceD2C, carries partyLocator + declineReason) or an operator/rule refused it via Refuse (carries productVersionId + previousStatus). Both paths move the quote to DECLINED atomically with the event. Consumers should treat declineReason as optional; only the underwriting path sets it.
| Key | quote locator (QTE-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/quote.go:priceD2C)enrollment (services/enrollment/internal/service/quote.go:Refuse) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["quote"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType quoteLocator accountId status |
quote.discarded
The user deliberately walked away from a DRAFT quote (#1783 P2): Discard flips it to DISCARDED and announces the abandon atomically. Its sibling Refuse emits quote.declined; this event exists so the funnel and timeline see the explicit walk-away rather than silence. Only a DRAFT quote can be discarded, so previousStatus is always DRAFT.
| Key | quote locator (QTE-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/quote.go:Discard) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["quote"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType quoteLocator accountId partyLocator previousStatus |
quote.updated
The member changed their declaration (add-ons, excess, tier, cover-start) before payment; the quote snapshots an immutable version and drops back to DRAFT because a changed declaration invalidates any shown price (#1675 / ENG-405). The payload carries the NEW declaration; previousStatus says what the quote was before the edit. The next price emits its own quote.calculated.
| Key | quote locator (QTE-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/quote.go:UpdateMemberDeclaration) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["quote"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType quoteLocator accountId partyLocator previousStatus declaration |
review.approved
A human approved one of the four vendor checks on a quote (#1783): ReviewCheck writes the decision onto the check inside the quote Document, snapshots an immutable version (reason 'review'), and announces it. Approval clears the issuance block without erasing the vendor outcome it overrides. reviewedBy comes from the reviewer's verified JWT, never the request body; note there is no accountId on this payload.
| Key | quote locator (QTE-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/quote.go:ReviewCheck) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["quote"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType quoteLocator partyLocator checkType reason reviewedBy |
review.rejected
A human rejected one of the four vendor checks on a quote (#1783): ReviewCheck writes the decision onto the check inside the quote Document, snapshots an immutable version (reason 'review'), and announces it. A rejected check hard-blocks Issue with a deterministic 409, checked before the payment gate. reviewedBy comes from the reviewer's verified JWT, never the request body; note there is no accountId on this payload.
| Key | quote locator (QTE-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/quote.go:ReviewCheck) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["quote"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType quoteLocator partyLocator checkType reason reviewedBy |
sanctions.screened
The sanctions/PEP screening check returned during D2C pricing (#1675), emitted by enrollment from the shared per-check loop in priceD2C so the contract holds when the mock (provider mock-sanctions) is swapped for a real vendor. outcome is the screening result (CLEAR today); list/hit detail stays on state.quote.Document.checks.screening, not the payload. Fires on every (re)price.
| Key | quote locator (QTE-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/quote.go:priceD2C) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["quote"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType quoteLocator accountId partyLocator outcome provider |
scheme.member_enrolled
A Flow-0 member policy linked a party to a group scheme, so the member has joined that scheme. Emitted by PolicyService.IssueInternal atomically with the policy row and its sibling policy.issued, but keyed on the SCHEME locator so roster consumers can group by scheme; the policy aggregate's own facts stay on policy.issued. No roster projection consumes it yet; the debugger records it for walk correlation.
| Key | scheme locator (SCH-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/policy.go:IssueInternal) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["policy"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType schemeLocator partyLocator policyLocator productVersionId planTier effectiveDate enrolledAt |
underwriting.decided
D2C underwriting reached a decision during pricing (#1675): ACCEPT (standard terms) or ACCEPT_WITH_TERMS (loadings and/or exclusions apply). A DECLINE never reaches this event; that path early-returns and emits quote.declined instead. basis is the underwriting basis offered, today always MORATORIUM (the D2C default); the loadings/exclusions detail lives on state.quote.Document.underwriting.
| Key | quote locator (QTE-YYYY-NNNNNN) |
| Producers | enrollment (services/enrollment/internal/service/quote.go:priceD2C) |
| Consumers | notifications (services/notifications/internal/projection/consumer.go)debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["quote"] |
| Lineage | correlationId required · client-interactive (session expected) |
| Payload required | eventType quoteLocator accountId partyLocator decision basis |
group-scheme.events
bulk_enrollment.completed
A bulk enrollment job reached its terminal COMPLETED status: RunJob walked the roster, issued one policy per member through enrollment's internal Flow 0 route, and at least one member succeeded. COMPLETED includes partial failure (failedCount can be nonzero; per-member errors live in the job row's errors column, not on this event). Published from the detached RunJob goroutine, so it carries no client session lineage; no full job entity is in scope at the emit site, so no state rides.
| Key | bulk job locator (BMJ-YYYY-NNNNNN) |
| Producers | group-scheme-service (services/group-scheme-service/internal/service/bulk_enrollment.go:RunJob) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · not client-interactive |
| Payload required | jobLocator schemeLocator total processed failedCount status |
bulk_enrollment.failed
A bulk enrollment job reached its terminal FAILED status: every member's policy issuance failed, or the job was started with an empty member list (in which case errors is null). Unlike bulk_enrollment.completed this event carries the per-member errors array inline so downstream reporting does not need the job row. Published from the detached RunJob goroutine, so it carries no client session lineage and no state.
| Key | bulk job locator (BMJ-YYYY-NNNNNN) |
| Producers | group-scheme-service (services/group-scheme-service/internal/service/bulk_enrollment.go:RunJob) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · not client-interactive |
| Payload required | jobLocator schemeLocator total processed failedCount status errors |
member.activated
A member completed activation: the DISPATCHED to ACTIVATED transition of the J-003 gift-box lifecycle, stamping activated_at. Emitted from the SetMemberStatus service seam; the HTTP surface that drives the transition is not wired yet, so no live traffic produces this today. State carries the parent Scheme; the legacy immediate-membership value ACTIVE never emits (only the two forward gift-box transitions carry a semantic event).
| Key | member party locator (PTY-YYYY-NNNNNN), mirroring member.added |
| Producers | group-scheme-service (services/group-scheme-service/internal/service/scheme.go:SetMemberStatus) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["scheme"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | schemeLocator memberPartyLocator status |
member.added
A member joined an employer scheme through the legacy AddMember path (bare memberPartyLocator), landing an ACTIVE scheme_members row. The Flow 0 compose path (demographics shape) and bulk enrollment insert roster rows through other code paths and do NOT emit this event today. State freezes both the new SchemeMember row and its parent Scheme.
| Key | member party locator (PTY-YYYY-NNNNNN); member events key on the member, not the scheme |
| Producers | group-scheme-service (services/group-scheme-service/internal/service/scheme.go:AddMember) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["member","scheme"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | schemeLocator memberPartyLocator status |
member.dispatched
A scheme member's gift box + activation PIN/QR was dispatched by Ops: the PENDING to DISPATCHED transition of the J-003 gift-box lifecycle, stamping dispatched_at. Emitted from the SetMemberStatus service seam; the HTTP surface that drives the transition is not wired yet, so no live traffic produces this today. repo.SetMemberStatus does not return the updated row, so state carries the parent Scheme and the member rides as locator + new status.
| Key | member party locator (PTY-YYYY-NNNNNN), mirroring member.added |
| Producers | group-scheme-service (services/group-scheme-service/internal/service/scheme.go:SetMemberStatus) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["scheme"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | schemeLocator memberPartyLocator status |
scheme.created
An employer group scheme now exists with an ACTIVE status: the employer-onboarding milestone that everything else (roster, bulk enrollment, billing anchors) hangs off. Guarded by the already-onboarded check, so one organisation name gets at most one ACTIVE scheme and a re-run funnel emits nothing. State freezes the created Scheme row.
| Key | scheme locator (SCH-YYYY-NNNNNN) |
| Producers | group-scheme-service (services/group-scheme-service/internal/service/scheme.go:CreateScheme) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["scheme"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | schemeLocator schemeName schemeCode employerPartyLocator status |
scheme.member_removed
A member was removed from an employer scheme. The scheme_members row is HARD deleted before this fires, so the removed member survives only as its locator in the payload and state carries the parent Scheme, the one subject entity still in scope at the emit site. Note the scheme.* prefix: this is the one member-lifecycle type not named member.*.
| Key | member party locator (PTY-YYYY-NNNNNN), mirroring member.added |
| Producers | group-scheme-service (services/group-scheme-service/internal/service/scheme.go:RemoveMember) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["scheme"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | schemeLocator memberPartyLocator |
scheme.updated
Scheme metadata changed via PATCH: name and/or status were rewritten and the full row persisted. The service applies no status transition validation, so status is whatever string the caller sent (ACTIVE is the only value the service itself ever writes). State freezes the post-update Scheme row.
| Key | scheme locator (SCH-YYYY-NNNNNN) |
| Producers | group-scheme-service (services/group-scheme-service/internal/service/scheme.go:UpdateScheme) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["scheme"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | schemeLocator schemeName schemeCode employerPartyLocator status |
identity.events
onboarding.account_created
A Keycloak member account now exists for this email, linked to its party via the party_locator attribute. Three producers with two payload shapes: the D2C OTP-verify and Google first-sign-in provisions add sub + flow (individual|google) and freeze the created user as state {user}; the employer-driven member-onboarding create instead adds userID + alreadyExisted and carries no state, so state.required is false. alreadyExisted=true means the emit re-announced an account that was already there (idempotent create), not a fresh provision.
| Key | party locator (PTY-YYYY-NNNNNN); every emit path passes party_locator, which publishOnboarding prefers as the key |
| Producers | identity (services/identity/internal/handler/otp.go:handleVerifyOTP)identity (services/identity/internal/handler/google_onboarding.go:handleGoogleOnboarding)identity (services/identity/internal/handler/member_onboarding.go:handleMemberOnboarding) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | email party_locator |
onboarding.device_bound
The member bound (or re-bound) a device to their account: the device record was appended to, or refreshed in, the Keycloak devices attribute. state {user} freezes the user WITH the post-write device list, which is fresher than the user row's own attribute at emit time. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | none: payload carries only sub + device_id; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/devices.go:handleBindDevice) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["user"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | sub device_id |
onboarding.device_faceid_set
The member toggled Face ID enrolment for one bound device. Only the boolean flag is stored and published; the biometric template never leaves the device. state {user} freezes the user with the post-write device list. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | none: payload carries only sub + device_id + enrolled; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/devices.go:handleSetDeviceFaceID) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["user"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | sub device_id enrolled |
onboarding.device_revoked
The member revoked one of their bound devices; the device record is kept but marked revoked, so a future PIN sign-in from it is refused. state {user} freezes the user with the post-revoke device list. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | none: payload carries only sub + device_id; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/devices.go:handleRevokeDevice) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["user"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | sub device_id |
onboarding.email_change_confirmed
The email change completed: the code matched and Keycloak now holds the new primary email, with the old address kept in a grace window and notified out of band. Emitted after the durable write; the payload carries both addresses for the audit trail. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | none: payload carries only sub + the two addresses; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/email_change.go:handleConfirmEmailChange) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | sub old_email new_email |
onboarding.email_change_resent
The member asked for the email-change confirmation code again; the code was re-issued to the pending new address with the attempt counter carried over (a resend is not a fresh brute-force budget). Payload-only. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | none: payload carries only sub; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/email_change.go:handleResendEmailChange) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | sub |
onboarding.email_change_started
A signed-in, PIN-gated member started changing their primary email: a pending-change entry was stashed and a 5-digit confirmation code was sent to the NEW address. Only sub and the requested address are in scope; nothing durable has changed yet. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | none: payload carries only sub + new_email; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/email_change.go:handleStartEmailChange) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | sub new_email |
onboarding.employer_activation_sent
An employer activation email went out. Two producers share the type: the legacy employer-onboarding path (Keycloak execute-actions email; payload carries userID + alreadyExisted, no state) and the magic-link activation path in handleSendMagicLink2, which carries sub and freezes the user as state {user}. Only email and org_locator are present on both, and state is optional because the legacy path emits none. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | member email (payload carries no party_locator) |
| Producers | identity (services/identity/internal/handler/onboarding.go:handleEmployerOnboarding)identity (services/identity/internal/handler/magic_link.go:handleSendMagicLink2) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | email org_locator |
onboarding.employer_magic_link_sent
An employer-portal sign-in magic link was generated, stashed, and emailed to an existing employer user. The resolved user is always frozen as state {user}; the token never rides the bus. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | the employer user's email |
| Producers | identity (services/identity/internal/handler/onboarding.go:handleSendMagicLink) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["user"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | email userID |
onboarding.godmode_user_deleted
An operator deleted a Keycloak user through the god-mode surface (X-Godmode-Key gated). email echoes the request and may be empty when the delete was by userID; the row is gone by emit time, so there is no state to freeze. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | the request email when given, else unkeyed |
| Producers | identity (services/identity/internal/handler/godmode_issue_pin.go:handleGodmodeDeleteUser) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | userID email |
onboarding.google_import_finished
The Google directory import callback completed and stashed its outcome for the SPA to collect: status is ready on success or error (consent denied, missing code, or fetch failure), and members counts the directory entries fetched. The member list itself stays in the token store, not on the bus. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | none: payload carries only status + members; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/onboarding_google_import.go:handleGoogleImportCallback) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | status members |
onboarding.google_import_started
An employer onboarding session was redirected to Google's consent screen to import the workspace member directory. Nothing is known beyond the session itself, so the payload is empty and the sessionId is the only tie to the walk. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | none: the payload is empty, so messages are unkeyed; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/onboarding_google_import.go:handleGoogleImportStart) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
onboarding.google_link_failed
A signed-in member tried to link a Google identity but the id_token failed verification; collapsed to one 401 with the precise reason kept in server logs only. Payload-only: the Google claims are untrusted at that point. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | none: payload carries only sub; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/link_google.go:handleLinkGoogle) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | sub |
onboarding.google_linked
A signed-in member linked a Google identity to their existing account: the id_token verified, the google_sub and google_email attributes were written, and fresh tokens were minted. The post-link user is always frozen as state {user} (nil-checked before the emit). identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | the member's primary email (payload carries no party_locator) |
| Producers | identity (services/identity/internal/handler/link_google.go:handleLinkGoogle) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["user"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | sub email google_email |
onboarding.google_signed_in
A member signed in with Google: the id_token verified, the Google identity was (re-)linked to the member, and tokens were minted. created is true when this sign-in provisioned the account (the sibling onboarding.account_created fires in the same request). The resolved user is always frozen as state {user}. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | party_locator when present, else the email |
| Producers | identity (services/identity/internal/handler/google_onboarding.go:handleGoogleOnboarding) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["user"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | email sub party_locator created |
onboarding.google_signup_failed
A Google sign-in presented an id_token that failed verification (bad signature, expired, wrong issuer or audience); collapsed to one 401 so the client cannot tell which check refused it. Nothing about the caller is trusted at that point, so the payload is empty. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | none: the payload is empty, so messages are unkeyed; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/google_onboarding.go:handleGoogleOnboarding) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
onboarding.login_disabled_account
A password login resolved to an account that is disabled in Keycloak; refused with an explicit 403 before any credential check. The resolved user is always frozen as state {user}. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | the attempted email (publishOnboarding sees no party_locator in this payload) |
| Producers | identity (services/identity/internal/handler/login.go:handleLogin) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["user"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | email sub |
onboarding.login_invalid_password
A password login resolved to a live account but the ROPC grant was refused (wrong password or any other grant refusal, collapsed to one 401). The resolved user is always frozen as state {user}; the password itself is never logged or published. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | the attempted email |
| Producers | identity (services/identity/internal/handler/login.go:handleLogin) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["user"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | email sub |
onboarding.login_lookup_error
A password login failed before authentication because the account lookup itself errored (Keycloak/backend fault, not bad credentials); the caller still gets the uniform 401 so the error is no oracle. The error text is for ops, keyed on the attempted email. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | the attempted email |
| Producers | identity (services/identity/internal/handler/login.go:handleLogin) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | email error |
onboarding.login_success
A member signed in with email + password: the ROPC grant succeeded and a token envelope was returned. The resolved user is always frozen as state {user}. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | the login email |
| Producers | identity (services/identity/internal/handler/login.go:handleLogin) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["user"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | email sub |
onboarding.login_unknown_email
A password login was attempted for an email no account (primary or linked secondary) owns; collapsed to the same 401 as a wrong password. Payload-only, keyed on the attempted email. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | the attempted email |
| Producers | identity (services/identity/internal/handler/login.go:handleLogin) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | email |
onboarding.magic_link_invalid
A magic-link redemption presented a token that is unknown, expired, or already consumed. Nothing about the caller is known at that point, so the payload is empty and the message is unkeyed; the sessionId from baggage is the only tie to the client walk. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | none: the payload is empty, so messages are unkeyed; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/magic_link_redeem.go:handleRedeemMagicLink) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
onboarding.magic_link_redeemed
A member redeemed a magic link: the single-use token matched and Keycloak tokens were minted for the stored user. state {user} freezes the fresh Keycloak lookup, but the emit degrades to payload-only when that lookup fails (the stored entry still authenticates), so state is optional. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | the entry email (payload carries no party_locator) |
| Producers | identity (services/identity/internal/handler/magic_link_redeem.go:handleRedeemMagicLink) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | email sub |
onboarding.magic_link_sent
A member sign-in magic link was generated, stashed in the token store, and emailed to the address the member asked for (their primary or a verified secondary). The resolved account owner is always frozen as state {user}; the token itself never rides the bus. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | member email (the address the link was sent to) |
| Producers | identity (services/identity/internal/handler/member_onboarding.go:handleSendMemberMagicLink) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["user"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | email sub |
onboarding.onboarding_session_scheme_bound
A pre-account onboarding session was bound to an employer scheme: the scheme locator was written onto the session entry so later JWT-gated onboarding hops act in that scheme's scope. The post-bind session entry is always frozen as state {onboarding}; a re-bind to the same scheme is idempotent and does not re-emit. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | the session email (no account or locator exists yet) |
| Producers | identity (services/identity/internal/handler/onboarding_session.go:handleVerifyOnboardingSession) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["onboarding"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | email scheme_locator |
onboarding.otp_invalid
An OTP verification failed: the submitted code did not match (or no code is pending) for the email. Two emit lines in handleVerifyOTP share the shape; the attempt counter and cooldown live in the token store, not on the event. Payload-only, keyed on the email. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | the email under verification |
| Producers | identity (services/identity/internal/handler/otp.go:handleVerifyOTP) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | email |
onboarding.otp_sent
A one-time verification code was generated, stored against the email, and sent out of band to the address starting onboarding or sign-in. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the funnel; the sessionId from W3C baggage is the only way to tie this pre-party event to the client session that caused it. state is deliberately absent: the subject entry holds credential material (the OTP code), which never rides the bus.
| Key | member email, NOT a locator: otp_sent fires pre-party, so no locator exists yet; publishOnboarding keys on party_locator else email, and otp_sent only carries email |
| Producers | identity (services/identity/internal/handler/otp.go:handleSendOTP) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | email |
onboarding.otp_verified
The member proved control of the email: the submitted code matched, was consumed (single use), and an onboarding session was minted. Four emit paths inside handleVerifyOTP: sign-in for an existing account and the D2C individual provision carry sub + party_locator and freeze the Keycloak user as state {user}; the pre-account employer email-verification ack and the no-Keycloak-admin degrade carry email only and no state, which is why state.required is false. party_locator may be an empty string when the Keycloak user lacks the attribute.
| Key | party_locator (PTY-YYYY-NNNNNN) when a Keycloak account exists at verify time, else the member email; publishOnboarding prefers party_locator |
| Producers | identity (services/identity/internal/handler/otp.go:handleVerifyOTP) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | email |
onboarding.password_change_confirmed
The password change completed: the code matched, the password history check passed, and the new credential is durable in Keycloak. Other sessions are deliberately not revoked (FR-25). Payload-only: only sub is in scope. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | none: payload carries only sub; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/password_change.go:handleConfirmPasswordChange) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | sub |
onboarding.password_change_resent
The member asked for the password-change confirmation code again; it was re-issued to the pending entry's email with the attempt counter carried over. Payload-only: only sub is in scope. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | none: payload carries only sub; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/password_change.go:handleResendPasswordChange) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | sub |
onboarding.password_change_started
A signed-in, PIN-gated member started changing their password: a pending-change entry was stashed and a 5-digit confirmation code was sent to their email. The looked-up Keycloak user is always frozen as state {user} (nil-checked before the emit); the payload deliberately carries only sub. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | none: payload carries only sub; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/password_change.go:handleStartPasswordChange) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["user"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | sub |
onboarding.password_set
The member set their account password through the onboarding set-password endpoint after proving email ownership; the credential was written via the Keycloak admin API. The single emit site always freezes the resolved Keycloak user as state {user} (the user is nil-checked before the write). Keyed on email because the payload carries no party_locator. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | member email (publishOnboarding prefers party_locator, and this payload carries only email + sub) |
| Producers | identity (services/identity/internal/handler/set_password.go:handleSetPassword) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["user"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | email sub |
onboarding.password_set_denied
A pre-auth attempt to set an account password was refused: the caller presented no proof of email ownership (X-Onboarding-Session or Bearer), or the proven email did not match the requested one. The reason field names which check failed; proven carries the mismatching proven email when there is one. No account state is in scope at either refusal, so the event is payload-only. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | member email (no party locator exists in scope at a denial) |
| Producers | identity (services/identity/internal/handler/set_password.go:handleSetPassword) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | email reason |
onboarding.pin_device_unbound
A PIN sign-in presented a device_id that is not bound (or was revoked) for the resolved member, so the sign-in was refused before the PIN was even checked. The resolved Keycloak user is always frozen as state {user}. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | none: the payload carries only sub + device_id, so messages are unkeyed; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/verify_pin.go:handleVerifyPin) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["user"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | sub device_id |
onboarding.pin_gate_failed
A signed-in member failed the PIN re-verification gate (the step-up check in front of email/password changes): wrong PIN and someone-else's PIN collapse to one 401 so the gate is no oracle. attempts is the running counter that drives the cooldown. Payload-only: the resolved user may not be the caller, so no state is frozen. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | none: payload carries only sub + attempts; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/pin_gate.go:handleMemberVerifyPin) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | sub attempts |
onboarding.pin_gate_passed
A signed-in member passed the PIN re-verification gate and was minted a short-lived gate token that unlocks the email/password change endpoints. The verified user is always frozen as state {user}; the gate token never rides the bus. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | none: payload carries only sub; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/pin_gate.go:handleMemberVerifyPin) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["user"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | sub |
onboarding.pin_invalid
A PIN sign-in attempt failed. Three emit shapes share the type: an unknown identifier carries only identifier, a wrong PIN for a resolved member carries sub and freezes the user as state {user}, and the legacy plaintext reverse-lookup miss carries party_locator (the submitted PIN). No field is present on every path, so nothing is required; state rides only on the resolved-member path. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | party_locator on the legacy-miss path, else unkeyed (identifier and sub are never used as keys) |
| Producers | identity (services/identity/internal/handler/verify_pin.go:handleVerifyPin) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
onboarding.pin_issued
God-mode created a fresh member with a newly allocated PIN (stored as the party_locator attribute) and best-effort emailed it; emailSent records whether that mail left. Only the new userID string is in scope (no full user entity was fetched), so the event is payload-only. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | the new member's PIN / party_locator |
| Producers | identity (services/identity/internal/handler/godmode_issue_pin.go:handleGodmodeIssuePin) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | email party_locator userID emailSent |
onboarding.pin_legacy_clear_failed
Setting a member PIN stored the new hash but failed to delete the legacy plaintext pin attribute; the member is usable, and the lingering attribute is cleaned up on the next write. Log-and-continue telemetry, payload-only. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | none: payload carries only sub; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/pin_set.go:handleSetMemberPIN) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | sub |
onboarding.pin_reissued
God-mode issue-pin hit an email that already has an account, so the existing PIN (the party_locator attribute) was surfaced again instead of re-keying the member. The existing user is always frozen as state {user}; party_locator may be empty when the account lacks the attribute. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | party_locator when present, else the email |
| Producers | identity (services/identity/internal/handler/godmode_issue_pin.go:handleGodmodeIssuePin) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["user"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | email party_locator userID |
onboarding.pin_set
The member set (or replaced) their 6-digit app PIN: the hash was written to the Keycloak pin_hash attribute and any legacy plaintext pin attribute retired. Only the Keycloak sub is in scope at the emit site, so the payload names no email or locator, the message has no key, and the sessionId from baggage is the only tie to the onboarding walk. state is deliberately absent: the subject user entity holds credential material, so it is skipped rather than frozen onto the bus.
| Key | none: publishOnboarding keys only on party_locator or email, and pin_set carries only sub, so messages are unkeyed; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/pin_set.go:handleSetMemberPIN) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | sub |
onboarding.pin_validated
A member signed in with their PIN: the hash matched (or the legacy reverse-lookup resolved), tokens were minted, and the session is live. The resolved Keycloak user is always frozen as state {user}. party_locator may be an empty string when the Keycloak user lacks the attribute; the PIN value itself is never logged or published. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | party_locator (PTY-YYYY-NNNNNN) when present, else the member email |
| Producers | identity (services/identity/internal/handler/verify_pin.go:handleVerifyPin) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["user"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | party_locator email sub |
onboarding.secondary_email_linked
The member completed linking a secondary email: the candidate address was written to the Keycloak secondary_emails attribute and fresh tokens were minted. The post-attach user is always frozen as state {user}. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | the member's primary email (payload carries no party_locator) |
| Producers | identity (services/identity/internal/handler/secondary_emails.go:handleRedeemSecondaryEmail) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["user"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | sub email candidate |
onboarding.secondary_email_rejected
A secondary-email verification token was redeemed by a bearer whose sub does not match the user the token was issued for; the link was refused with the same generic response as an expired token so validity is not leaked. Payload-only by design. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | none: payload carries only sub; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/secondary_emails.go:handleRedeemSecondaryEmail) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | sub |
onboarding.secondary_email_requested
A signed-in member asked to link a secondary email: a verification token was minted and mailed to the candidate address. The candidate is not yet verified, so no user state is frozen and the token never rides the bus. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | none: payload carries only sub + candidate; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/secondary_emails.go:handleInitSecondaryEmail) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | sub candidate |
onboarding.slack_connect_state_minted
An employer admin started the Slack workspace-connect flow: a single-use state credential was minted and stashed so the OAuth callback can be tied back to their scheme. Only locators are in scope, so the event is payload-only. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | none: payload carries only org_locator + scheme_locator; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/slack_connect_state.go:handleCreateSlackConnectState) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | org_locator scheme_locator |
onboarding.slack_import_finished
The Slack directory import callback completed and stashed its outcome for the SPA to collect: status reports the exchange-and-fetch result and members counts the directory entries. The member list itself stays in the token store, not on the bus; the callback never 5xxs, it always stashes an outcome. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | none: payload carries only status + members; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/onboarding_slack_import.go:handleSlackImportCallback) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | status members |
onboarding.slack_import_started
An employer flow was redirected to Slack's consent screen to import the workspace member directory. Nothing beyond the stashed credential is known, so the payload is empty and the sessionId is the only tie to the walk. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | none: the payload is empty, so messages are unkeyed; correlate via sessionId |
| Producers | identity (services/identity/internal/handler/onboarding_slack_import.go:handleSlackImportStart) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
onboarding.waitlist_joined
Someone joined the public waitlist. created is false when the email had already joined (the join is idempotent); no account exists, so the event is payload-only and keyed on email. identity is DB-free, so this stream (landed in BigQuery olly_analytics by the analytics sink) is the only durable record of the moment; the identity envelope carries session lineage from W3C baggage but no correlationId.
| Key | the joining email (no account or locator exists) |
| Producers | identity (services/identity/internal/handler/waitlist.go:handleJoinWaitlist) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | email created |
notifications.events
notification.dispatched
A dispatch outcome was logged: the member was, or measurably was not, told about a source event (status SENT, SKIPPED or FAILED, with failureReason saying why not). causationId is the SOURCE event's eventId (the platform's first causation stamp) and subjectLocator carries the source message's key, the quote or policy the notification is about. Emitted only when the notification_log row inserts, so a redelivered source event announces nothing twice. Published direct to Kafka (no outbox); correlationId rides the trace extracted from the source message's headers, so it is present in practice but not structurally guaranteed.
| Key | party locator (PTY-YYYY-NNNNNN) |
| Producers | notifications (services/notifications/internal/dispatch/dispatcher.go:writeLogWithSubjectBody) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["dispatch"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | channel failureReason notificationLocator partyLocator sourceEventId sourceEventType status subject subjectLocator |
policy-admin.events
account.created
An employer/broker account record was first created in policy-admin: the billing-level anchor (ACCOUNT | POLICY) that policies and invoices hang off. Mirrors member.party_created: fire-and-forget, so a publish failure never blocks the create, and state freezes the full account row as {account} because the locator points at a mutable row. The policy-admin producer stamps no session or correlation lineage yet, so the clientInteractive sessionId warning is expected on live events.
| Key | account locator (ACT-YYYY-NNNNNN) |
| Producers | policy-admin (services/policy-admin/internal/service/account.go:CreateAccount) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["account"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | accountLocator name billingLevel currency |
member.party_created
A party row (member, organisation or provider; type is passed through unvalidated, documented values INDIVIDUAL | ORGANISATION | PROVIDER) was first created in policy-admin: the P0 onboarding fact. D2C onboarding reaches it through identity's PartyCreator hitting the internal idempotent create, which reuses an existing party by email WITHOUT re-emitting, so one party means one event. state freezes the full party row as {party} because the locator points at a mutable row. The policy-admin producer stamps no session or correlation lineage yet, so the clientInteractive sessionId warning is expected on live events until it lifts baggage like identity's does.
| Key | party locator (PTY-YYYY-NNNNNN) |
| Producers | policy-admin (services/policy-admin/internal/service/party.go:CreateParty) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["party"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | partyLocator type name email |
party.updated
A party's mutable details (names, email, phone) changed via UpdateParty. Mirrors member.party_created: fire-and-forget, and state freezes the full post-update party row as {party} because the payload's locator points at a mutable row and cannot tell an auditor what changed to what. No session or correlation lineage is stamped by this producer yet.
| Key | party locator (PTY-YYYY-NNNNNN) |
| Producers | policy-admin (services/policy-admin/internal/service/party.go:UpdateParty) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["party"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | partyLocator type name email |
product.published
A product version went live: PublishVersion flipped the row to PUBLISHED, after which its schemas are immutable and quotes can rate against it. state freezes both entities in scope as {product} and {productVersion}; note the in-memory version row predates the publish transition, so its Status/PublishedAt still show the pre-publish values and the event itself is the transition marker. Fire-and-forget, with no session or correlation lineage stamped by this producer yet.
| Key | product VERSION locator (PVR-YYYY-NNNNNN); the product's own locator (PRD-) rides in the payload |
| Producers | policy-admin (services/policy-admin/internal/service/product.go:PublishVersion) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["product","productVersion"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | productLocator versionLocator version code name |
provider.events
credentialing.approved
A SUBMITTED credentialing request was approved via PATCH /credentialing/{locator}/approve, which in the same service call also sets the linked provider ACTIVE and stamps activated_at. The payload carries the reviewer's notes, possibly empty. State freezes the post-decision request when the handler's response re-read succeeds; state.required is false because that re-read can fail while the publish still happens.
| Key | credentialing request locator (CRD-YYYY-NNNNNN) |
| Producers | provider (services/provider/internal/handler/credentialing.go:approve) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | credentialingLocator reviewerNotes |
credentialing.rejected
A SUBMITTED credentialing request was rejected via PATCH /credentialing/{locator}/reject, recording the decision and reviewer notes while leaving the provider untouched (it stays PENDING). State freezes the post-decision request when the handler's response re-read succeeds; state.required is false because that re-read can fail while the publish still happens.
| Key | credentialing request locator (CRD-YYYY-NNNNNN) |
| Producers | provider (services/provider/internal/handler/credentialing.go:reject) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | credentialingLocator reviewerNotes |
credentialing.submitted
A credentialing application was filed for an existing provider via POST /credentialing, creating the request in SUBMITTED and starting provider-network onboarding. The provider itself stays PENDING until an approve decision. State freezes the new request plus the provider it credentials.
| Key | credentialing request locator (CRD-YYYY-NNNNNN) |
| Producers | provider (services/provider/internal/handler/credentialing.go:submit) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["credentialing","provider"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | credentialingLocator providerLocator status |
provider.activated
A provider reached ACTIVE network status via PATCH /providers/{locator}/activate and is now matchable for members. Note that credentialing approval also activates the provider through the same service call but announces itself as credentialing.approved, not this event. State freezes the post-transition provider row.
| Key | provider locator (PRV-YYYY-NNNNNN) |
| Producers | provider (services/provider/internal/handler/providers.go:activate) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["provider"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | providerLocator npi name networkStatus |
provider.created
A provider was registered into the network directory via POST /providers, starting life as PENDING. Fires after the row is committed; a partyLocator is present only when the caller linked a Policy Admin party at create time. State freezes the just-created provider row.
| Key | provider locator (PRV-YYYY-NNNNNN) |
| Producers | provider (services/provider/internal/handler/providers.go:create) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["provider"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | providerLocator npi name specialty partyLocator networkStatus |
provider.deactivated
A provider was set INACTIVE via PATCH /providers/{locator}/deactivate and is no longer matchable for members. Mirrors provider.activated as the other direction of the network-status pair. State freezes the post-transition provider row.
| Key | provider locator (PRV-YYYY-NNNNNN) |
| Producers | provider (services/provider/internal/handler/providers.go:deactivate) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["provider"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | providerLocator npi name networkStatus |
provider.reviewed
A member review was recorded against a clinician profile via POST /providers/{locator}/reviews (#1108), shifting the aggregate rating and count members see. The aggregate itself is computed on read, not stored, so the event carries the single review that moved it. State freezes the new review row plus the reviewed provider row.
| Key | provider locator (PRV-YYYY-NNNNNN) |
| Producers | provider (services/provider/internal/handler/reviews.go:addReview) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | required, subjects ["review","provider"] |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | providerLocator reviewId author rating |
provider.searched
A caller searched the provider directory (GET /providers or /providers/list), a behavioural core-journey signal rather than a row change. Fires on every list call with the filters used and the result counts; there is no persisted subject entity, so the event carries no state.
| Key | the specialty filter string; empty when the search had no specialty filter, which leaves the Kafka key unset |
| Producers | provider (services/provider/internal/handler/providers.go:list) |
| Consumers | debugger (services/debugger/internal/consumer/consumer.go) |
| State | not required |
| Lineage | correlation optional · client-interactive (session expected) |
| Payload required | specialty networkStatus resultCount total |
Full schemas and golden examples live beside each definition in the repo. To add or evolve an event type, see packages/go/domain/eventregistry/README.md.
