Skip to main content
RapidDev - Software Development Agency
stripe-guide

How to redirect after successful Stripe Checkout payment

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.

What you'll learn

  • How to configure success_url with the session ID template variable
  • How to retrieve and verify the Checkout Session after redirect
  • Why webhooks are essential even with a success redirect
  • How to build a proper order confirmation page
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner6 min read10 minutesStripe API v2024-12+, Node.js 18+, any frontend frameworkMarch 2026RapidDev Engineering Team
TL;DR

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

1

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.

typescript
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.

2

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.

typescript
1app.get('/api/verify-session', async (req, res) => {
2 const { session_id } = req.query;
3
4 if (!session_id) {
5 return res.status(400).json({ error: 'Missing session_id' });
6 }
7
8 try {
9 const session = await stripe.checkout.sessions.retrieve(session_id);
10
11 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'.

3

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.

typescript
1// On /success page
2const urlParams = new URLSearchParams(window.location.search);
3const sessionId = urlParams.get('session_id');
4
5if (!sessionId) {
6 window.location.href = '/'; // No session ID, redirect home
7} else {
8 const res = await fetch(`/api/verify-session?session_id=${sessionId}`);
9 const data = await res.json();
10
11 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.

4

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.

typescript
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;
5
6 try {
7 event = stripe.webhooks.constructEvent(
8 req.body,
9 sig,
10 process.env.STRIPE_WEBHOOK_SECRET
11 );
12 } catch (err) {
13 return res.status(400).send(`Webhook Error: ${err.message}`);
14 }
15
16 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 }
21
22 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

server.js
1const express = require('express');
2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
3
4const app = express();
5
6// 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_SECRET
12 );
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 database
17 }
18 res.json({ received: true });
19 } catch (err) {
20 res.status(400).send(`Webhook Error: ${err.message}`);
21 }
22});
23
24app.use(express.static('public'));
25app.use(express.json());
26
27// Create Checkout Session
28app.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});
44
45// Verify session for success page
46app.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});
58
59app.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.

ChatGPT Prompt

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.

Stripe Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.