Skip to Content
WidgetConfiguration

Configuration

showCancelFlow accepts a configuration object that controls which Stripe subscription is targeted, how the widget behaves, what offers are available, and how it looks and reads. All fields except subscriptionId and merchantId are optional.


showCancelFlow options

FieldTypeDefaultDescription
subscriptionIdstringRequired. The Stripe subscription ID for the subscriber initiating the flow.
merchantIdstringRequired. Your Unchurn merchant ID (from your dashboard).
authTokenstringA server-signed auth token. Required for production. See Auth Tokens.
mode'test' | 'live''live'Which Stripe account to target. See Test & Live Modes.
baseUrlstring'https://app.unchurn.dev'Override the API base URL. Useful for self-hosted or proxy setups.
retriesnumber3Number of automatic retries on network failure.
retryDelaynumber500Milliseconds to wait between retries.
onComplete(outcome: FlowOutcome) => voidCalled when the flow ends with any outcome.
onError(error: Error) => voidCalled if the widget encounters a fatal error it cannot recover from.
pausePauseConfigConfigure available pause durations. See pause.allowedDurations.
supportSupportConfigConfigure the support contact experience. See Support config.
effectiveDateDate | stringDate shown to the user as “access ends on X”. See effectiveDate.
themeWidgetThemeConfigBrand colors and color scheme. See Theme.
copyWidgetCopyInputOverride any visible string. See Copy / i18n.

onComplete callback

onComplete receives a FlowOutcome object when the widget closes after a completed action. It is not called when the user simply dismisses the widget; use { type: 'abandoned' } to detect that.

outcome.typeExtra fieldsWhen it fires
'canceled'The subscription was canceled.
'paused'pauseMonths: numberThe subscription was paused. pauseMonths is the duration the user selected.
'discounted'A discount coupon was applied and the subscription was retained.
'abandoned'The user closed the widget without completing any action.
'uncanceled'A previously scheduled cancellation was reversed.
'resumed'A paused subscription was resumed.
'trial_extended'daysExtended: numberThe trial was extended. daysExtended is the number of days added.
showCancelFlow({ merchantId: 'mch_YOUR_MERCHANT_ID', subscriptionId: 'sub_STRIPE_SUB_ID', onComplete: (outcome) => { switch (outcome.type) { case 'canceled': router.push('/account/canceled') break case 'paused': console.log(`Paused for ${outcome.pauseMonths} month(s)`) break case 'trial_extended': console.log(`Trial extended by ${outcome.daysExtended} day(s)`) break case 'discounted': case 'uncanceled': case 'resumed': // subscription retained — refresh subscription state void refetchSubscription() break case 'abandoned': // user dismissed the widget break } }, })

pause.allowedDurations

Control which pause durations are offered to the user. Durations are in months.

showCancelFlow({ merchantId: 'mch_YOUR_MERCHANT_ID', subscriptionId: 'sub_STRIPE_SUB_ID', pause: { allowedDurations: [1, 3, 6], // offer 1 month, 3 months, 6 months }, })

When pause is omitted, available durations are determined by your dashboard configuration. Providing allowedDurations at call time overrides the dashboard defaults for that specific invocation.

If the pause offer is not enabled in your dashboard, this field has no effect — the pause screen will not appear regardless.


Support config

SupportConfig controls what happens when a user selects “Technical issues” as their cancellation reason.

FieldTypeDefaultDescription
urlstringLink to your support page or help center. Opens in a new tab.
labelstring'Contact support'Button label on the technical-issue screen.
descriptionstringSubtitle shown on the technical-issue screen beneath the heading.
onContact() => voidCallback fired when the user clicks the support button. Fires in addition to opening url when both are provided.
showCancelFlow({ merchantId: 'mch_YOUR_MERCHANT_ID', subscriptionId: 'sub_STRIPE_SUB_ID', support: { url: 'https://help.yourapp.com', label: 'Open Help Center', description: 'Our support team typically replies within one business day.', onContact: () => { analytics.track('support_clicked_from_cancel_flow') }, }, })

When url is provided, the widget shows a prominent contact button. When only onContact is provided without a url, the callback fires on button click but no tab is opened. When neither is configured, the technical-issue screen falls back to a describe-your-issue text input that submits feedback.


effectiveDate

effectiveDate sets the date displayed to the user as “access ends on X” — the date their subscription access actually stops if they proceed with cancellation. Pass a Date object or an ISO 8601 string.

showCancelFlow({ merchantId: 'mch_YOUR_MERCHANT_ID', subscriptionId: 'sub_STRIPE_SUB_ID', effectiveDate: user.subscriptionCurrentPeriodEnd, // Date object or ISO string })

When omitted, the widget derives the date from the subscription’s current_period_end returned by Stripe. Pass effectiveDate explicitly when your app uses a custom billing model or you want to show a grace period end date instead.


Theme

WidgetThemeConfig controls the color scheme and brand colors. All fields are optional.

FieldTypeDefaultDescription
mode'auto' | 'light' | 'dark''auto'Color scheme. 'auto' follows the OS/browser preference.
light.primarystringNear-blackPrimary accent color in light mode. Any CSS color format.
light.secondarystringSecondary accent color in light mode.
dark.primarystringNear-whitePrimary accent color in dark mode.
dark.secondarystringSecondary accent color in dark mode.

Accepted color formats: oklch(...), hsl(...), rgb(...), #hex.

When you provide only light.primary, a reasonable dark.primary is automatically derived via an OKLCH lightness shift. You do not need to specify both unless you want precise control over the dark-mode accent.

The default palette is monochrome (near-black on white in light mode, near-white on dark in dark mode).

Example: brand color

showCancelFlow({ merchantId: 'mch_YOUR_MERCHANT_ID', subscriptionId: 'sub_STRIPE_SUB_ID', theme: { mode: 'auto', light: { primary: '#6366f1', // indigo-500 }, dark: { primary: '#818cf8', // indigo-400, slightly lighter for dark backgrounds }, }, })

Copy / i18n

Every visible string in the widget can be overridden through the copy field. All fields are optional — provide only the strings you want to change. Template variables use {variableName} syntax. Unknown variables pass through unchanged.

copy: { back: 'Back', // default continue: 'Continue', // default cancelAnyway: 'Cancel anyway', noThanks: 'No thanks', done: 'Done', }

Feedback screen

Variables: none.

copy: { feedback: { title: 'Reason for canceling', subtitle: 'Your feedback helps us improve.', textareaLabel: 'Additional details (optional)', textareaPlaceholder: 'Share any additional context.', reasonLabels: { // Override individual reason labels by ID too_expensive: 'Too expensive', not_using: 'Not using it enough', missing_feature: 'Missing a feature I need', technical_issues: 'Technical issues', switching_product: 'Switching to another product', other: 'Other', }, }, }

Default cancellation reason IDs: too_expensive, not_using, missing_feature, technical_issues, switching_product, other.

Discount offer screen

Variables: {percentOff}, {durationMonths}, {durationLabel}, {discountedAmount}, {currentAmount}.

copy: { discount: { title: '{percentOff}% off for {durationMonths} {durationLabel}.', subtitle: '{discountedAmount}/mo instead of {currentAmount}...', monthLabel: 'month', wasLabel: 'was', ctaButton: 'Claim discount', }, }

Pause offer screen

Variables: {durationMonths}, {durationLabel}.

copy: { pause: { title: 'Pause your subscription.', subtitle: undefined, ctaButton: 'Pause for {durationMonths} {durationLabel}', }, }

Feedback detail screens

These screens appear for specific cancellation reasons and each accept title, subtitle, and placeholder.

copy: { missingFeature: { title: undefined, subtitle: undefined, placeholder: undefined, }, switchingProduct: { title: undefined, subtitle: undefined, placeholder: undefined, }, technicalIssue: { contactTitle: undefined, contactSubtitle: undefined, contactButton: undefined, describeTitle: undefined, describeSubtitle: undefined, describePlaceholder: undefined, }, }

Cancellation confirmation screen

Variables: {date}.

copy: { cancelConfirm: { title: 'Confirm cancellation', subtitle: 'Your subscription will end on {date}...', cancelButton: 'Cancel subscription', keepButton: 'Keep subscription', }, }

Success screens

Each outcome has a title and body. Available variables are listed per outcome.

copy: { success: { canceledTitle: undefined, // {date} canceledBody: undefined, // {date} keptTitle: undefined, keptBody: undefined, discountTitle: undefined, // {discountedAmount}, {durationMonths}, {durationLabel} discountBody: undefined, pauseTitle: undefined, // {durationMonths}, {durationLabel} pauseBody: undefined, supportTitle: undefined, supportBody: undefined, uncanceledTitle: undefined, // {date} uncanceledBody: undefined, // {date} resumedTitle: undefined, // {date} resumedBody: undefined, // {date} trialExtendedTitle: undefined, // {daysExtended} trialExtendedBody: undefined, // {daysExtended} }, }

Trial extension offer screen

Variables: {days}.

copy: { trialExtension: { title: undefined, subtitle: undefined, ctaButton: undefined, declineButton: undefined, }, }

Trial-subscriber screens

These strings are used only when the subscription has an active trial (i.e. trial_end is in the future).

Variables: {date} where noted.

copy: { trial: { feedbackSubtitle: undefined, cancelConfirmSubtitle: undefined, // {date} cancelButton: undefined, canceledTitle: undefined, canceledBody: undefined, // {date} offerUnavailableTitle: undefined, offerUnavailableBody: undefined, offerUnavailableClose: undefined, }, }

Uncancel prompt

Shown when the subscription is already scheduled to cancel at period end.

copy: { uncancelPrompt: { title: undefined, subtitle: undefined, ctaButton: undefined, declineButton: undefined, }, }

Resume prompt

Shown when the subscription is currently paused.

copy: { resumePrompt: { title: undefined, titleIndefinite: undefined, // used when the pause has no set end date subtitle: undefined, ctaButton: undefined, declineButton: undefined, }, }

Complete French-language example

The example below overrides every user-visible string for a French-language product. Pass this object to the copy field of showCancelFlow.

const frenchCopy = { back: 'Retour', continue: 'Continuer', cancelAnyway: 'Annuler quand même', noThanks: 'Non merci', done: 'Terminé', feedback: { title: 'Raison de l\'annulation', subtitle: 'Vos commentaires nous aident à améliorer notre service.', textareaLabel: 'Détails supplémentaires (facultatif)', textareaPlaceholder: 'Partagez tout contexte additionnel.', reasonLabels: { too_expensive: 'Trop cher', not_using: 'Je ne l\'utilise pas assez', missing_feature: 'Il manque une fonctionnalité', technical_issues: 'Problèmes techniques', switching_product: 'Je passe à un autre produit', other: 'Autre', }, }, discount: { title: '{percentOff} % de réduction pendant {durationMonths} {durationLabel}.', subtitle: '{discountedAmount}/mois au lieu de {currentAmount}...', monthLabel: 'mois', wasLabel: 'était', ctaButton: 'Profiter de la réduction', }, pause: { title: 'Mettre en pause votre abonnement.', subtitle: 'Reprenez quand vous êtes prêt.', ctaButton: 'Mettre en pause {durationMonths} {durationLabel}', }, missingFeature: { title: 'Dites-nous ce qui manque', subtitle: 'Nous prenons toutes les suggestions en compte.', placeholder: 'Décrivez la fonctionnalité souhaitée.', }, switchingProduct: { title: 'Vers quel produit passez-vous ?', subtitle: 'Votre retour nous aide à nous améliorer.', placeholder: 'Nom du produit ou raison.', }, technicalIssue: { contactTitle: 'Rencontrez-vous un problème technique ?', contactSubtitle: 'Notre équipe peut vous aider rapidement.', contactButton: 'Contacter le support', describeTitle: 'Décrivez le problème', describeSubtitle: 'Nous allons enquêter.', describePlaceholder: 'Décrivez le problème que vous rencontrez.', }, cancelConfirm: { title: 'Confirmer l\'annulation', subtitle: 'Votre abonnement se terminera le {date}.', cancelButton: 'Annuler l\'abonnement', keepButton: 'Conserver l\'abonnement', }, success: { canceledTitle: 'Abonnement annulé', canceledBody: 'Votre accès se termine le {date}.', keptTitle: 'Abonnement conservé', keptBody: 'Nous sommes ravis de vous garder.', discountTitle: 'Réduction appliquée', discountBody: '{discountedAmount}/mois pendant {durationMonths} {durationLabel}.', pauseTitle: 'Abonnement suspendu', pauseBody: 'Suspendu pendant {durationMonths} {durationLabel}.', supportTitle: 'Demande envoyée', supportBody: 'Notre équipe reviendra vers vous prochainement.', uncanceledTitle: 'Annulation annulée', uncanceledBody: 'Votre abonnement continue jusqu\'au {date}.', resumedTitle: 'Abonnement repris', resumedBody: 'Votre prochain renouvellement est le {date}.', trialExtendedTitle: 'Essai prolongé', trialExtendedBody: 'Votre essai a été prolongé de {daysExtended} jours.', }, trialExtension: { title: 'Prolongez votre essai', subtitle: 'Bénéficiez de {days} jours supplémentaires, gratuitement.', ctaButton: 'Prolonger l\'essai', declineButton: 'Non merci', }, trial: { feedbackSubtitle: 'Dites-nous comment améliorer votre expérience.', cancelConfirmSubtitle: 'Votre essai se terminera le {date}.', cancelButton: 'Terminer l\'essai', canceledTitle: 'Essai terminé', canceledBody: 'Votre accès se termine le {date}.', offerUnavailableTitle: 'Offre non disponible', offerUnavailableBody: 'Aucune offre de rétention n\'est disponible pour votre compte.', offerUnavailableClose: 'Fermer', }, uncancelPrompt: { title: 'Annuler la résiliation programmée ?', subtitle: 'Votre abonnement est actuellement programmé pour se terminer.', ctaButton: 'Conserver l\'abonnement', declineButton: 'Continuer la résiliation', }, resumePrompt: { title: 'Reprendre votre abonnement ?', titleIndefinite: 'Votre abonnement est en pause.', subtitle: 'Votre facturation reprendra normalement.', ctaButton: 'Reprendre l\'abonnement', declineButton: 'Rester en pause', }, } showCancelFlow({ merchantId: 'mch_YOUR_MERCHANT_ID', subscriptionId: 'sub_STRIPE_SUB_ID', copy: frenchCopy, })
Last updated on