Invite-only betaFirst 100 SaaS founders: 3 months freeNo credit card · Zero dark patterns
BoolBul
BOOLBUL.The Retention Interceptor

Integration

Stripe cancellation flow

BoolBul adds an in-app cancel flow to your Stripe SaaS with a script tag and two server routes. It runs on your existing cancel button, shows an exit survey, matches a pause or discount offer to the reason, and keeps the cancel link visible on every screen. If BoolBul doesn't respond within 1.5 seconds it fails open to your normal cancellation. Read the quickstart or the server adapters guide for the full picture.

What you get

  • Exit survey whose answers map to Stripe's cancellation feedback values.
  • One reason-matched pause or discount offer per session.
  • Cancel link visible on every screen, one click to proceed.
  • Fails open after 1.5 seconds to your normal cancellation.

How it works with Stripe

  1. Widget opens on your cancel button; customer picks a reason and sees a matched offer.
  2. BoolBul's edge signs an offer token with your signing secret (subscription, terms, nonce, 5-min expiry).
  3. Your server verifies the token, confirms the subscription belongs to the user, and calls Stripe.

What happens in your Stripe account

ActionStripe API call
Pause N dayspause_collection with behavior: void and resumes_at — auto-resumes, access kept.
Discount X% for M monthsCreates a single-use coupon (repeating, duration_in_months: M) and adds it to discounts, keeping existing ones.
Cancelcancel_at_period_end + cancellation_details.feedback + comment (or immediate).

Add it to your app

Two pieces: the widget on your cancel button, and two routes on your server that verify the offer token and call Stripe. The server example is a Next.js route; the same calls work in any backend that runs JavaScript.

Cancel button
<script src="https://boolbul.com/v1/boolbul.js" data-public-key="bb_pub_your_key" defer></script>
<script>
  document.getElementById("cancel-subscription").addEventListener("click", () => {
    BoolBul.open({
      publicKey: "bb_pub_your_key",
      customerId: currentUser.customerId,         // your billing customer id
      subscriptionId: currentUser.subscriptionId, // sub_... from Dodo or Stripe
      mrrCents: 4900,                             // optional: powers "saved MRR"

      // Customer took an offer -> apply it on YOUR server
      onOfferAccepted: (offer) =>
        fetch("/api/boolbul/apply", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ offerJwt: offer.offerJwt }),
        }).then((res) => { if (!res.ok) throw new Error("apply failed"); }),

      // Customer chose to cancel (or BoolBul was unreachable for 1.5s)
      onCancel: ({ reasonId, comment }) =>
        fetch("/api/boolbul/cancel", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ reasonId, comment }),
        }).then(() => location.reload()),
    });
  });
</script>
Your server (Stripe)
// app/api/boolbul/apply/route.ts  (Next.js; same idea in Express, Hono, Remix...)
import { redeemOffer } from "@boolbul/sdk/server";

const gateway = { gateway: "stripe", secretKey: process.env.STRIPE_SECRET_KEY! } as const;

export async function POST(req: Request) {
  const user = await requireUser(req); // YOUR auth: never trust ids from the body
  const { offerJwt } = await req.json();

  await redeemOffer(offerJwt, {
    signingSecret: process.env.BOOLBUL_SIGNING_SECRET!, // Dashboard -> Settings
    expected: { subscriptionId: user.subscriptionId },
    // Optional replay guard: return true if this nonce was already used
    seenNonce: async (nonce) => !(await redis.set(`boolbul:${nonce}`, 1, { nx: true, ex: 300 })),
    gateway,
  });
  return Response.json({ ok: true });
}

// app/api/boolbul/cancel/route.ts
import { cancelSubscription } from "@boolbul/sdk/server";

export async function POST(req: Request) {
  const user = await requireUser(req);
  const { reasonId, comment } = await req.json();
  // Cancels at period end and records the exit reason in your gateway's churn analytics
  await cancelSubscription(user.subscriptionId, gateway, { reasonId, comment });
  return Response.json({ ok: true });
}

Your Stripe secret key stays on your server

BoolBul never receives or stores your Stripe secret key. The edge signs a short-lived JWT with your signing secret containing the subscription ID, offer terms and a nonce. Your server verifies the signature, checks the subscription belongs to the authenticated user, and only then calls Stripe. The gateway credentials never leave your infrastructure.

FAQ

Does BoolBul store my Stripe secret key?

No. Your Stripe secret key stays on your server. BoolBul signs an offer token with your signing secret; your server verifies the token and then calls Stripe directly. BoolBul never sees or stores gateway credentials.

What happens if BoolBul is slow or down?

The widget steps aside after 1.5 seconds and calls your onCancel handler with failedOpen: true, so your normal cancellation runs. No survey answer is collected in that case.

Can I customize the pause length or discount size?

You pick from preset offers in the dashboard: a 30 or 60 day pause, and 50% off for 2 months, 30% off for 3 months or 1 month free. The signed token carries exactly the offer the customer accepted, and your server applies that.

Is the exit reason saved in Stripe?

Yes, when you cancel through cancelSubscription() from @boolbul/sdk/server. Your server sends the survey answer to Stripe as cancellation_details.feedback, plus the customer's comment if they wrote one.

See the Stripe Billing Portal comparison or try the live demo.

Add a cancel flow to your Stripe SaaS

First 100 SaaS founders get BoolBul Pro free for 3 months — no credit card.