How the cancel flow works
A cancel session moves through five phases. Knowing them helps you reason about edge cases, debug failures, and decide what to log on your side. The whole flow is synchronous from the customer’s point of view — they click cancel, work through the widget, and see the confirmation screen before any cross-system messages settle.
Phase 1 — Token mint
Your server is the only place that knows which Stripe subscription belongs to the signed-in customer. When the customer clicks cancel, your server signs a short-lived token saying this customer is allowed to open the cancel flow for this subscription. The token carries the merchant ID, the subscription ID, the mode (test or live), and a 600-second TTL.
Anyone who captures a token can replay it until it expires. Keep token lifetimes short, and rotate your Unchurn secret from the dashboard if a token leaks.
Phase 2 — Session create
The widget sends the token to Unchurn’s backend. Unchurn does three things, in order:
- Verify the token — confirms the signature is valid, the merchant ID matches, and the expiry hasn’t passed.
- Read the subscription from Stripe — through your Stripe Connect link, in the connected account, in the right mode.
- Evaluate each offer — for the live subscription, decide which offers fit and which don’t. A subscription that can be cancelled may still be a poor fit for pause if it bills yearly. See Eligibility for the rules.
The session is recorded in your dashboard regardless of which offers fit — even if none of them do.
If the subscription is in a shape Stripe can’t safely auto-cancel, the session opens in manual mode instead. The cancel button records a manual cancellation request and the customer sees a confirmation that you’ll follow up; we don’t pretend Stripe was changed.
Phase 3 — Offer waterfall
The widget routes the customer to an offer based on the reason they picked in the feedback step. Each reason in your dashboard maps to one offer — “too expensive” might route to discount, “taking a break” to pause, and so on. If the routed offer isn’t eligible for this subscription (e.g. pause on a yearly plan), or if the customer declines it, the widget shows one backup offer when eligibility allows before going to the cancel confirmation.
Every decline and every acceptance is recorded. Your dashboard shows the full path the customer took, not just where they ended up — that’s how you spot which reasons need a different offer or which offer is being declined too often.
Phase 4 — Stripe mutation
When the customer accepts an offer or confirms cancellation, Unchurn calls Stripe. Never the browser, never your server. The call is safe to retry — if a network blip causes the response to be lost, the next attempt sees the change already applied and treats it as a successful repeat instead of double-applying.
Unchurn reads the subscription back from Stripe before marking the session complete, so the dashboard reflects what Stripe actually did, not just what was requested. If Stripe rejects the change for any reason, the session is left in a clear error state so it can be replayed by hand. The exact Stripe call per offer is documented in The five offers.
Phase 5 — Confirmation
The customer sees a confirmation screen tailored to the outcome — discount applied, pause scheduled, cancel scheduled for a specific date. The widget renders it directly with no round-trip to your server.
Retention outcomes (offer accepted, cancel scheduled) land in your dashboard. When a session goes to manual cancellation, the request appears in your dashboard with the subscription ID, timestamp, and reason.
When the subscription is already paused
A customer can click your cancel button while their subscription is mid-pause. The engine catches that during Phase 2 and routes the widget to a different screen.
After reading the subscription, the engine checks for an active pause it set itself. The check confirms pause_collection.behavior is void and pause_collection.resumes_at matches the recorded pause for that session within a 60-second tolerance. When both hold and the resume date is still in the future, the widget opens directly on a Resume prompt instead of the feedback screen.
The Resume prompt shows the scheduled resume date and a primary CTA to resume billing. Accepting calls subscriptions.update(subscriptionId, { pause_collection: null }) on Stripe, verifies the pause is cleared by reading the subscription back, and records the session with outcome resumed. Declining closes the widget and the pause continues to its scheduled resume date.
The persistent Cancel button stays visible on the Resume prompt. A paused customer who has fully decided can still cancel from here. Because pause_collection.behavior is void, Stripe lets the cancel schedule for end of period without first lifting the pause, and the pause itself is preserved on the subscription.
This branch only fires for pauses Unchurn set. Pauses created directly in the Stripe dashboard, even with the same behavior: 'void', don’t open the Resume prompt because they aren’t tied to a recorded Unchurn session.
What does not happen
- The browser never talks to Stripe directly. Every Stripe call comes from Unchurn’s backend through your Stripe Connect link.
- Your server never talks to Stripe in the cancel flow. Your server signs one token and that’s it; Unchurn handles the rest.
- No customer can start a flow for a subscription they don’t own. The merchant ID and subscription ID are signed into the token and checked on Unchurn before any Stripe read.
Failure modes
| When | What happens |
|---|---|
| Token expired | The widget fetches a fresh token from your server and retries automatically. |
| Token signature invalid | Unchurn returns a 401, the widget shows a generic error, and no Stripe call is made. |
| Subscription unsafe for automated cancel | The session opens in manual mode; the cancel button records a request and notifies you. |
| Stripe rejects the change | The session is flagged as failed in your dashboard with the Stripe error visible. |
| Customer closes the widget mid-flow | The session is marked abandoned and shown in your dashboard. |