Stripe's changelog states it in one line: "The stripe.redirectToCheckout method is no longer supported in Stripe.js." The removal covers "both client-only and client and server" integrations, which is why it reaches two very different groups. Server-backed apps that passed a sessionId need a two-line change. Static sites and apps with no backend, which passed line items straight from the browser, lost the ability to build a Checkout cart in browser code.
Which versions are affected
Stripe.js now ships named versions twice a year, and each major version of @stripe/stripe-js loads one of them. The removal follows the Stripe.js version your page loads, not your account's API version:
| How your page loads Stripe.js | Stripe.js version | redirectToCheckout |
|---|
| js.stripe.com/v3, or @stripe/stripe-js below 6 | v3 | Stripe.js does not block the call |
| @stripe/stripe-js 6, or js.stripe.com/acacia | acacia | Stripe.js does not block the call |
| @stripe/stripe-js 7, or js.stripe.com/basil | basil | Stripe.js does not block the call |
| @stripe/stripe-js 8, or js.stripe.com/clover | clover | Throws an IntegrationError; the TypeScript types no longer declare the method |
| @stripe/stripe-js 9, or js.stripe.com/dahlia | dahlia | Throws an IntegrationError |
| @stripe/stripe-js 10 (the current npm latest), or js.stripe.com/endive | endive | Throws an IntegrationError |
A plain npm install @stripe/stripe-js installs version 10 today, so a new project, a fresh lockfile, or generated code that follows a tutorial written before October 2025 lands on a version where the call throws.
The four errors, and what each means
Only the first is the removal itself. The third, is not a function, has a different cause, and upgrading Stripe.js will not fix it.
The fix with a server: redirect to session.url
This is the code most integrations have:
// Worked before clover. Throws on @stripe/stripe-js 8 and later.
const stripe = await loadStripe("pk_test_...");
const { id } = await fetch("/create-checkout-session", { method: "POST" })
.then((r) => r.json());
await stripe.redirectToCheckout({ sessionId: id });
The server already creates the session. Change what it returns, from the ID to the URL, as Stripe's changelog directs: "use standard redirect functions (such as window.location.href = session.url)".
// server.js (Express). The secret key never leaves the server.
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);
app.post("/create-checkout-session", async (req, res) => {
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items: [{ price: "price_...", quantity: 1 }],
success_url: "https://example.com/thanks?session_id={CHECKOUT_SESSION_ID}",
cancel_url: "https://example.com/cart",
});
// Return the URL, not just the ID.
res.json({ url: session.url });
});
// Browser. No Stripe object needed for the redirect itself.
const { url } = await fetch("/create-checkout-session", { method: "POST" })
.then((r) => r.json());
window.location.assign(url);
Or skip client JavaScript with a 303
Stripe's own Checkout quickstart posts a plain form and redirects from the server, so the button works with JavaScript disabled:
<!-- No client JavaScript at all: the browser follows the 303. -->
<form action="/create-checkout-session" method="POST">
<button type="submit">Checkout</button>
</form>
app.post("/create-checkout-session", async (req, res) => {
const session = await stripe.checkout.sessions.create({ /* same as above */ });
res.redirect(303, session.url);
});
What goes wrong in the migration
- Calling the 303 endpoint with fetch. fetch follows the redirect itself and hands your code the Checkout page as a response; the browser tab never navigates, so the buyer sees nothing happen. Use the form with the 303, or fetch with the JSON response, not fetch with the 303.
- Building the URL from the ID. Use
url exactly as returned. Stripe's documented example carries more than the session ID, and with a custom domain the URL uses your subdomain instead of checkout.stripe.com. - A null url.
url applies to hosted sessions, the default ui_mode, and "is only present when the session is active." Embedded sessions never have one, and a session that has completed or expired no longer does, so create the session when the buyer clicks rather than when the page loads. - Dropping Stripe.js from your pages. The redirect no longer needs it, but the stripe-js README recommends loading Stripe.js on every page so Stripe's fraud detection sees how buyers browse. Keep loading it; just stop calling the removed method.
Pinning an older Stripe.js
If you need the old call working while you migrate, pin a version that still has it:
# Stopgap only: keeps Stripe.js on basil, which does not block the call.
npm install @stripe/stripe-js@7
# Or, with a script tag:
# <script src="https://js.stripe.com/basil/stripe.js"></script>
Stripe's versioning policy says "We continue to support and update older versions," and that v3 stays supported "for the foreseeable future." No end date has been published. The costs: TypeScript types are not backported to old @stripe/stripe-js majors, new features arrive in newer versions, and Stripe recommends keeping Stripe.js and your server's API version on the same release train. A pin buys time; it is not a destination.
The fix with no backend: Payment Links
The client-only integration called redirectToCheckout with Price IDs, a mode, and a success URL from the browser, using only the publishable key. Stripe's instruction for it is to "migrate to Payment Links and Buy Buttons." Create the link in the Dashboard, then use it as a plain link:
<!-- A Payment Link from the Dashboard, used as a plain link.
client_reference_id comes back on checkout.session.completed. -->
<a href="https://buy.stripe.com/test_...?client_reference_id=cart_1842">
Buy now
</a>
Or click Buy button on the link in the Dashboard and paste the embed code it generates:
<script async src="https://js.stripe.com/v3/buy-button.js"></script>
<stripe-buy-button
buy-button-id="buy_btn_..."
publishable-key="pk_test_..."
></stripe-buy-button>
The Buy Button needs a website domain to render, so test it from a local HTTP server rather than a file opened from disk.
What changes when you move to links
- The cart is fixed when you create the link. A Payment Link carries up to 20 line items, optional items included, chosen when it is created. Buyers can adjust quantities you allow, but browser code can no longer assemble an arbitrary cart. Selling any combination of products means one link per offer, or a server.
- Per-buyer data is a URL parameter.
client_reference_id accepts up to 200 characters of letters, digits, dashes, and underscores, and comes back on checkout.session.completed. Invalid values are silently dropped. UTM codes are passed through to your redirect URL when the link redirects after payment. Do not put anything secret in a link you share. - Fulfillment still needs somewhere to run. With no server, payments appear in the Dashboard, and you can turn on an email for every successful payment. Automated fulfillment means receiving
checkout.session.completed, which is a server again. The webhook events reference covers which event to trust.
In exchange, prices are set in the Dashboard, so code in your page cannot change what a buyer is charged beyond options you enable on the link, such as promotion codes, and the only key in your page is a publishable one.
Questions, answered
Why is stripe.redirectToCheckout not working anymore?
Stripe removed it from Stripe.js in the clover release, dated 2025-09-30. Any page that loads @stripe/stripe-js 8 or later, or a clover, dahlia, or endive script URL, gets an IntegrationError instead of a redirect. Redirect the browser to the Checkout Session's url instead.
What replaces redirectToCheckout?
For integrations with a server, create the Checkout Session on the server and send the browser to session.url, either with window.location or a 303 redirect from the server. For client-only integrations with no server, Stripe's replacement is a Payment Link, optionally embedded as a Buy Button.
Can I keep using redirectToCheckout by pinning an older Stripe.js?
For now. Stripe.js v3, acacia, and basil do not block the call; load them through @stripe/stripe-js 7 or lower, or a script URL with that version name. Stripe says it continues to support older versions and has published no end date, but TypeScript types are not backported and new features land in newer versions. Treat a pin as time to migrate.
Can I use Stripe Checkout without a backend?
Through Stripe's no-code embeds: Payment Links, Buy Buttons, and, for subscriptions, the embeddable pricing table, which all take only a publishable key or none. Creating a Checkout Session yourself takes a secret key, which must stay on a server. A Payment Link is created once in the Dashboard and works as a plain URL. Its items, up to 20, are set when you create it, not by code in the browser.
Why does fetch not follow my server's redirect to Stripe Checkout?
A redirect answered to fetch is followed by fetch itself, not by the page: your code receives the Checkout page as a response and the browser tab never navigates. Either submit a plain HTML form to an endpoint that responds with a 303, or have the endpoint return the URL as JSON and assign it to window.location.
Is it safe to build the Checkout URL from the session ID?
No. Use the url field exactly as returned. It is only present while the session is open, it uses your subdomain if you have configured a custom domain, and Stripe's documented URL carries more than the ID. Older code that passed only the ID needs its server endpoint changed to return the url.
Can a browser create a Flint checkout with no API key?
It can open a Flint payment link, which needs no key and no server. Flint's public resolve endpoint also takes no key, but Flint's API does not answer cross-origin browser requests from your own domain, so code on your site cannot call it. For a cart built in your app, create the order and checkout session in a server function.
On Flint, a link that creates an order
Flint is a payments API with orders, tax, and inventory built in, running on Stripe rails. Its no-backend path is the same shape as Stripe's: a payment link, created once, opened as a URL. What differs is what each checkout leaves behind. Every checkout through a Flint link creates its own order, with the link's line items, tax when the link enables it, origin: "payment_link", and the link's metadata copied on, so refunds and reporting work per order instead of per payment. Create the link in the dashboard, or with one request:
curl -X POST https://api.withflintpay.com/v1/payment-links \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: payment-link-field-notes-001" \
-d '{
"name": "Field notes, 3-pack",
"line_items": [{
"name": "Field notes, 3-pack",
"quantity": 1,
"unit_price_money": { "amount": 1800, "currency": "USD" },
"allow_quantity_adjustment": true,
"min_quantity": 1,
"max_quantity": 5
}],
"tax": { "enabled": true },
"customer_collection": { "require_email": true },
"redirects": { "success_redirect_url": "https://example.com/thanks" },
"inactive_message": "This edition is sold out."
}'
{
"data": {
"payment_link_id": "pl_1kmn0aExample",
"url": "https://checkout.withflintpay.com/pay/pl_1kmn0aExample",
"status": "active"
}
}
Your page needs only the URL. There is no publishable key to embed, because Flint has none: every Flint key is a server credential, and a payment link needs no key at all.
<a href="https://checkout.withflintpay.com/pay/pl_1kmn0aExample">Buy now</a>
Buyers choose a quantity between the bounds you set, max_completions caps a limited drop, and inactive_message is what buyers see after you deactivate the link. Fulfill from the order.paid webhook when you add a server, or from the dashboard until then.
When the app needs a cart
A cart assembled in your app needs a server function, as it does on Stripe. On Flint it is two calls, and Flint prices the order, so the function sends items and gets back a URL:
// One server function, any runtime with fetch. The key comes from
// the runtime's secret store and holds two scopes:
// commerce.orders.write and checkouts.checkout_sessions.write.
async function startCheckout(cart) {
const headers = {
Authorization: `Bearer ${process.env.FLINT_API_KEY}`,
"Content-Type": "application/json",
};
const order = await fetch("https://api.withflintpay.com/v1/orders", {
method: "POST",
headers: { ...headers, "Idempotency-Key": `order-${cart.id}` },
body: JSON.stringify({
line_items: cart.items.map((item) => ({
variant_id: item.variantId,
quantity: item.quantity,
})),
}),
}).then((r) => r.json());
const checkout = await fetch("https://api.withflintpay.com/v1/checkout-sessions", {
method: "POST",
headers: { ...headers, "Idempotency-Key": `checkout-${cart.id}` },
body: JSON.stringify({
order_id: order.data.order_id,
redirects: { success_redirect_url: "https://example.com/thanks" },
}),
}).then((r) => r.json());
// The browser assigns this to window.location, exactly as returned.
return checkout.data.checkout_session.url;
}
The browser then does what it does on Stripe after the migration: window.location.assign(url). Flint computes the order's totals, so no amount travels from the browser. Flint also has keyless endpoints that start a payment link checkout, but they serve Flint's own hosted pages: Flint's API does not answer cross-origin browser requests from your site, so call Flint from the server function or link to the payment link.
The payment links guide covers link types, custom fields, and fulfillment, and checkout sessions covers the server path. For apps built with Lovable, v0, Bolt, or Replit, payments for AI app builders shows where each one keeps server code and secrets.
Sources
- Stripe changelog: Removes support for the redirectToCheckout method (2025-09-30.clover), accessed 2026-10-09
- Stripe docs: Stripe.js versioning and support policy, accessed 2026-10-09
- stripe-js README: package to Stripe.js version table, loadStripe behavior, accessed 2026-10-09
- stripe-js v8.0.0 release notes (2025-10-01): Remove types for redirectToCheckout, accessed 2026-10-09
- Stripe.js clover build: the IntegrationError message, read from the served script, accessed 2026-10-09
- Stripe API reference: The Checkout Session object (url, ui_mode), accessed 2026-10-09
- Stripe docs: Build a Stripe-hosted checkout page (303 redirect to session.url), accessed 2026-10-09
- Stripe docs: Create an embeddable buy button, accessed 2026-10-09
- Stripe docs: Embeddable pricing table for subscriptions, accessed 2026-10-09
- Stripe docs: Track a payment link (client_reference_id and UTM parameters), accessed 2026-10-09
- Stripe API reference: Create a payment link (line item limits), accessed 2026-10-09
- Stripe docs: Checkout migration guide, accessed 2026-10-09
- stripe-node issue #1345: TypeError: stripe.redirectToCheckout is not a function (February 2022), accessed 2026-10-09