Webhook handler for events arriving on the :connect endpoint.
Stripe Connect platforms receive two distinct webhook streams:
- Platform events (
/webhooks/stripe) — account-scoped events targeting the platform account itself; dispatched toAccrue.Webhook.DefaultHandler. - Connect events (
/webhooks/stripe/connect) — events relayed by Stripe from a connected account (account.updated,account.application.{authorized,deauthorized},capability.updated,payout.*,person.*); dispatched to this module.
The Accrue.Webhook.DispatchWorker reads row.endpoint from the
persisted accrue_webhook_events row and selects the handler module at
dispatch time. This module implements the Connect-specific reducers.
Reducer shape
Every reducer runs inside Accrue.Repo.transact/1 with an
Accrue.Events.record/1 call appended so the state mutation and audit
row commit atomically. For events whose payload is needed
beyond the %Accrue.Webhook.Event{} struct's object_id, the raw
accrue_webhook_events.data jsonb row is refetched via
ctx.webhook_event_id — the handler never trusts the lean Event
struct alone for anything beyond routing.
Out-of-order delivery (Pitfall 3)
Stripe does not guarantee webhook delivery order. An account.updated
for a just-created account can arrive before the local
accrue_connect_accounts row has been inserted. The reducer handles
this by calling Accrue.Connect.retrieve_account/2, which runs the
canonical processor round-trip and upserts the local row via
Accrue.Connect.Account.force_status_changeset/2 — so a missing row
is seeded from the latest Stripe state in the same transaction that
would otherwise have failed. Later, in-order events arriving on a
now-settled row update it the same way (idempotent).
The reducer uses "refetch canonical" rather than "compare timestamps" for stale-event detection: because the Stripe round-trip always returns the current account state, a stale webhook replay overwrites the row with the same values it already has — a functional no-op — rather than clobbering later events with older snapshot data (Pitfall 3).
Deauthorization tombstoning
account.application.deauthorized NEVER hard-deletes the local row.
It stamps deauthorized_at via force_status_changeset/2 so the row
survives for audit, and emits an ops telemetry event
[:accrue, :ops, :connect_account_deauthorized] via
Accrue.Telemetry.Ops.
Scope
Reducers for person.* log a debug line and no-op — Custom-account
person KYC is deferred to v1.x.