Handling payments is one of the highest-stakes things you'll add to an app. Get it wrong and you lose money, upset customers, or worse, expose card data. Stripe exists precisely to take that complexity off your plate, it handles PCI compliance, fraud detection, and the banking relationships so you don't have to.
Core Stripe concepts
Products and prices
Stripe's data model separates what you're selling from how you're pricing it. A Product is the thing, "Pro Plan", "Design Course". A Price is the specific offer, "$29/month" or "$299 one-time". One Product can have many Prices (monthly vs annual, different currencies).
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// Create the product once
const product = await stripe.products.create({
name: 'Pro Plan',
description: 'Unlimited access to all courses'
});
// Create a recurring price for it
const monthlyPrice = await stripe.prices.create({
product: product.id,
unit_amount: 2900, // $29.00 - Stripe uses cents
currency: 'usd',
recurring: { interval: 'month' }
});
// Create a one-time price
const oneTimePrice = await stripe.prices.create({
product: product.id,
unit_amount: 29900, // $299.00
currency: 'usd'
});Checkout sessions
The fastest way to accept payments is a Checkout SessionWhat is session?A server-side record that tracks a logged-in user. The browser holds only a session ID in a cookie, and the server looks up the full data on each request.. You create a session on your server, then redirect the user to a Stripe-hosted payment page. Stripe handles the card form, 3D Secure, and Apple Pay, you just get a callbackWhat is callback?A function you pass into another function to be called later, often when an operation finishes or an event occurs. when it's done.
// Server: create the checkout session
app.post('/create-checkout', async (req, res) => {
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price: 'price_1234567890', // The Price ID from your dashboard
quantity: 1
}
],
mode: 'payment', // 'payment', 'subscription', or 'setup'
success_url: 'https://example.com/success?session_id={CHECKOUT_SESSION_ID}',
cancel_url: 'https://example.com/pricing',
customer_email: req.user.email // Pre-fill the email field
});
// Send the URL back to the frontend
res.json({ url: session.url });
});
// Frontend: redirect to Stripe's page
const { url } = await fetch('/create-checkout', { method: 'POST' }).then(r => r.json());
window.location.href = url;Webhooks
When the user finishes payment on Stripe's page, Stripe redirects them to your success_url. But that redirect is not a reliable signal, users can close their browser, the redirect can fail, or the URL can be manipulated. Webhooks are the authoritative signal.
Stripe will POST to your webhookWhat is webhook?An HTTP request that a service sends to your server when a specific event occurs, like a payment completing. Your server processes the event automatically. endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. when the payment is confirmed, regardless of what happens to the browser sessionWhat is session?A server-side record that tracks a logged-in user. The browser holds only a session ID in a cookie, and the server looks up the full data on each request..
// IMPORTANT: use express.raw() - Stripe needs the raw body to verify signatures
app.post('/webhook/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
// This verifies the request actually came from Stripe
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle the event
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object;
await activateSubscription(session.customer_email, session.id);
break;
}
case 'invoice.payment_failed': {
const invoice = event.data.object;
await notifyPaymentFailed(invoice.customer_email);
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object;
await revokeAccess(subscription.customer);
break;
}
}
// Respond quickly - Stripe retries if you don't respond within 30s
res.json({ received: true });
});Test mode
Stripe has a full test environment. Your test APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. keys (sk_test_...) hit test mode, and you can trigger events without real money moving. Use test card numbers to simulate different scenarios:
| Card number | Behavior |
|---|---|
4242 4242 4242 4242 | Always succeeds |
4000 0000 0000 9995 | Always declines (insufficient funds) |
4000 0025 0000 3155 | Requires 3D Secure authentication |
4000 0000 0000 0002 | Always declines (generic) |
Use any future expiry date and any 3-digit CVC. To test webhooks locally, use the Stripe CLIWhat is cli?Short for Command Line Interface. A tool you use by typing commands in the terminal instead of clicking buttons.:
# Install Stripe CLI, then forward events to your local server
stripe listen --forward-to localhost:3001/webhook/stripe
# Trigger a specific event manually
stripe trigger checkout.session.completedQuick reference
| Concept | What it is | When you use it |
|---|---|---|
| Product | What you're selling | Create once in dashboard or API |
| Price | How much / how often | Reference in Checkout Session |
| Checkout Session | Hosted payment page | User goes to pay |
| Payment Intent | Payment object with status | Custom payment flows |
| Webhook | Event notification from Stripe | Provisioning, cancellation, failures |
// Complete Stripe integration
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// 1. Create a payment
async function createPayment(amount, currency = 'eur') {
try {
const paymentIntent = await stripe.paymentIntents.create({
amount: amount * 100, // Stripe uses cents
currency,
automatic_payment_methods: { enabled: true }
});
return paymentIntent.client_secret;
} catch (error) {
console.error('Stripe error:', error);
throw error;
}
}
// 2. Webhook handler
async function handleStripeWebhook(event) {
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
console.log('Payment succeeded:', paymentIntent.id);
// TODO: Activate service for the user
break;
case 'payment_intent.payment_failed':
console.log('Payment failed');
// TODO: Notify the user
break;
}
}