Demo, all content is generated
Question

After deploying, Stripe checkout sends customers back to localhost:3000

Solved · 483 views · asked by sophie_l · edited

Payment works on production, but after paying the customer lands on http://localhost:3000/success which obviously doesn't load. First real customer emailed me confused. The code:

const session = await stripe.checkout.sessions.create({
  ...
  success_url: "http://localhost:3000/success?session_id={CHECKOUT_SESSION_ID}",
  cancel_url: "http://localhost:3000/pricing",
});
What I’ve tried

Searched for localhost in the project, found it in 3 places, not sure what to replace it with so it also still works locally.

Comment
Also check your Stripe dashboard for the webhook endpoint URL. If that's localhost too, orders aren't being processed at all. tobiasw · edited

2 answers

Marked as helpful by the asker
tobiasw · edited

Put the base URL in an env var with a different value per environment:

# .env.local
NEXT_PUBLIC_SITE_URL=http://localhost:3000
# Vercel, Production
NEXT_PUBLIC_SITE_URL=https://yourapp.com
const base = process.env.NEXT_PUBLIC_SITE_URL;
success_url: `${base}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${base}/pricing`,

For preview deployments, leave it unset for Preview and fall back to https://${process.env.VERCEL_URL}. Don't use VERCEL_URL for production: it's the unique deployment URL, not your custom domain.

And don't trust the success page alone to mark an order paid. Fulfil orders from the checkout.session.completed webhook; customers close tabs.

Comment
Fixed and emailed the customer. And yes, I was marking paid on the success page... webhook is next on my list. sophie_l · edited
Good. That one will bite harder than localhost. tobiasw · edited
nils_tw · edited

Alternative without an env var, inside a server action or route handler:

import { headers } from "next/headers";
const origin = (await headers()).get("origin");

That's always the URL the user is actually on, including previews. I'd still validate it against a list of known domains.

Comment
Works, and the validation part is important. Don't let a spoofed Origin header decide where Stripe redirects to. tobiasw · edited