Day 03 Event-Driven Billing

Webhooks: Reacting to Stripe Events

Stripe can't call your frontend — it calls your server. Webhooks are how you find out that a payment succeeded, a subscription renewed, or a card was declined. Today you build a webhook endpoint, verify the signature, and handle the three most critical events.

~1 hour Intermediate Hands-on Precision AI Academy

Today's Objective

Build a verified webhook handler that processes checkout.session.completed, invoice.payment_failed, and customer.subscription.deleted without ever processing the same event twice.

The payment redirect tells you the user clicked through — it does not guarantee the money moved. Webhooks are Stripe's authoritative signal. Every production Stripe integration must handle webhooks; everything else is the optimistic path.

01

Signature Verification

Stripe signs every webhook payload with your endpoint's secret. If you skip verification, any attacker can POST fake events to your endpoint and trigger payouts, provision accounts, or mark unpaid orders as complete.

webhook.js (Express)
JavaScript
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;

  try {
    event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    return res.status(400).send(`Webhook Error: \${err.message}`);
  }

  // handle event.type below
  res.json({received: true});
});
Use express.raw(), not express.json(). Signature verification requires the raw request body. JSON parsing destroys whitespace and breaks the HMAC check.
02

The Three Events You Must Handle

Most production Stripe integrations handle dozens of event types. Three are non-negotiable on day one:

event_handler.js
JavaScript
switch (event.type) {
  case 'checkout.session.completed': {
    const session = event.data.object;
    await provisionAccess(session.customer, session.subscription);
    break;
  }
  case 'invoice.payment_failed': {
    const invoice = event.data.object;
    await sendPaymentFailedEmail(invoice.customer_email);
    break;
  }
  case 'customer.subscription.deleted': {
    const sub = event.data.object;
    await revokeAccess(sub.customer);
    break;
  }
}
03

Idempotency — Processing Events Exactly Once

Stripe delivers webhooks at least once, meaning the same event may arrive multiple times during network retries. If your handler provisions access twice or charges twice, you have a real problem. Store processed event IDs in your database and skip re-processing.

idempotent_handler.js
JavaScript
const existing = await db.query(
  'SELECT id FROM processed_events WHERE stripe_event_id = $1',
  [event.id]
);
if (existing.rows.length > 0) {
  return res.json({received: true}); // already processed
}

// process event...

await db.query(
  'INSERT INTO processed_events (stripe_event_id) VALUES ($1)',
  [event.id]
);

Supporting Videos & Reading

Go deeper with these external references.

Day 3 Checkpoint

Before moving on, you should be able to answer these without looking:

Continue To Day 4
Connect & Marketplace Payments