After a Stripe Checkout payment, Stripe redirects the customer to your success_url with a {CHECKOUT_SESSION_ID} parameter. Retrieve the session on your server to verify payment status before showing a confirmation. Never rely solely on the redirect — always verify with webhooks for fulfillment.
Handling the Post-Payment Redirect in Stripe Checkout
When a customer completes payment on Stripe Checkout, Stripe redirects them to your success_url. You can include {CHECKOUT_SESSION_ID} in this URL to receive the session ID as a query parameter. On your success page, retrieve the session from Stripe to verify the payment actually succeeded. This is important because users could manually navigate to your success URL without paying. For order fulfillment, always use webhooks as the source of truth.
Prerequisites
- A working Stripe Checkout Session (see 'How to Create a Checkout Session')
- A server endpoint to retrieve Checkout Session details
- Basic understanding of HTTP redirects and query parameters
Step-by-step guide
Configure success_url with the session ID
Configure success_url with the session ID
When creating the Checkout Session, include {CHECKOUT_SESSION_ID} in your success_url. Stripe replaces this placeholder with the actual session ID after payment.
1const session = await stripe.checkout.sessions.create({2 mode: 'payment',3 line_items: [{ price: 'price_xxx', quantity: 1 }],4 success_url: 'https://yoursite.com/success?session_id={CHECKOUT_SESSION_ID}',5 cancel_url: 'https://yoursite.com/cancel',6});Expected result: After payment, the customer lands on a URL like https://yoursite.com/success?session_id=cs_test_abc123.
Create a server endpoint to verify the session
Create a server endpoint to verify the session
Build an endpoint that retrieves the Checkout Session and returns its payment status. This confirms the customer actually paid before you show a confirmation.
1app.get('/api/verify-session', async (req, res) => {2 const { session_id } = req.query;34 if (!session_id) {5 return res.status(400).json({ error: 'Missing session_id' });6 }78 try {9 const session = await stripe.checkout.sessions.retrieve(session_id);1011 res.json({12 payment_status: session.payment_status, // 'paid', 'unpaid', 'no_payment_required'13 customer_email: session.customer_details?.email,14 amount_total: session.amount_total,15 currency: session.currency,16 });17 } catch (err) {18 res.status(500).json({ error: 'Invalid session' });19 }20});Expected result: The endpoint returns the payment status. Only show confirmation if payment_status is 'paid'.
Build the success page
Build the success page
On your success page, extract the session_id from the URL and call your verification endpoint. Show a confirmation only if the payment is verified.
1// On /success page2const urlParams = new URLSearchParams(window.location.search);3const sessionId = urlParams.get('session_id');45if (!sessionId) {6 window.location.href = '/'; // No session ID, redirect home7} else {8 const res = await fetch(`/api/verify-session?session_id=${sessionId}`);9 const data = await res.json();1011 if (data.payment_status === 'paid') {12 document.getElementById('status').textContent =13 `Thank you! Payment of $${(data.amount_total / 100).toFixed(2)} confirmed.`;14 } else {15 document.getElementById('status').textContent =16 'Payment not confirmed. Please contact support.';17 }18}Expected result: The success page shows a personalized confirmation with the payment amount and email.
Set up a webhook for reliable fulfillment
Set up a webhook for reliable fulfillment
The success redirect is for user experience — not for fulfillment. A user might close the browser before redirecting, or navigate to the success URL manually. Use the checkout.session.completed webhook as the source of truth.
1// Webhook endpoint (see webhook tutorial for full setup)2app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {3 const sig = req.headers['stripe-signature'];4 let event;56 try {7 event = stripe.webhooks.constructEvent(8 req.body,9 sig,10 process.env.STRIPE_WEBHOOK_SECRET11 );12 } catch (err) {13 return res.status(400).send(`Webhook Error: ${err.message}`);14 }1516 if (event.type === 'checkout.session.completed') {17 const session = event.data.object;18 // Fulfill the order here (e.g., grant access, send email)19 console.log('Payment confirmed via webhook:', session.id);20 }2122 res.json({ received: true });23});Expected result: Your server receives the checkout.session.completed event and fulfills the order regardless of whether the customer reached the success page.
Complete working example
1const express = require('express');2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);34const app = express();56// Webhook must use raw body — register BEFORE express.json()7app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {8 const sig = req.headers['stripe-signature'];9 try {10 const event = stripe.webhooks.constructEvent(11 req.body, sig, process.env.STRIPE_WEBHOOK_SECRET12 );13 if (event.type === 'checkout.session.completed') {14 const session = event.data.object;15 console.log('Order fulfilled:', session.id);16 // TODO: fulfill order, send email, update database17 }18 res.json({ received: true });19 } catch (err) {20 res.status(400).send(`Webhook Error: ${err.message}`);21 }22});2324app.use(express.static('public'));25app.use(express.json());2627// Create Checkout Session28app.post('/create-checkout-session', async (req, res) => {29 const session = await stripe.checkout.sessions.create({30 mode: 'payment',31 line_items: [{32 price_data: {33 currency: 'usd',34 product_data: { name: 'Widget' },35 unit_amount: 2000,36 },37 quantity: 1,38 }],39 success_url: `${req.headers.origin}/success?session_id={CHECKOUT_SESSION_ID}`,40 cancel_url: `${req.headers.origin}/cancel`,41 });42 res.json({ url: session.url });43});4445// Verify session for success page46app.get('/api/verify-session', async (req, res) => {47 try {48 const session = await stripe.checkout.sessions.retrieve(req.query.session_id);49 res.json({50 payment_status: session.payment_status,51 customer_email: session.customer_details?.email,52 amount_total: session.amount_total,53 });54 } catch (err) {55 res.status(400).json({ error: 'Invalid session' });56 }57});5859app.listen(3000, () => console.log('Server on port 3000'));Common mistakes when redirecting after successful Stripe Checkout payment
Why it's a problem: Fulfilling orders based solely on the success URL redirect
How to avoid: Users can navigate to your success URL without paying. Always use the checkout.session.completed webhook for fulfillment and order processing.
Why it's a problem: Forgetting the {CHECKOUT_SESSION_ID} placeholder in success_url
How to avoid: Without it, you have no way to identify which session completed. Include {CHECKOUT_SESSION_ID} as a query parameter in your success_url.
Why it's a problem: Not handling the cancel URL properly
How to avoid: The cancel URL is visited when users click back. Show a friendly page explaining they can try again, not a generic 404.
Why it's a problem: Using express.json() for the webhook route
How to avoid: The Stripe webhook signature verification requires the raw body. Use express.raw({ type: 'application/json' }) and register the webhook route before express.json() middleware.
Best practices
- Always include {CHECKOUT_SESSION_ID} in your success_url to identify the completed session
- Verify the session server-side before showing a payment confirmation to the user
- Use checkout.session.completed webhook as the source of truth for order fulfillment
- Register the webhook route with express.raw() before any express.json() middleware
- Design a friendly cancel page that encourages the customer to try again
- Store the session ID and payment status in your database for audit purposes
- Test the full flow in test mode with card 4242 4242 4242 4242 before going live
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Write a Node.js Express server with Stripe Checkout that includes: (1) creating a Checkout Session with {CHECKOUT_SESSION_ID} in the success URL, (2) a verify-session endpoint, and (3) a webhook endpoint for checkout.session.completed. Use express.raw for the webhook route.
Add a success page to my Stripe Checkout flow. After payment, redirect to /success with the session ID. Create an API endpoint that verifies the session and returns payment status. Also add a webhook for checkout.session.completed to handle fulfillment.
Frequently asked questions
What if the customer closes the browser before reaching the success page?
The payment still goes through. This is exactly why you should use webhooks for fulfillment. The checkout.session.completed webhook fires regardless of whether the customer reaches your success page.
Can someone access my success page without paying?
Yes, anyone can navigate to a URL. That is why you must verify the session server-side by retrieving it from Stripe and checking that payment_status is 'paid'.
What is the cancel_url for?
When a customer clicks the back arrow on the Stripe Checkout page, they go to your cancel_url. This does not mean the payment failed — it means they chose not to complete it.
Can I customize the success page based on what was purchased?
Yes. Retrieve the Checkout Session with expand: ['line_items'] to get the full list of purchased items, then display order details on your success page.
How do I handle redirect failures in a large application?
For complex redirect flows in production applications — especially with multiple product types, fulfillment systems, or post-payment workflows — RapidDev can help architect a robust solution with proper webhook handling and idempotent fulfillment logic.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation