Skip to content

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.

Keybilling account locator (ACC-YYYY-NNNNNN)
Producersbilling (services/billing/internal/job/delinquency.go:processRow)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
document-service (services/document-service/internal/kafka/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Statenot required
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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.

Keyadjustment locator (ADJ-<uuid>)
Producersbilling (services/billing/internal/service/billing.go:ApplyAdjustment)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
document-service (services/document-service/internal/kafka/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Statenot required
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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).

Keyadjustment locator (ADJ-<uuid>)
Producersbilling (services/billing/internal/service/billing.go:CreateAdjustment)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
document-service (services/document-service/internal/kafka/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["adjustment"]
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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.

Keyadjustment locator (ADJ-<uuid>)
Producersbilling (services/billing/internal/service/billing.go:ReverseAdjustment)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
document-service (services/document-service/internal/kafka/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Statenot required
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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.

Keycharge locator (CHG-YYYY-NNNNNN)
Producersbilling (services/billing/internal/service/onboarding.go:SettleFunnelPayment)
billing (services/billing/internal/service/direct_purchase.go:SettleDirectPurchase)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
document-service (services/document-service/internal/kafka/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["charge"]
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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.

Keycharge locator (CHG-YYYY-NNNNNN)
Producersbilling (services/billing/internal/service/billing.go:VoidCharge)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
document-service (services/document-service/internal/kafka/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Statenot required
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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.

Keyinstallment schedule locator (ISC-YYYY-NNNNNN)
Producersbilling (services/billing/internal/service/installment.go:CancelSchedule)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
document-service (services/document-service/internal/kafka/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Statenot required
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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.

Keyinstallment schedule locator (ISC-YYYY-NNNNNN)
Producersbilling (services/billing/internal/service/installment.go:CreateSchedule)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
document-service (services/document-service/internal/kafka/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["schedule"]
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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.

Keyinvoice locator (INV-YYYY-NNNNNN)
Producersbilling (services/billing/internal/job/delinquency.go:processRow)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
document-service (services/document-service/internal/kafka/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Statenot required
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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.

Keyinvoice locator (INV-YYYY-NNNNNN)
Producersbilling (services/billing/internal/service/billing.go:FinaliseInvoice)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
document-service (services/document-service/internal/kafka/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Statenot required
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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.

Keyinvoice locator (INV-YYYY-NNNNNN)
Producersbilling (services/billing/internal/job/delinquency.go:processRow)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
document-service (services/document-service/internal/kafka/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Statenot required
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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.

Keyinvoice locator (INV-YYYY-NNNNNN)
Producersbilling (services/billing/internal/job/lapse.go:processRow)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
document-service (services/document-service/internal/kafka/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Statenot required
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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.

Keyinvoice locator (INV-YYYY-NNNNNN)
Producersbilling (services/billing/internal/service/onboarding.go:SettleFunnelPayment)
billing (services/billing/internal/service/direct_purchase.go:SettleDirectPurchase)
billing (services/billing/internal/service/billing.go:RecordPayment)
Consumersdocument-service (services/document-service/internal/kafka/consumer.go)
notifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Statenot required
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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.

Keyinvoice locator (INV-YYYY-NNNNNN)
Producersbilling (services/billing/internal/service/billing.go:VoidInvoice)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
document-service (services/document-service/internal/kafka/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Statenot required
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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.

Keypayment locator (PAY-YYYY-NNNNNN)
Producersbilling (services/billing/internal/service/billing.go:RecordPayment)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
document-service (services/document-service/internal/kafka/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["payment"]
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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.

Keypayment locator (PAY-YYYY-NNNNNN)
Producersbilling (services/billing/internal/service/billing.go:VoidPayment)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
document-service (services/document-service/internal/kafka/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Statenot required
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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.

Keypayment locator (PAY-YYYY-NNNNNN)
Producersbilling (services/billing/internal/service/onboarding.go:SettleFunnelPayment)
billing (services/billing/internal/service/direct_purchase.go:SettleDirectPurchase)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
document-service (services/document-service/internal/kafka/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["payment"]
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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.

Keypayment locator (PAY-YYYY-NNNNNN)
Producersbilling (services/billing/internal/service/billing.go:VoidPayment)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
document-service (services/document-service/internal/kafka/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Statenot required
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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.

Keyquote locator (QTE-YYYY-NNNNNN)
Producersbilling (services/billing/internal/repository/gorm_quote_payments.go:MarkQuoteFailed)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
document-service (services/document-service/internal/kafka/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["quotePayment"]
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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.

Keyquote locator (QTE-YYYY-NNNNNN)
Producersbilling (services/billing/internal/handler/payment_intents.go:createD2CPaymentIntentHandler)
billing (services/billing/internal/handler/subscriptions.go:createD2CSubscriptionHandler)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
document-service (services/document-service/internal/kafka/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects []
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyquote locator (QTE-YYYY-NNNNNN)
Producersbilling (services/billing/internal/repository/gorm_quote_payments.go:MarkQuotePaid)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
document-service (services/document-service/internal/kafka/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["quotePayment"]
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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.

KeybrokerId (the broker's Keycloak brokerId claim)
Producersbroker-api (services/broker-api/internal/service/authority.go:Upsert)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["brokerConfig"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredbrokerConfigLocator 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.

KeybrokerId (the issued policy's broker_locator)
Producersbroker-api (services/broker-api/internal/consumer/projector.go:ProcessPolicyIssued)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["policy"]
Lineagecorrelation optional · not client-interactive
Payload requiredbrokerId 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.

KeybrokerId (the broker's Keycloak brokerId claim)
Producersbroker-api (services/broker-api/internal/service/authority.go:Upsert)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["brokerConfig"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredbrokerConfigLocator 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).

KeybrokerId (the broker's Keycloak brokerId claim / policy broker_locator)
Producersbroker-api (services/broker-api/internal/service/commission.go:Create)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["commission"]
Lineagecorrelation optional · not client-interactive
Payload requiredcommissionLocator 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.

KeybrokerId (the broker's Keycloak brokerId claim)
Producersbroker-api (services/broker-api/internal/service/commission.go:MarkPaid)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["commission"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredcommissionLocator brokerId policyLocator amount status

care.events

care.appointment.attended

An appointment was marked ATTENDED via the attend action. state.appointment freezes the attended row.

Keyappointment locator (APT-xxxxxxxx)
Producerscare (services/care/internal/service/service.go:UpdateAppointmentStatus)
Consumersnotifications (services/notifications/internal/dispatch/dispatcher.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["appointment"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyappointment locator (APT-xxxxxxxx)
Producerscare (services/care/internal/service/service.go:UpdateAppointmentStatus)
Consumersnotifications (services/notifications/internal/dispatch/dispatcher.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["appointment"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyappointment locator (APT-xxxxxxxx)
Producerscare (services/care/internal/service/service.go:UpdateAppointmentStatus)
Consumersnotifications (services/notifications/internal/dispatch/dispatcher.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["appointment"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventId 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.

Keyappointment locator (APT-xxxxxxxx)
Producerscare (services/care/internal/service/service.go:UpdateAppointmentStatus)
Consumersnotifications (services/notifications/internal/dispatch/dispatcher.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["appointment"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyappointment locator (APT-xxxxxxxx)
Producerscare (services/care/internal/service/reminders.go:SweepReminders)
Consumersnotifications (services/notifications/internal/dispatch/dispatcher.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["appointment"]
Lineagecorrelation optional · not client-interactive
Payload requiredeventId 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.

Keyepisode locator (EP-xxxxxxxx)
Producerscare (services/care/internal/service/service.go:CancelEpisode)
Consumersnotifications (services/notifications/internal/dispatch/dispatcher.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["episode"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyepisode locator (EP-xxxxxxxx)
Producerscare (services/care/internal/service/service.go:CloseEpisode)
Consumersnotifications (services/notifications/internal/dispatch/dispatcher.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["episode"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyepisode locator (EP-xxxxxxxx)
Producerscare (services/care/internal/service/service.go:CreateEpisode)
Consumersnotifications (services/notifications/internal/dispatch/dispatcher.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["episode"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyprescription locator (RX-xxxxxxxx)
Producerscare (services/care/internal/service/service.go:UpdatePrescriptionStatus)
Consumersnotifications (services/notifications/internal/dispatch/dispatcher.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["prescription"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyprescription locator (RX-xxxxxxxx)
Producerscare (services/care/internal/service/service.go:IssuePrescription)
Consumersnotifications (services/notifications/internal/dispatch/dispatcher.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["prescription"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType prescriptionLocator episodeLocator providerLocator occurredAt

care.prescription.voided

A prescription was voided via the void action. state.prescription freezes the voided row.

Keyprescription locator (RX-xxxxxxxx)
Producerscare (services/care/internal/service/service.go:UpdatePrescriptionStatus)
Consumersnotifications (services/notifications/internal/dispatch/dispatcher.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["prescription"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType prescriptionLocator episodeLocator providerLocator occurredAt

care.referral.cancelled

A referral was cancelled via the cancel action. state.referral freezes the cancelled row.

Keyreferral locator (REF-xxxxxxxx)
Producerscare (services/care/internal/service/service.go:UpdateReferralStatus)
Consumersnotifications (services/notifications/internal/dispatch/dispatcher.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["referral"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyreferral locator (REF-xxxxxxxx)
Producerscare (services/care/internal/service/service.go:UpdateReferralStatus)
Consumersnotifications (services/notifications/internal/dispatch/dispatcher.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["referral"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyreferral locator (REF-xxxxxxxx)
Producerscare (services/care/internal/service/service.go:CreateReferral)
Consumersnotifications (services/notifications/internal/dispatch/dispatcher.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["referral"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyclaim locator (CLM-YYYY-NNNNNN)
Producersclaims (services/claims/internal/service/claim.go:ReviewClaim)
claims (services/claims/internal/service/claim.go:ApproveClaim)
claims (services/claims/internal/service/claim.go:autoApprove)
Consumersbilling (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)
Staterequired, subjects ["claim"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyclaim locator (CLM-YYYY-NNNNNN)
Producersclaims (services/claims/internal/service/claim.go:CloseClaim)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["claim"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyclaim locator (CLM-YYYY-NNNNNN)
Producersclaims (services/claims/internal/service/claim.go:RequestInfo)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["claim"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyclaim locator (CLM-YYYY-NNNNNN)
Producersclaims (services/claims/internal/service/claim.go:PayClaim)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["claim"]
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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.

Keyclaim locator (CLM-YYYY-NNNNNN)
Producersclaims (services/claims/internal/service/claim.go:PayClaim)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["claim"]
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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.

Keyclaim locator (CLM-YYYY-NNNNNN)
Producersclaims (services/claims/internal/service/claim.go:RejectClaim)
claims (services/claims/internal/kafka/handlers.go:autoDenyOpenClaims)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["claim"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyclaim locator (CLM-YYYY-NNNNNN)
Producersclaims (services/claims/internal/service/claim.go:ReviewClaim)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["claim"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyclaim locator (CLM-YYYY-NNNNNN)
Producersclaims (services/claims/internal/service/claim.go:SubmitClaim)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["claim"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyprior-auth locator (PAU-YYYY-NNNNNN)
Producersclaims (services/claims/internal/service/priorauth.go:Review)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["priorAuth"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyprior-auth locator (PAU-YYYY-NNNNNN)
Producersclaims (services/claims/internal/service/priorauth.go:Submit)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["priorAuth"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyprior-auth locator (PAU-YYYY-NNNNNN)
Producersclaims (services/claims/internal/service/priorauth.go:Approve)
claims (services/claims/internal/service/priorauth.go:Deny)
Consumersdocument-service (services/document-service/internal/kafka/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["priorAuth"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keynone (published unkeyed by the direct producers)
Producersclaims (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)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredrecord_type record_locator viewer_subject viewed_at path

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.

Keyparty locator (PTY-YYYY-NNNNNN)
Producersconsent (services/consent/internal/handler/consent.go:recordConsentChange)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["consent"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType partyLocator consentType granted changedByType occurredAt

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.

Keyparty locator of the member to erase (PTY-YYYY-NNNNNN)
Producersconsent (services/consent/internal/handler/deletion.go:postDeletion)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["deletionRequest"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType id partyLocator deleteType occurredAt

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.

Keyparty locator (PTY-YYYY-NNNNNN)
Producersconsent (services/consent/internal/handler/consent.go:recordConsentChange)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["consent"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType partyLocator consentType granted changedByType occurredAt

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.

Keyparty locator (PTY-YYYY-NNNNNN)
Producersconsent (services/consent/internal/handler/consent.go:recordConsentChange)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["consent"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyparty locator of the erased member (PTY-YYYY-NNNNNN)
Producersconsent (services/consent/internal/job/erasure.go:processRequest)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["deletionRequest"]
Lineagecorrelation optional · not client-interactive
Payload requiredeventType 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.

Keyparty locator of the member whose erasure failed (PTY-YYYY-NNNNNN)
Producersconsent (services/consent/internal/job/erasure.go:processRequest)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["deletionRequest"]
Lineagecorrelation optional · not client-interactive
Payload requiredeventType 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.

Keypolicy locator first, then party / invoice / document locator (domain.MessageKeyOf order in services/document-service/internal/outbox/producer.go:PublishState)
Producersdocument-service (services/document-service/internal/service/document.go:RecordDownload)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["document"]
LineagecorrelationId required · client-interactive (session expected)
Payload requireddocumentLocator 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.

Keypolicy 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
Producersdocument-service (services/document-service/internal/service/document.go:emitGenerationFailed)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
LineagecorrelationId required · not client-interactive
Payload requiredpartyLocator 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.

Keypolicy locator first, then party / invoice / document locator (domain.MessageKeyOf order in services/document-service/internal/outbox/producer.go:PublishState)
Producersdocument-service (services/document-service/internal/service/document.go:Generate)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["document"]
LineagecorrelationId required · client-interactive (session expected)
Payload requireddocumentLocator 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.

Keypolicy locator first, then party / invoice / document locator (domain.MessageKeyOf order in services/document-service/internal/outbox/producer.go:PublishState)
Producersdocument-service (services/document-service/internal/service/document.go:Generate)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["document"]
LineagecorrelationId required · not client-interactive
Payload requireddocumentLocator 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.

Keymember's party locator (PTY-YYYY-NNNNNN)
Producerseligibility (services/eligibility/internal/handler/internal.go:internalApplyAccumulatorsHandler)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · not client-interactive
Payload requiredmemberLocator 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.

Keymember's party locator (PTY-YYYY-NNNNNN)
Producerseligibility (services/eligibility/internal/projection/handlers.go:publishAccumulatorsReset)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · not client-interactive
Payload requiredmemberLocator 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.

Keymember's party locator (PTY-YYYY-NNNNNN)
Producerseligibility (services/eligibility/internal/projection/handlers.go:publishCoverageChanged)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["coverage"]
Lineagecorrelation optional · not client-interactive
Payload requiredmemberLocator 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.

Keymember's party locator as the caller supplied it
Producerseligibility (services/eligibility/internal/handler/check.go:runCheck)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · not client-interactive
Payload requiredmemberLocator 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.

Keymember's party locator (PTY-YYYY-NNNNNN)
Producerseligibility (services/eligibility/internal/handler/check.go:runCheck)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["coverage","accumulators"]
Lineagecorrelation optional · not client-interactive
Payload requiredmemberLocator 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.

Keyquote locator (QTE-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/quote.go:priceD2C)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["quote"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyelement locator (ELM-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/quote.go:Issue)
enrollment (services/enrollment/internal/service/transaction.go:ApplyElementChange)
enrollment (services/enrollment/internal/service/transaction.go:Apply)
Consumerseligibility (services/eligibility/internal/projection/handlers.go:handleElementAdded)
notifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["element"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyelement locator (ELM-YYYY-NNNNNN) of the NEW element version
Producersenrollment (services/enrollment/internal/service/transaction.go:ApplyElementChange)
enrollment (services/enrollment/internal/service/transaction.go:Apply)
Consumerseligibility (services/eligibility/internal/projection/handlers.go:handleElementRemoved)
notifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["element"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyelement locator (ELM-YYYY-NNNNNN) of the NEW element version
Producersenrollment (services/enrollment/internal/service/upgrade.go:UpgradeCoverage)
enrollment (services/enrollment/internal/service/transaction.go:Apply)
enrollment (services/enrollment/internal/service/transaction.go:ApplyElementChange)
Consumerseligibility (services/eligibility/internal/projection/handlers.go:handleElementUpdated)
notifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["element"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyquote locator (QTE-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/quote.go:priceD2C)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["quote"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyquote locator (QTE-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/quote.go:priceD2C)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["quote"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keypolicy locator (POL-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/quote.go:Issue)
enrollment (services/enrollment/internal/service/policy.go:IssueInternal)
enrollment (services/enrollment/internal/job/activation.go:Tick)
Consumerseligibility (services/eligibility/internal/projection/handlers.go:handlePolicyActivated)
notifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["policy"]
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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).

Keypolicy locator (POL-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/policy.go:Cancel)
enrollment (services/enrollment/internal/service/transaction.go:Apply)
Consumerseligibility (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)
Staterequired, subjects ["policy","transaction"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keypolicy locator (POL-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/upgrade.go:UpgradeCoverage)
enrollment (services/enrollment/internal/service/transaction.go:Apply)
Consumerseligibility (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)
Staterequired, subjects ["policy","transaction"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keypolicy locator (POL-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/policy.go:Endorse)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["policy","transaction"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keypolicy locator (POL-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/quote.go:Issue)
enrollment (services/enrollment/internal/service/policy.go:IssueInternal)
Consumerseligibility (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)
Staterequired, subjects ["policy"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keypolicy locator (POL-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/policy.go:Lapse)
Consumerseligibility (services/eligibility/internal/projection/handlers.go:handlePolicyCancelledOrLapsed)
notifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["policy"]
LineagecorrelationId required · not client-interactive
Payload requiredeventType 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.

Keypolicy locator (POL-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/policy.go:Reinstate)
enrollment (services/enrollment/internal/service/transaction.go:Apply)
Consumerseligibility (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)
Staterequired, subjects ["policy","transaction"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keypolicy locator (POL-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/policy.go:Renew)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["policy","transaction"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keypolicy locator (POL-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/transaction.go:Apply)
Consumerseligibility (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)
Staterequired, subjects ["policy","transaction"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyquote locator (QTE-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/quote.go:priceD2C)
enrollment (services/enrollment/internal/service/quote.go:Price)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["quote"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyquote locator (QTE-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/quote.go:Issue)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["quote","policy"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyquote locator (QTE-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/quote.go:priceD2C)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["quote"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyquote locator (QTE-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/quote.go:Create)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["quote"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyquote locator (QTE-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/quote.go:priceD2C)
enrollment (services/enrollment/internal/service/quote.go:Refuse)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["quote"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyquote locator (QTE-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/quote.go:Discard)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["quote"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyquote locator (QTE-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/quote.go:UpdateMemberDeclaration)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["quote"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyquote locator (QTE-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/quote.go:ReviewCheck)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["quote"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyquote locator (QTE-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/quote.go:ReviewCheck)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["quote"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyquote locator (QTE-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/quote.go:priceD2C)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["quote"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyscheme locator (SCH-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/policy.go:IssueInternal)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["policy"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keyquote locator (QTE-YYYY-NNNNNN)
Producersenrollment (services/enrollment/internal/service/quote.go:priceD2C)
Consumersnotifications (services/notifications/internal/projection/consumer.go)
debugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["quote"]
LineagecorrelationId required · client-interactive (session expected)
Payload requiredeventType 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.

Keybulk job locator (BMJ-YYYY-NNNNNN)
Producersgroup-scheme-service (services/group-scheme-service/internal/service/bulk_enrollment.go:RunJob)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · not client-interactive
Payload requiredjobLocator 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.

Keybulk job locator (BMJ-YYYY-NNNNNN)
Producersgroup-scheme-service (services/group-scheme-service/internal/service/bulk_enrollment.go:RunJob)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · not client-interactive
Payload requiredjobLocator 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).

Keymember party locator (PTY-YYYY-NNNNNN), mirroring member.added
Producersgroup-scheme-service (services/group-scheme-service/internal/service/scheme.go:SetMemberStatus)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["scheme"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredschemeLocator 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.

Keymember party locator (PTY-YYYY-NNNNNN); member events key on the member, not the scheme
Producersgroup-scheme-service (services/group-scheme-service/internal/service/scheme.go:AddMember)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["member","scheme"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredschemeLocator 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.

Keymember party locator (PTY-YYYY-NNNNNN), mirroring member.added
Producersgroup-scheme-service (services/group-scheme-service/internal/service/scheme.go:SetMemberStatus)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["scheme"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredschemeLocator 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.

Keyscheme locator (SCH-YYYY-NNNNNN)
Producersgroup-scheme-service (services/group-scheme-service/internal/service/scheme.go:CreateScheme)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["scheme"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredschemeLocator 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.*.

Keymember party locator (PTY-YYYY-NNNNNN), mirroring member.added
Producersgroup-scheme-service (services/group-scheme-service/internal/service/scheme.go:RemoveMember)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["scheme"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredschemeLocator 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.

Keyscheme locator (SCH-YYYY-NNNNNN)
Producersgroup-scheme-service (services/group-scheme-service/internal/service/scheme.go:UpdateScheme)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["scheme"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredschemeLocator 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.

Keyparty locator (PTY-YYYY-NNNNNN); every emit path passes party_locator, which publishOnboarding prefers as the key
Producersidentity (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)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredemail 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.

Keynone: payload carries only sub + device_id; correlate via sessionId
Producersidentity (services/identity/internal/handler/devices.go:handleBindDevice)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["user"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredsub 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.

Keynone: payload carries only sub + device_id + enrolled; correlate via sessionId
Producersidentity (services/identity/internal/handler/devices.go:handleSetDeviceFaceID)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["user"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredsub 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.

Keynone: payload carries only sub + device_id; correlate via sessionId
Producersidentity (services/identity/internal/handler/devices.go:handleRevokeDevice)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["user"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredsub 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.

Keynone: payload carries only sub + the two addresses; correlate via sessionId
Producersidentity (services/identity/internal/handler/email_change.go:handleConfirmEmailChange)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredsub 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.

Keynone: payload carries only sub; correlate via sessionId
Producersidentity (services/identity/internal/handler/email_change.go:handleResendEmailChange)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredsub

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.

Keynone: payload carries only sub + new_email; correlate via sessionId
Producersidentity (services/identity/internal/handler/email_change.go:handleStartEmailChange)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredsub 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.

Keymember email (payload carries no party_locator)
Producersidentity (services/identity/internal/handler/onboarding.go:handleEmployerOnboarding)
identity (services/identity/internal/handler/magic_link.go:handleSendMagicLink2)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredemail org_locator

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.

Keythe employer user's email
Producersidentity (services/identity/internal/handler/onboarding.go:handleSendMagicLink)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["user"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredemail 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.

Keythe request email when given, else unkeyed
Producersidentity (services/identity/internal/handler/godmode_issue_pin.go:handleGodmodeDeleteUser)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requireduserID 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.

Keynone: payload carries only status + members; correlate via sessionId
Producersidentity (services/identity/internal/handler/onboarding_google_import.go:handleGoogleImportCallback)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredstatus 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.

Keynone: the payload is empty, so messages are unkeyed; correlate via sessionId
Producersidentity (services/identity/internal/handler/onboarding_google_import.go:handleGoogleImportStart)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)

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.

Keynone: payload carries only sub; correlate via sessionId
Producersidentity (services/identity/internal/handler/link_google.go:handleLinkGoogle)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredsub

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.

Keythe member's primary email (payload carries no party_locator)
Producersidentity (services/identity/internal/handler/link_google.go:handleLinkGoogle)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["user"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredsub 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.

Keyparty_locator when present, else the email
Producersidentity (services/identity/internal/handler/google_onboarding.go:handleGoogleOnboarding)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["user"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredemail 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.

Keynone: the payload is empty, so messages are unkeyed; correlate via sessionId
Producersidentity (services/identity/internal/handler/google_onboarding.go:handleGoogleOnboarding)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation 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.

Keythe attempted email (publishOnboarding sees no party_locator in this payload)
Producersidentity (services/identity/internal/handler/login.go:handleLogin)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["user"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredemail 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.

Keythe attempted email
Producersidentity (services/identity/internal/handler/login.go:handleLogin)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["user"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredemail 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.

Keythe attempted email
Producersidentity (services/identity/internal/handler/login.go:handleLogin)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredemail 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.

Keythe login email
Producersidentity (services/identity/internal/handler/login.go:handleLogin)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["user"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredemail 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.

Keythe attempted email
Producersidentity (services/identity/internal/handler/login.go:handleLogin)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredemail

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.

Keynone: the payload is empty, so messages are unkeyed; correlate via sessionId
Producersidentity (services/identity/internal/handler/magic_link_redeem.go:handleRedeemMagicLink)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)

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.

Keythe entry email (payload carries no party_locator)
Producersidentity (services/identity/internal/handler/magic_link_redeem.go:handleRedeemMagicLink)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredemail sub

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.

Keymember email (the address the link was sent to)
Producersidentity (services/identity/internal/handler/member_onboarding.go:handleSendMemberMagicLink)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["user"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredemail 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.

Keythe session email (no account or locator exists yet)
Producersidentity (services/identity/internal/handler/onboarding_session.go:handleVerifyOnboardingSession)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["onboarding"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredemail 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.

Keythe email under verification
Producersidentity (services/identity/internal/handler/otp.go:handleVerifyOTP)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredemail

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.

Keymember 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
Producersidentity (services/identity/internal/handler/otp.go:handleSendOTP)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredemail

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.

Keyparty_locator (PTY-YYYY-NNNNNN) when a Keycloak account exists at verify time, else the member email; publishOnboarding prefers party_locator
Producersidentity (services/identity/internal/handler/otp.go:handleVerifyOTP)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredemail

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.

Keynone: payload carries only sub; correlate via sessionId
Producersidentity (services/identity/internal/handler/password_change.go:handleConfirmPasswordChange)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredsub

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.

Keynone: payload carries only sub; correlate via sessionId
Producersidentity (services/identity/internal/handler/password_change.go:handleResendPasswordChange)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredsub

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.

Keynone: payload carries only sub; correlate via sessionId
Producersidentity (services/identity/internal/handler/password_change.go:handleStartPasswordChange)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["user"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredsub

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.

Keymember email (publishOnboarding prefers party_locator, and this payload carries only email + sub)
Producersidentity (services/identity/internal/handler/set_password.go:handleSetPassword)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["user"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredemail 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.

Keymember email (no party locator exists in scope at a denial)
Producersidentity (services/identity/internal/handler/set_password.go:handleSetPassword)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredemail 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.

Keynone: the payload carries only sub + device_id, so messages are unkeyed; correlate via sessionId
Producersidentity (services/identity/internal/handler/verify_pin.go:handleVerifyPin)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["user"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredsub 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.

Keynone: payload carries only sub + attempts; correlate via sessionId
Producersidentity (services/identity/internal/handler/pin_gate.go:handleMemberVerifyPin)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredsub 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.

Keynone: payload carries only sub; correlate via sessionId
Producersidentity (services/identity/internal/handler/pin_gate.go:handleMemberVerifyPin)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["user"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredsub

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.

Keyparty_locator on the legacy-miss path, else unkeyed (identifier and sub are never used as keys)
Producersidentity (services/identity/internal/handler/verify_pin.go:handleVerifyPin)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation 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.

Keythe new member's PIN / party_locator
Producersidentity (services/identity/internal/handler/godmode_issue_pin.go:handleGodmodeIssuePin)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredemail 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.

Keynone: payload carries only sub; correlate via sessionId
Producersidentity (services/identity/internal/handler/pin_set.go:handleSetMemberPIN)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredsub

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.

Keyparty_locator when present, else the email
Producersidentity (services/identity/internal/handler/godmode_issue_pin.go:handleGodmodeIssuePin)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["user"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredemail 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.

Keynone: publishOnboarding keys only on party_locator or email, and pin_set carries only sub, so messages are unkeyed; correlate via sessionId
Producersidentity (services/identity/internal/handler/pin_set.go:handleSetMemberPIN)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredsub

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.

Keyparty_locator (PTY-YYYY-NNNNNN) when present, else the member email
Producersidentity (services/identity/internal/handler/verify_pin.go:handleVerifyPin)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["user"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredparty_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.

Keythe member's primary email (payload carries no party_locator)
Producersidentity (services/identity/internal/handler/secondary_emails.go:handleRedeemSecondaryEmail)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["user"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredsub 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.

Keynone: payload carries only sub; correlate via sessionId
Producersidentity (services/identity/internal/handler/secondary_emails.go:handleRedeemSecondaryEmail)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredsub

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.

Keynone: payload carries only sub + candidate; correlate via sessionId
Producersidentity (services/identity/internal/handler/secondary_emails.go:handleInitSecondaryEmail)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredsub 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.

Keynone: payload carries only org_locator + scheme_locator; correlate via sessionId
Producersidentity (services/identity/internal/handler/slack_connect_state.go:handleCreateSlackConnectState)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredorg_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.

Keynone: payload carries only status + members; correlate via sessionId
Producersidentity (services/identity/internal/handler/onboarding_slack_import.go:handleSlackImportCallback)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredstatus 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.

Keynone: the payload is empty, so messages are unkeyed; correlate via sessionId
Producersidentity (services/identity/internal/handler/onboarding_slack_import.go:handleSlackImportStart)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation 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.

Keythe joining email (no account or locator exists)
Producersidentity (services/identity/internal/handler/waitlist.go:handleJoinWaitlist)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredemail 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.

Keyparty locator (PTY-YYYY-NNNNNN)
Producersnotifications (services/notifications/internal/dispatch/dispatcher.go:writeLogWithSubjectBody)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["dispatch"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredchannel 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.

Keyaccount locator (ACT-YYYY-NNNNNN)
Producerspolicy-admin (services/policy-admin/internal/service/account.go:CreateAccount)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["account"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredaccountLocator 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.

Keyparty locator (PTY-YYYY-NNNNNN)
Producerspolicy-admin (services/policy-admin/internal/service/party.go:CreateParty)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["party"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredpartyLocator 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.

Keyparty locator (PTY-YYYY-NNNNNN)
Producerspolicy-admin (services/policy-admin/internal/service/party.go:UpdateParty)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["party"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredpartyLocator 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.

Keyproduct VERSION locator (PVR-YYYY-NNNNNN); the product's own locator (PRD-) rides in the payload
Producerspolicy-admin (services/policy-admin/internal/service/product.go:PublishVersion)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["product","productVersion"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredproductLocator 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.

Keycredentialing request locator (CRD-YYYY-NNNNNN)
Producersprovider (services/provider/internal/handler/credentialing.go:approve)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredcredentialingLocator 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.

Keycredentialing request locator (CRD-YYYY-NNNNNN)
Producersprovider (services/provider/internal/handler/credentialing.go:reject)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredcredentialingLocator 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.

Keycredentialing request locator (CRD-YYYY-NNNNNN)
Producersprovider (services/provider/internal/handler/credentialing.go:submit)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["credentialing","provider"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredcredentialingLocator 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.

Keyprovider locator (PRV-YYYY-NNNNNN)
Producersprovider (services/provider/internal/handler/providers.go:activate)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["provider"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredproviderLocator 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.

Keyprovider locator (PRV-YYYY-NNNNNN)
Producersprovider (services/provider/internal/handler/providers.go:create)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["provider"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredproviderLocator 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.

Keyprovider locator (PRV-YYYY-NNNNNN)
Producersprovider (services/provider/internal/handler/providers.go:deactivate)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["provider"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredproviderLocator 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.

Keyprovider locator (PRV-YYYY-NNNNNN)
Producersprovider (services/provider/internal/handler/reviews.go:addReview)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Staterequired, subjects ["review","provider"]
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredproviderLocator 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.

Keythe specialty filter string; empty when the search had no specialty filter, which leaves the Kafka key unset
Producersprovider (services/provider/internal/handler/providers.go:list)
Consumersdebugger (services/debugger/internal/consumer/consumer.go)
Statenot required
Lineagecorrelation optional · client-interactive (session expected)
Payload requiredspecialty 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.

Olly Health Insurance Platform