Retention offers
Offer a discount to subscribers who set out to cancel, find out why they are leaving, and see how much revenue the offer keeps.
A subscriber clicking Cancel is the last chance to keep them. A retention offer turns that click into a short flow:
- Why are you leaving? An optional survey: one of seven reasons, plus a comment.
- Before you go. When the environment makes an offer and the subscriber qualifies, they are offered a discount, say 50% off their next three invoices.
- Stay or leave. Accepting applies the discount and the subscription carries on. Declining cancels at the end of the period, as before.
Every attempt is recorded, whichever way it went, so the dashboard can show how many people tried to leave, why, how many stayed, and the monthly revenue they represent.
The hosted billing portal and <SubscriptionManager /> run this flow without
any code from you. To build your own cancel screen, use the two-step API below.
Set up the offer
The offer is one of your existing coupons (Product → Coupons). It is not a separate
kind of discount, so it is rated, counted against the coupon's
maxRedemptions and printed on invoices exactly like any other subscription
discount.
In the dashboard, open Settings → Retention offers, choose a coupon and turn on Make an offer. Or from your server:
await base316.retention.update({
enabled: true,
couponId: "cpn_...",
// Optional: replace the default wording, which states the offer's terms.
headline: "Before you go",
message: "Stay on your plan and get 50% off your next 3 invoices.",
});Making an offer needs the Pro plan or above. The survey and the report work on every plan, so a workspace that upgrades already has its history.
Who is offered what
A subscriber is offered the discount only when all of these hold:
- the offer is on, and its coupon is active, unexpired, not used up and, for a fixed amount, in the subscription's currency;
- the subscription is not free;
- the subscription has no discount already. The offer never replaces one, because doing that could quietly swap a better deal for a worse one;
- the customer has not accepted an offer in the last
cooldownDays(365 by default). Without this limit, Cancel becomes the way to get a discount.
When no offer is made, the attempt records why (ineligibleReason) for your
reporting. Never show that reason to the subscriber; they see no offer, and
cancelling works as it always has.
An offer can be accepted for 24 hours after it is shown. A tab left open for a week must not redeem a coupon that has since been withdrawn.
Leaving is never blocked. The offer is shown, never imposed: confirming works whether an offer was made, expired or ignored, and "No thanks, cancel" sits beside "Accept offer and stay" rather than behind it.
Build your own cancel screen
In the browser, for a signed-in user
// 1. Start: records the reason, cancels nothing.
const attempt = await base316.billing.startCancellation(token, {
reason: "TOO_EXPENSIVE", // optional
comment: "Over budget this quarter", // optional
});
if (attempt.offer) {
// 2. Show attempt.offer: its type, percentOff or amountOff, duration and
// durationInPeriods, plus your headline and message.
// The coupon's code is never included.
await base316.billing.acceptRetentionOffer(token, attempt.id); // stays
// …or
await base316.billing.confirmCancellation(token, attempt.id); // leaves
} else {
await base316.billing.confirmCancellation(token, attempt.id);
}From your server
For an integration with its own sign-in, the same three steps take a secret key and a subscription id:
const attempt = await base316.retention.start(subscriptionId, { reason: "NOT_USING" });
await base316.retention.accept(attempt.id); // or retention.confirm(attempt.id)Every language SDK has the same methods, for example start_cancellation,
accept_retention_offer and confirm_cancellation in Python and Ruby.
The reasons are TOO_EXPENSIVE, NOT_USING, MISSING_FEATURES,
SWITCHING, TECHNICAL_ISSUES, TEMPORARY and OTHER.
GET /v1/retention lists them in display order.
| Call | Errors |
|---|---|
POST /v1/me/subscription/cancellation | 404 no_subscription; 403 billed_by_parent |
POST /v1/me/subscription/cancellation/:id/accept | 409 offer_expired, offer_unavailable (the coupon or plan changed since), already_resolved, no_offer; 404 |
POST /v1/me/subscription/cancellation/:id/confirm | 409 already_resolved (they accepted the offer); confirming twice answers as the first time did |
Starting again while an attempt is open returns that same attempt, so a double click or the back button does not count as two people leaving.
The one-step POST /v1/me/subscription/cancel still works and still cancels
straight away. It makes no offer, but it is recorded, so the report counts
everyone who left.
Accepting after a cancellation was already scheduled
A subscriber whose subscription is already set to end, and who then goes through
the flow and accepts, is un-cancelled: the discount goes on and the
subscription renews. The webhook says so with resumed: true.
Webhooks
| Event | change | Carries |
|---|---|---|
subscription.updated | retention_offer_accepted | cancellationId, cancellationReason, couponId, discountId, resumed |
subscription.updated | cancel_scheduled | cancellationId and cancellationReason when it came through a cancel flow |
The report
Insights → Retention shows, for the last 7, 30, 90 or 365 days:
- how many subscribers started to cancel, how many were saved, and the save rate, which is saves divided by offers made;
- the monthly revenue kept: the recurring value of the saved subscriptions, per currency, before the discount;
- the breakdown by reason;
- the most recent attempts, with what was offered and what the subscriber chose. An attempt nobody finished, whose offer has lapsed, shows as Abandoned; that subscriber is still subscribed.
The same figures come from GET /v1/retention/report (base316.retention.report())
and the attempts from GET /v1/cancellations.
Privacy
The comment is in the subscriber's own words. When a person exercises their right to erasure, the comment is cleared wherever their billing account's name and address are stripped. The attempt itself stays, as a billing record.
Reseller mode
Let the firms on your product charge their own clients on their own Stripe account, while you keep billing each firm wholesale on yours.
SAML identity provider
Signing your users in to applications that only speak SAML — a helpdesk, an LMS, a vendor portal — with the account they already have in Base316.

