Skip to main content

Widget Customization

Customize the look, feel, and behavior of the Windback cancellation widget to match your brand.

Themes

The widget ships with two built-in themes.

Light Theme

Clean white background with dark text. Best for light-themed apps.
<script
  src="https://api.windbackai.com/widget.js"
  data-api-key="pub_your_key"
  data-theme="light"
  async
></script>

Dark Theme

Dark background with light text. Best for dark-themed apps.
<script
  src="https://api.windbackai.com/widget.js"
  data-api-key="pub_your_key"
  data-theme="dark"
  async
></script>

Custom Cancel Reasons

By default, the widget shows a standard set of cancel reasons. You can customize these in Settings > Cancel Flow in your dashboard, or pass them programmatically:
window.Windback.show({
  customerEmail: "jane@example.com",
  reasons: [
    { id: "too_expensive", label: "Too expensive" },
    { id: "missing_features", label: "Missing features I need" },
    { id: "switching", label: "Switching to a competitor" },
    { id: "not_using", label: "Not using it enough" },
    { id: "temporary", label: "Just need a break" },
    { id: "other", label: "Other", allowFreeText: true },
  ],
});
When allowFreeText is true, the widget shows a text area for the customer to elaborate. This free-text feedback is sent to Windback and used by the AI to personalize recovery emails.

Event Callbacks

Register callbacks to respond to widget events in your application:
window.Windback.show({
  customerEmail: "jane@example.com",
  onSubmit: (data) => {
    console.log("Cancel reason:", data.reason);
    console.log("Feedback:", data.feedbackText);
    console.log("Accepted offer:", data.selectedOffer);

    // Proceed with your cancellation logic
    cancelSubscription(data);
  },
  onDismiss: () => {
    console.log("Customer dismissed the widget");
    // Customer chose not to cancel --- no action needed
  },
});

Callback Reference

Called when the customer submits their cancel reason. The data object contains:
reason
string
The selected cancel reason ID (e.g., too_expensive).
reasonLabel
string
The human-readable cancel reason label.
feedbackText
string
Free-text feedback if provided. Empty string if not.
selectedOffer
string | null
The retention offer the customer accepted, or null if they declined all offers.
customerEmail
string
The customer’s email address.
Called when the customer closes the widget without submitting a cancel reason. No data is passed. Use this to keep the customer on their current plan.

Required Fields

The widget requires certain data to function correctly:
FieldRequiredPassed ViaNotes
data-api-keyYesScript attributeYour public key (pub_)
customerEmailYesWindback.show()Needed to create the churn event
customerNameNoWindback.show()Improves personalization
planNameNoWindback.show()Shown in retention offers
mrrNoWindback.show()Used for revenue impact tracking
If customerEmail is not provided when calling show(), the widget will display an email input field. For the best experience, always pass the customer’s email programmatically.

Full Example

<!-- Add the widget script -->
<script
  src="https://api.windbackai.com/widget.js"
  data-api-key="pub_your_key"
  data-theme="dark"
  data-position="center"
  async
></script>

<script>
  document.getElementById("cancel-btn").addEventListener("click", () => {
    window.Windback.show({
      customerEmail: currentUser.email,
      customerName: currentUser.name,
      planName: currentUser.plan,
      mrr: currentUser.mrr,
      reasons: [
        { id: "too_expensive", label: "Too expensive" },
        { id: "missing_features", label: "Missing features" },
        { id: "not_using", label: "Not using it enough" },
        { id: "other", label: "Other", allowFreeText: true },
      ],
      onSubmit: (data) => {
        // Your cancellation logic
        fetch("/api/cancel", {
          method: "POST",
          body: JSON.stringify({ reason: data.reason }),
        });
      },
      onDismiss: () => {
        // Customer stayed --- nothing to do
      },
    });
  });
</script>