Stripe Checkout is the fastest legitimate way to take payments online. Stripe hosts the payment page, handles the card fields, and deals with PCI compliance, so your code never touches a card number. In this guide you'll wire up a Node app that sends customers to a Stripe-hosted checkout page, then confirms the payment with a webhook. By the end you'll have taken a test payment end to end and know what "going live" involves.
Prerequisites
- A Stripe account (free to create at stripe.com; you can build everything in test mode before activating).
- Node.js 18 or newer installed.
- A basic Node/Express app, or willingness to start one from scratch.
- The Stripe CLI installed (used to test webhooks locally).
Step 1: Get your test mode keys
In the Stripe Dashboard, make sure the test mode toggle is on, then go to Developers, then API keys. You'll see two keys:
- Publishable key (starts with
pk_test_): safe to expose in a browser. - Secret key (starts with
sk_test_): server-side only. Never commit it, never put it in frontend code.
Put the secret key in an environment variable. Locally, a .env file works:
STRIPE_SECRET_KEY=sk_test_your_key_here
Add .env to your .gitignore before you do anything else.
Step 2: Create a product and price
In the Dashboard, go to the Product catalog and add a product. Give it a name and a price. Stripe separates the two concepts: a Product is the thing you sell, a Price is an amount attached to it (one product can have several prices). After saving, copy the Price ID, which looks like price_1AbCdE.... You'll reference it in code.
You can also create products from code, but for a handful of fixed offerings, the Dashboard is simpler and keeps pricing changes out of your deploys.
Step 3: Create a checkout session from your server
Install the SDK:
npm install stripe express
Then add a route that creates a Checkout Session and redirects the customer to it:
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const app = express();
app.post('/create-checkout-session', async (req, res) => {
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [
{
price: 'price_YOUR_PRICE_ID',
quantity: 1,
},
],
success_url: 'https://yourdomain.com/success?session_id={CHECKOUT_SESSION_ID}',
cancel_url: 'https://yourdomain.com/cancel',
});
res.redirect(303, session.url);
});
app.listen(4242, () => console.log('Running on port 4242'));
A few things worth knowing:
mode: 'payment'is a one-time charge. Use'subscription'for recurring billing.- The
success_urlis where Stripe sends the customer after paying. The{CHECKOUT_SESSION_ID}placeholder gets filled in by Stripe so your success page can look up the session. - The
cancel_urlis where they land if they back out. Send them back to the product page, not a dead end.
On your site, the buy button is just a form that posts to this route:
<form action="/create-checkout-session" method="POST">
<button type="submit">Buy now</button>
</form>
Step 4: Confirm payment with a webhook
Here is the part people skip and regret. Do not treat "customer landed on the success page" as proof of payment. Browsers close, connections drop. The reliable signal is the checkout.session.completed webhook, which Stripe sends to your server when the payment actually goes through.
Add a webhook endpoint. Note that this route needs the raw request body for signature verification, so it uses express.raw:
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
req.headers['stripe-signature'],
endpointSecret
);
} catch (err) {
return res.status(400).send(`Webhook error: ${err.message}`);
}
if (event.type === 'checkout.session.completed') {
const session = event.data.object;
// Fulfill the order here: mark it paid, send the email, grant access.
console.log('Payment confirmed for session', session.id);
}
res.json({ received: true });
});
The signature check matters. Without it, anyone who finds your webhook URL can fake a "payment succeeded" event.
To test locally, use the Stripe CLI to forward events to your machine:
stripe listen --forward-to localhost:4242/webhook
The CLI prints a webhook signing secret (starts with whsec_). Put that in STRIPE_WEBHOOK_SECRET while developing.
Step 5: Run a test payment
Start your server, click your buy button, and you'll land on the Stripe-hosted checkout page. In test mode, use card number 4242 4242 4242 4242 with any future expiration date, any CVC, and any ZIP. Submit it and watch three things happen: the browser redirects to your success URL, the payment shows up in the Dashboard under Payments, and your webhook logs the completed session.
Stripe publishes other test card numbers for declines and failed payments. Test at least one decline so you know what the customer sees.
Step 6: Go live
When test mode works end to end:
- Activate your Stripe account (business details, bank account for payouts).
- Switch the Dashboard out of test mode and copy your live keys (
sk_live_). - Recreate your product and price in live mode. Test and live data are completely separate, so the Price ID will be different.
- Register your webhook endpoint in the Dashboard under Developers, then Webhooks, pointing at your production URL, subscribed to
checkout.session.completed. Copy the new signing secret. - Update the environment variables on your server. Never hardcode keys, and keep live keys out of the repo entirely.
Run one real transaction with your own card and refund it from the Dashboard. Cheap insurance.
Wrap-up
You now have a checkout flow where Stripe handles the card data, your server creates sessions, and the webhook is the source of truth for fulfillment. Stripe's fee is roughly 3% plus a small per-transaction charge for standard card payments in the US, and there is no monthly minimum, which makes it a fair deal until your volume is large enough to negotiate.
Stuck on this, or want it done for you? That's the job.
Email us →