Grant access on subscription.active, which Polar also sends for a trial, with the status trialing, and take it away on subscription.revoked, not on subscription.canceled. Or listen to a single event, customer.state_changed, and give access while the subscription is in its list of active subscriptions. Below, each sequence in the order it arrives, from Polar's documentation and our own captures in Polar's sandbox (API version 2026-10).
A paid checkout
In our captures, the whole sequence took less than half a minute after the customer paid:
checkout.updated, with the checkoutconfirmedcustomer.createdandcustomer.state_changed, with no active subscription yet (a customer who bought before getscustomer.updatedinstead)subscription.created, alreadyactivesubscription.activesubscription.updatedorder.created,order.updatedandorder.paid, with thebilling_reasonsubscription_createcheckout.updated, with the checkoutsucceeded, andcustomer.state_changed, now listing the subscription
subscription.active is the event to grant access on. subscription.created was already active in our captures, so a handler that reads the status can grant on either; one that waits for order.paid grants last.
A trial, and a declined card
With a trial, subscription.created and subscription.active both arrive with the status trialing, and the first order is paid at zero. A handler that grants only when the status is active locks every trial out: treat trialing as access.
When the card is declined, the checkout is confirmed and then reopened. The customer still gets customer.created and a customer.state_changed with no active subscription, but no subscription or order event arrives at all.
Cancellation, and changing one's mind
| What happens | Events | Status | Access |
|---|---|---|---|
| The customer cancels, by default at the period's end | subscription.updated, subscription.canceled | active, with cancel_at_period_end | Keep it |
| The customer changes their mind | subscription.updated, subscription.uncanceled | active | Keep it |
| The period ends | subscription.updated, subscription.revoked | canceled | Take it away |
| The merchant cancels immediately | subscription.updated, subscription.canceled, subscription.revoked, at once | canceled | Take it away |
So subscription.canceled means the subscription will end, not that it has: revoking on it takes away access the customer has paid for until the period's end.
Renewals, failures, pauses and migrations
- Renewal.
subscription.cycled,subscription.updatedand a pendingorder.createdarrive first, thenorder.updatedandorder.paidonce the payment succeeds. Polar sendssubscription.cycledwhether or not the payment succeeds, so extend access onorder.paid, not onsubscription.cycledalone. - Failed payment.
subscription.past_duearrives; the customer recovers by updating the payment method. - Pause. Like a cancellation, it applies at the period's end:
subscription.updatedfirst, thensubscription.updatedandsubscription.pausedwhen it takes effect, with the statuspausedand the benefits revoked. A resume sendssubscription.updated,subscription.resumedand anorder.created: the customer is charged at once. - Migration. A subscription that Polar takes over from another provider sends
subscription.updatedandsubscription.migrated, but nosubscription.createdand nosubscription.active: a handler that waits forsubscription.activenever unlocks it.
A refund is not a cancellation
In our capture, a full refund of the first order sent refund.created, order.updated, order.refunded and two refund.updated, and the subscription stayed active until we cancelled it separately. If a refund should end access in your app, cancel the subscription along with it, or handle order.refunded yourself.
One event for all of it: customer.state_changed
Polar's customer state holds the customer, their active subscriptions, their granted benefits and their active meters. Polar sends customer.state_changed when the customer is created, updated or deleted, when a subscription is created or updated, and when a benefit is granted or revoked. In our captures, its list of active subscriptions held the trial with the status trialing, kept the subscription through a cancellation at the period's end and emptied when the subscription was revoked.
To know which of your users it is, pass your own user id as external_customer_id when you create the checkout session; it comes back as the customer's external_id. With Polar's Next.js adapter:
// app/api/webhook/polar/route.ts
import { Webhooks } from "@polar-sh/nextjs";
export const POST = Webhooks({
webhookSecret: process.env.POLAR_WEBHOOK_SECRET!,
onCustomerStateChanged: async (payload) => {
const state = payload.data;
// external_id: your own user id, passed as external_customer_id at checkout
await setAccess(state.external_id, state.active_subscriptions.length > 0);
},
});
The adapter checks the signature before it calls a handler and answers 403 when it does not verify. In @polar-sh/nextjs 1.1.0 the fields keep the JSON's snake case, as above. Polar now marks its older adapters, among them Express, Hono and Supabase, as deprecated, and recommends its SDK for those frameworks.
Let Polar grant the access itself
Some access Polar can give without any code of yours: its benefits. License keys, access to a private GitHub repository, Discord invites and roles, file downloads, feature flags, credits and a shared Slack channel are granted to active subscribers, kept for good after a one-time purchase, and revoked when a subscription ends.
How PayHook reports it
PayHook, the webhook inspector we are building, verifies Polar events with both of Polar's signing keys and follows each subscription's events as one chain. It knows which events Polar sends for a checkout, a trial and a cancellation, says whether access should be on, and names what is missing, such as a subscription.active that never arrived. PayHook is in closed beta; the home page has the details.
Sources
- Polar documentation, Webhook events, Customer State, customer.state_changed, Automated Benefits, Framework Adapters, the Next.js adapter and the Checkout API, checked on 9 October 2026.
- The sequences of a paid checkout, a trial, a declined card, a cancellation and a refund: PayHook's own captures in Polar's sandbox, 2 October 2026.
- The adapter's payload and its answer to a bad signature: PayHook's own run of
@polar-sh/nextjs1.1.0, with@polar-sh/sdk1.0.2, on those captured events, 9 October 2026.