Status: AcceptedArea: BackendDate: 2026-09-15
Context
Outgoing Polar webhooks come in two shapes: a catch-all*.updated (subscription.updated,
order.updated, …) and specialized siblings for a particular transition (subscription.canceled,
order.paid, subscription.migrated). Merchants often subscribe only to the catch-all and treat
it as “anything changed on this resource.” A specialized event without the catch-all leaves
those subscribers with no signal.
The public webhook events docs already describe
subscription.updated as a catch-all for the specialized subscription events. That contract was
implicit in code (_after_subscription_updated always fires subscription.updated first) and
easy to drop when adding a new specialized type.
Decision
When a webhook resource has a catch-all*.updated and a more specific sibling for the same
change, emit both. Never replace the catch-all with the specialized event. The specialized
event is optional precision; the catch-all is the signal that something changed.
order.updated then
order.paid via _on_order_updated). Resources with no catch-all (customer_seat.*) are out of
scope until they get one.
*.created is a birth event, not an update. Only subscriptions currently also fire
subscription.updated after create; this ADR does not require that of other resources.
Consequences
- Merchants who listen only to
*.updatedkeep seeing every mutation, including new specialized transitions such as a migrated subscription. - New specialized webhook types must call the catch-all sender as well. Do not drop an existing
*.updatedwhen adding a specialized event. - Merchants subscribed to both events receive two deliveries with the same resource payload. That duplication is the accepted cost.
- Existing specialized-only senders (
order.refunded,checkout.expired) should grow a matching*.updatedthe next time that path is touched.
Alternatives considered
- Specialized event only: fewer deliveries, but catch-all subscribers miss the change.
- Catch-all only: merchants cannot subscribe to a precise transition.
- Let each flow decide: the contract drifts, which is how
subscription.migratedalmost shipped withoutsubscription.updated.
References
- Webhook events —
subscription.updatedas catch-all; cancellation, renewal, and pause sequences always listsubscription.updatedalongside the specialized event. server/polar/subscription/service.py—_after_subscription_updatedfiressubscription.updatedbefore specialized handlers.server/polar/order/service.py—_on_order_updatedfiresorder.updatedthenorder.paid.- PR #14508 — review
that recorded this convention for
subscription.migrated.

