The portal cancel flow
Cancel self-service lets customers end a subscription from the portal without contacting support. Recurso runs a cancel flow — a sequence of steps (survey → retention offer → confirmation) designed to reduce churn while respecting the customer’s intent. The flow you build with the Cancel Flows API is what the customer walks through.1. Authenticate the customer
Cancellation happens inside a portal session — get the customer signed in with a magic link first:2. Build the flow (steps)
A cancel flow is a name plus ordered steps. Each step has astep_type and a free-form config; cooldown_days sets how long a saved
customer waits before re-entering the flow. Survey, offer, and confirmation
are simply three step types:
config is stored as-is and rendered by your portal UI, so the exact keys
(reasons, type, coupon_code, behavior, …) are your convention —
these are conventions the reference portal uses, not enforced schema.
Offer types you’ll commonly model: discount (a coupon), pause,
downgrade.PUT /v1/cancel-flows/{id}, POST /v1/cancel-flows/{id}/steps,
PUT|DELETE /v1/cancel-flows/steps/{id}.
3. Run a cancel session
When the customer clicks “Cancel”, start a session and advance it step by step. The session carries the state your portal reads:
Aggregate outcomes (saves vs. cancels) come from
GET /v1/cancel-flows/stats.
Confirmation behavior
The confirmation step’sbehavior decides timing:
Which actions the portal exposes
Whether the cancel button appears, and which flow it uses, is a dashboard setting (Settings → Portal) — there is no/v1/settings/portal endpoint.
Gift redemption
The portal also redeems gift codes. Customers do this in the portal UI (POST /portal/api/redeem); the merchant-side equivalent is:
Events
Recurso emitssubscription.canceled when a cancellation takes effect
(and subscription.renewed if a saved customer’s billing continues). The
per-step cancel-session state (survey answer, offer outcome, saved_by_offer)
lives on the session object above and in GET /v1/cancel-flows/stats —
not as separate webhook events.