Production Engineering/
Lesson

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'
});
Stripe amounts are always integers in the smallest currency unit. For USD/EUR that's cents: $29.00 = 2900. For currencies with no decimals (like JPY), the amount is the face value. Getting this wrong means charging 100x too much or too little.

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;
02

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 });
});
Webhook handlers must be idempotent. Stripe will retry a webhook if your server doesn't respond in time, so the same event can arrive multiple times. Check if you've already processed an event before acting on it.
03

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 numberBehavior
4242 4242 4242 4242Always succeeds
4000 0000 0000 9995Always declines (insufficient funds)
4000 0025 0000 3155Requires 3D Secure authentication
4000 0000 0000 0002Always 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.completed
04

Quick reference

ConceptWhat it isWhen you use it
ProductWhat you're sellingCreate once in dashboard or API
PriceHow much / how oftenReference in Checkout Session
Checkout SessionHosted payment pageUser goes to pay
Payment IntentPayment object with statusCustom payment flows
WebhookEvent notification from StripeProvisioning, cancellation, failures
javascript
// 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;
  }
}