Skip to content

Instantly share code, notes, and snippets.

@AlejandroAkbal
Last active June 21, 2026 10:17
Show Gist options
  • Select an option

  • Save AlejandroAkbal/44a952ec534f203c4d8a098c30ebbc8e to your computer and use it in GitHub Desktop.

Select an option

Save AlejandroAkbal/44a952ec534f203c4d8a098c30ebbc8e to your computer and use it in GitHub Desktop.
Stripe — Bulk disable subscription renewals (cancel_at_period_end)
# Your Stripe secret key (starts with sk_live_ in production)
# Get it at: https://dashboard.stripe.com/apikeys
STRIPE_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxxxxx
import "dotenv/config";
import Stripe from "stripe";
// ─── Constants ────────────────────────────────────────────────────────────────
const RETRY_MAX = 4; // max retries on transient errors
const RETRY_BASE_MS = 500; // exponential backoff base
const MAX_SUBS_PER_PAGE = 100; // Stripe's max per page
// ─── Credential check ─────────────────────────────────────────────────────────
const SECRET_KEY = process.env.STRIPE_SECRET_KEY;
if (!SECRET_KEY) {
console.error("❌ STRIPE_SECRET_KEY is not set.");
console.error(" Create a .env file from .env.example or export it as an environment variable.");
process.exit(1);
}
if (!SECRET_KEY.startsWith("sk_live_") && !SECRET_KEY.startsWith("sk_test_")) {
console.error("❌ STRIPE_SECRET_KEY doesn't look valid (must start with sk_live_ or sk_test_).");
process.exit(1);
}
// ─── Stripe client ────────────────────────────────────────────────────────────
const stripe = new Stripe(SECRET_KEY);
// ─── Retry with exponential backoff ───────────────────────────────────────────
async function withRetry(fn, label) {
for (let attempt = 1; attempt <= RETRY_MAX; attempt++) {
try {
return await fn();
} catch (err) {
const isRecoverable =
err.statusCode === 429 ||
err.statusCode >= 500 ||
err.type === "StripeConnectionError" ||
err.code === "rate_limit";
if (!isRecoverable || attempt === RETRY_MAX) throw err;
const delay = RETRY_BASE_MS * Math.pow(2, attempt - 1) + Math.random() * 1000;
console.warn(
` ⏳ Error on ${label} (attempt ${attempt}/${RETRY_MAX}) — ` +
`retrying in ${Math.round(delay)}ms…`
);
await new Promise((r) => setTimeout(r, delay));
}
}
}
// ─── Graceful shutdown (Ctrl+C) ───────────────────────────────────────────────
let gracefulShutdown = false;
process.on("SIGINT", () => {
if (gracefulShutdown) {
console.error("\n\n⚠️ Force exiting…");
process.exit(1);
}
gracefulShutdown = true;
console.error("\n\n⏸️ Ctrl+C received — finishing current subscription and exiting…");
});
// ─── Progress bar (single line) ────────────────────────────────────────────────
function progressStr(done, total, label) {
const pct = total > 0 ? ((done / total) * 100).toFixed(1) : "0.0";
const barLen = 24;
const filled = Math.round((done / total) * barLen);
const bar = "█".repeat(filled) + "░".repeat(barLen - filled);
return ` ${bar} ${done}/${total} (${pct}%) — ${label}`;
}
// ─── Stats ────────────────────────────────────────────────────────────────────
const stats = {
total: 0,
alreadySet: 0,
updated: 0,
errors: 0,
};
// ─── Main ─────────────────────────────────────────────────────────────────────
async function main() {
console.log("");
console.log("╔══════════════════════════════════════════════════════════╗");
console.log("║ Stripe — Disable subscription renewals ║");
console.log("║ Mode: cancel_at_period_end = true ║");
console.log("╚══════════════════════════════════════════════════════════╝");
console.log("");
// ── Phase 1: Scan ──────────────────────────────────────────────────────────
console.log("🔍 Scanning subscriptions with status = active…\n");
const subs = [];
for await (const sub of stripe.subscriptions.list({
status: "active",
limit: MAX_SUBS_PER_PAGE,
})) {
subs.push({
id: sub.id,
customer: sub.customer,
cancelAtPeriodEnd: sub.cancel_at_period_end,
});
}
stats.total = subs.length;
console.log(` 📊 Total active subscriptions found: ${stats.total}\n`);
if (stats.total === 0) {
console.log("✅ No active subscriptions found. Exiting.");
return;
}
// ── Phase 2: Pre-filter ────────────────────────────────────────────────────
const pending = [];
for (const sub of subs) {
if (sub.cancelAtPeriodEnd) {
stats.alreadySet++;
} else {
pending.push(sub);
}
}
if (stats.alreadySet > 0) {
console.log(
` ⏭️ ${stats.alreadySet} already had cancel_at_period_end = true (no changes needed).`
);
}
if (pending.length === 0) {
console.log("\n✅ All subscriptions were already set not to renew.");
return;
}
console.log(
` ⚙️ ${pending.length} subscriptions need to be updated.\n`
);
console.log(" Processing (press Ctrl+C for safe interruption)…\n");
// ── Phase 3: Sequential update ─────────────────────────────────────────────
let processed = 0;
for (const sub of pending) {
if (gracefulShutdown) break;
processed++;
process.stdout.write(progressStr(processed - 1, pending.length, sub.id) + "\n");
try {
await withRetry(
() =>
stripe.subscriptions.update(sub.id, {
cancel_at_period_end: true,
}),
sub.id
);
stats.updated++;
// Erase previous line and write the completed version
process.stdout.write("\x1b[1A\x1b[2K");
process.stdout.write(progressStr(processed, pending.length, sub.id) + " ✅\n");
} catch (err) {
stats.errors++;
process.stdout.write("\x1b[1A\x1b[2K");
process.stdout.write(progressStr(processed, pending.length, sub.id) + ` ❌ ${err.message}\n`);
}
}
// ── Final summary ─────────────────────────────────────────────────────────
console.log("");
console.log("╔" + "═".repeat(56) + "╗");
console.log("║ SUMMARY" + " ".repeat(48) + "║");
console.log("╟" + "─".repeat(56) + "╢");
console.log(`║ 📦 Active subscriptions found: ${String(stats.total).padStart(5)} ║`);
console.log(`║ ⏭️ Already set not to renew: ${String(stats.alreadySet).padStart(5)} ║`);
console.log(`║ ✅ Updated now: ${String(stats.updated).padStart(5)} ║`);
if (stats.errors > 0) {
console.log(`║ ❌ Errors: ${String(stats.errors).padStart(5)} ║`);
} else {
console.log(`║ ❌ Errors: 0 ✅ No errors ║`);
}
console.log("╚" + "═".repeat(56) + "╝");
if (gracefulShutdown) {
console.log("\n⚠️ Process interrupted by user. Some subscriptions may not have been updated.");
console.log(" Run the script again to process the remaining ones (already done ones will be skipped).");
}
console.log("\n🏁 Done. No subscription was cancelled immediately.");
}
main().catch((err) => {
console.error("\n💥 Fatal error:", err.message);
process.exit(1);
});
{
"name": "stripe-cancel-subscriptions",
"version": "1.0.0",
"private": true,
"description": "Bulk disable Stripe subscription renewals (cancel_at_period_end)",
"type": "module",
"dependencies": {
"dotenv": "^16.4.7",
"stripe": "^17.6.0"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment