Churn Offer Copy Customization
Rewrite every piece of copy in the cancellation offer modal and the dashboard active-offer badge from your theme, using window.SubscribfyOffers, DOM events, and CSS hooks.
Rewrite any text in the cancellation (churn) offer modal and the dashboard "active offer" badge from your own theme, without touching Subscribfy's code.
This is a developer reference for shop themes. To create the offers themselves, see Churn Offers Configuration. To drive offers programmatically, see the Churn API.
Three Ways to Customize
Use these in order of preference: reach for the simplest one that does the job.
1. window.SubscribfyOffers
Static copy overrides. The everyday case for rewording titles, descriptions, and buttons.
2. DOM Events
Dynamic copy, analytics, pixels, or custom UI driven by live offer data.
3. CSS Hooks
Styling, or flicker-free badge copy with no JavaScript timing.
Define it early
Set window.SubscribfyOffers before Subscribfy's scripts run by placing it high in your theme's <head>. For the dashboard badge, this also guarantees no text flicker.
Good to know
- Every field is optional. Omit anything to keep the Subscribfy default copy.
%placeholders%are filled with the real offer values at render time.- Use
%%for a literal percent sign, e.g.Save %discount_value%%renders asSave 20%. - HTML is allowed in any copy field, e.g.
modal_title: 'Your Credit Just <span>Got Bigger</span>'.
1. window.SubscribfyOffers
The object has three independent surfaces. Each is optional, and within each every field is optional.
churn
The anti-churn modal shown to an active member who is trying to cancel. Keyed by cancellation reason, then by offer type.
applied
The active-offer modal shown when the member ALREADY has an offer (a "cancel again?" confirmation). Keyed by offer type.
applied_banner
The dashboard "active offer" badge. Keyed by offer type.
Offer types are always one of: discount_price, change_frequency, add_store_credits.
Full example
window.SubscribfyOffers = {
// Anti-churn modal (active member trying to cancel).
// Copy is nested PER cancellation reason alias, then PER offer type.
churn: {
reasons: {
// key = cancellation reason alias (as shown in the admin)
enough_items: {
modal_title: 'Too much to keep up with?',
modal_subtitle: 'Pause or skip a delivery instead of cancelling.',
offers: {
discount_price: {
title: 'Stay and save %discount_value%%',
description: 'Applied to your next order automatically.' // hidden unless set
},
change_frequency: {
title: "Change how often you're billed",
description: 'Switch to every %interval_count% %interval_name%.',
duration: 'Applies for %duration_value% %duration_unit%, then reverts to your original schedule.'
},
add_store_credits: {
title: "Here's %currency_code% %store_credits% in store credit",
description: 'Added to your account if you stay.',
duration: 'One-time bonus.'
}
}
},
too_expensive: {
modal_title: "Let's find a price that works",
offers: {
discount_price: { title: "Here's %discount_value%% off to keep you going" }
}
}
},
// Button labels (the "see next offer" button is intentionally NOT customizable)
buttons: { apply: 'KEEP MY OFFER', cancel: 'CANCEL ANYWAY', keep: 'KEEP OFFER' }
},
// Active-offer modal: the member already has an offer. Keyed by TYPE.
// The header lives PER type, since the modal shows the one offer they already have.
// pre_modal_title is a small, centered eyebrow above the title (defaults to "Before you cancel").
applied: {
offers: {
discount_price: {
pre_modal_title: 'Before you cancel',
modal_title: 'You have an active discount',
modal_subtitle: 'Cancelling now means losing it. Are you sure?',
title: 'Discount the price of the subscription',
description: 'Discount Subscription Price',
duration: 'Applies for %duration_value% %duration_unit%, then reverts to your original price.',
note: ''
},
change_frequency: {
pre_modal_title: 'Before you cancel',
modal_title: 'Your billing schedule is set',
modal_subtitle: 'Cancelling now means losing it. Are you sure?',
title: 'Change how often the subscription is billed',
description: 'Change Subscription Frequency',
duration: 'Applies for %duration_value% %duration_unit%, then reverts to your original billing schedule.',
note: ''
},
add_store_credits: {
pre_modal_title: 'Before you cancel',
modal_title: 'You have store credit active',
modal_subtitle: 'Cancelling now means losing it. Are you sure?',
title: 'Store credit added to your account',
description: '',
duration: 'One-time bonus',
note: ''
}
}
},
// Dashboard "active offer" badge. Keyed by TYPE.
applied_banner: {
offers: {
discount_price: { title: 'DISCOUNT ACTIVE', description: 'Your %discount_value%% discount is live' },
change_frequency: { title: 'FREQUENCY UPDATED', description: 'Now billed every %interval_count% %interval_name%' },
add_store_credits: { title: 'CREDIT ADDED', description: '%currency_code% %store_credits% in store credit added' }
}
}
};Field reference
Nest copy under churn.reasons.{alias}, where the alias matches the cancellation reason as shown in the admin. See Cancellation Reasons to manage that list.
| Field | Level | Description |
|---|---|---|
modal_title | per reason | Modal header. |
modal_subtitle | per reason | Modal subtitle. |
offers.{type}.title | per offer type | Offer title. |
offers.{type}.description | per offer type | Offer description (hidden unless set). |
offers.{type}.duration | per offer type | How long the offer lasts (where applicable). |
buttons.apply / buttons.cancel / buttons.keep | global | Button labels. |
The "see next offer" button is intentionally not customizable.
Shown when the member already has an active offer. Independent from churn.reasons, and keyed by offer type. The header lives per type because the modal shows the single offer they already hold.
| Field | Description |
|---|---|
pre_modal_title | Small, centered eyebrow above the title. Applied modal only; defaults to "Before you cancel". |
modal_title | Modal header. |
modal_subtitle | Modal subtitle. |
title | Offer title. |
description | Offer description. |
duration | How long the offer lasts (where applicable). |
note | Optional fine print below the offer. |
2. Placeholders
Use these inside any title, description, duration, or note. Unknown or empty placeholders render as nothing.
| Placeholder | Applies to | Description | Example |
|---|---|---|---|
%discount_value% | discount_price | Discount amount | 20 |
%discount_type% | discount_price | percentage or fixed | percentage |
%interval_count% | change_frequency | Billing interval count | 2 |
%interval_name% | change_frequency | Interval unit (pluralized) | months |
%store_credits% | add_store_credits | Credit amount | 15 |
%currency_code% | add_store_credits | Subscription currency | USD |
%duration_value% | any (when set) | How long the offer lasts | 3 |
%duration_unit% | any (when set) | billing cycles or months | months |
%offer_name% | any | The offer's name | |
%reason% | churn modal only | The cancellation reason |
%% renders a literal %, so Save %discount_value%% becomes Save 20%.
3. Events
All events are CustomEvents dispatched on document. Every detail carries this base context:
{ flow, reason, reason_title, scid, cid, offer?, index?, total? }
// where offer = { id, type, name, value, value_type, duration_type, duration_value }| Event | Fires when | Extra detail |
|---|---|---|
sbf:offer:opened | Modal becomes visible | { applied, offersCount } |
sbf:offer:viewed | An offer is shown | { offer, index, total } |
sbf:offer:apply_clicked | Member clicks apply/keep | { offer } |
sbf:offer:applied | Activation succeeded | { offer } |
sbf:offer:apply_failed | Activation failed | { offer, error } |
sbf:offer:cancelled | Cancellation completed | { reason } |
sbf:offer:closed | Modal closed | { via } — backdrop / close / applied / cancelled / dismiss |
Example: send to analytics
document.addEventListener('sbf:offer:viewed', function (e) {
gtag && gtag('event', 'retention_offer_viewed', {
offer_type: e.detail.offer.type,
value: e.detail.offer.value
});
});Advanced: rewrite copy from live data
The sbf:offer:render event fires before paint, so it lets you rewrite copy from live offer data. The handler must be synchronous.
document.addEventListener('sbf:offer:render', function (e) {
if (e.detail.flow !== 'churn') return;
// e.detail = { flow, reason, applied, state, offers, currentOffer, el }
});4. CSS Hooks
Every surface and offer carries scoped classes and data-* attributes you can target for styling.
| Class | Where | Meaning |
|---|---|---|
.sbf-offer-flow--churn / .sbf-offer-flow--applied_banner | all | Which surface is rendering. |
.sbf-offer-reason--{alias} | churn modal | The cancellation reason alias. |
.sbf-offer-state--applied / .sbf-offer-state--offers | churn modal | Whether an offer is applied or being offered. |
.sbf-offer-type--{type} | all | discount_price / change_frequency / add_store_credits. |
.sbf-offer-id--{id} | all | The specific offer ID. |
.sbf-offer-button--apply / --cancel / --keep | modal | The action buttons. |
Flicker-free badge copy (CSS only)
Replace badge text with no JavaScript timing by hiding the original and injecting your own:
.sbf-offer-flow--applied_banner.sbf-offer-type--discount_price .exm_ps_offer_description {
font-size: 0;
}
.sbf-offer-flow--applied_banner.sbf-offer-type--discount_price .exm_ps_offer_description::after {
content: 'Your discount is live';
font-size: 14px;
}Troubleshooting
Related
Churn Offers Configuration
Create the retention offers shown in the modal.
Cancellation Reasons
Manage the reason aliases used as churn keys.
Churn API
Retrieve and activate offers programmatically.
Custom Pixel
Track storefront interactions for analytics.
Was this page helpful?