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
- Widget opens on your cancel button; customer picks a reason and sees a matched offer.
- BoolBul's edge signs an offer token with your signing secret (subscription, terms, nonce, 5-min expiry).
- Your server verifies the token, confirms the subscription belongs to the user, and calls Stripe.
What happens in your Stripe account
| Action | Stripe API call |
|---|---|
| Pause N days | pause_collection with behavior: void and resumes_at — auto-resumes, access kept. |
| Discount X% for M months | Creates a single-use coupon (repeating, duration_in_months: M) and adds it to discounts, keeping existing ones. |
| Cancel | cancel_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.
<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>// 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.