Stripe Integration

Learn how to integrate Stripe webhooks with HooksDash for testing payment events locally.

Setup

1. Create a HooksDash Endpoint

# Via CLI
hooksdash endpoints create --name "Stripe Development" --slug stripe-dev

# Your webhook URL:
https://hooksdash-api.ibadsiddiqui.dev/api/h/stripe-dev

2. Configure in Stripe

  1. Go to Stripe Dashboard
  2. Navigate to Developers → Webhooks
  3. Click Add endpoint
  4. Enter URL: https://hooksdash-api.ibadsiddiqui.dev/api/h/stripe-dev
  5. Select events to listen for
  6. Click Add endpoint

3. Get Your Webhook Secret

After creating the endpoint in Stripe, you'll see a Signing secret (starts with whsec_). Save this for signature verification.

Implementation

Basic Webhook Handler

const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

const app = express();

// Stripe requires raw body for signature verification
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['stripe-signature'];
  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;

  let event;

  try {
    event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
  } catch (err) {
    console.error('Signature verification failed:', err.message);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // Handle the event
  switch (event.type) {
    case 'payment_intent.succeeded':
      const paymentIntent = event.data.object;
      console.log('Payment succeeded:', paymentIntent.id);
      // Fulfill the order, send email, etc.
      break;

    case 'payment_intent.payment_failed':
      console.log('Payment failed:', event.data.object.id);
      // Notify customer, retry payment, etc.
      break;

    case 'customer.subscription.created':
      console.log('New subscription:', event.data.object.id);
      // Activate subscription, send welcome email
      break;

    case 'customer.subscription.deleted':
      console.log('Subscription cancelled:', event.data.object.id);
      // Deactivate features, send cancellation email
      break;

    default:
      console.log(`Unhandled event type ${event.type}`);
  }

  res.json({ received: true });
});

app.listen(3000);

TypeScript Implementation

import express from 'express';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2023-10-16',
});

const app = express();

app.post(
  '/webhook',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const sig = req.headers['stripe-signature'] as string;

    let event: Stripe.Event;

    try {
      event = stripe.webhooks.constructEvent(
        req.body,
        sig,
        process.env.STRIPE_WEBHOOK_SECRET!
      );
    } catch (err) {
      const error = err as Error;
      console.error('Signature verification failed:', error.message);
      return res.status(400).send(`Webhook Error: ${error.message}`);
    }

    // Handle the event
    switch (event.type) {
      case 'payment_intent.succeeded': {
        const paymentIntent = event.data.object as Stripe.PaymentIntent;
        await handlePaymentSuccess(paymentIntent);
        break;
      }
      case 'payment_intent.payment_failed': {
        const paymentIntent = event.data.object as Stripe.PaymentIntent;
        await handlePaymentFailure(paymentIntent);
        break;
      }
      default:
        console.log(`Unhandled event type ${event.type}`);
    }

    res.json({ received: true });
  }
);

async function handlePaymentSuccess(paymentIntent: Stripe.PaymentIntent) {
  console.log('Payment succeeded:', paymentIntent.id);
  // Your business logic here
}

async function handlePaymentFailure(paymentIntent: Stripe.PaymentIntent) {
  console.log('Payment failed:', paymentIntent.id);
  // Your business logic here
}

app.listen(3000);

Common Event Types

EventDescription
payment_intent.succeededPayment completed successfully
payment_intent.payment_failedPayment failed
customer.createdNew customer created
customer.updatedCustomer information updated
customer.subscription.createdNew subscription created
customer.subscription.updatedSubscription updated
customer.subscription.deletedSubscription cancelled
invoice.payment_succeededInvoice paid successfully
invoice.payment_failedInvoice payment failed

Testing Locally

1. Start Your Server

node server.js

2. Start HooksDash Tunnel

hooksdash forward --port 3000 --endpoint stripe-dev

3. Send Test Webhook

In Stripe Dashboard:

  1. Go to Developers → Webhooks
  2. Click on your endpoint
  3. Click Send test webhook
  4. Select an event type
  5. Click Send test webhook

You should see the webhook arrive in your local server!

Production Deployment

When deploying to production:

  1. Create a separate HooksDash endpoint for production
  2. Update your Stripe webhook URL to point to your production server
  3. Use a different webhook secret for production
  4. Enable all relevant events in Stripe
  5. Implement proper error handling and retry logic

Best Practices

1. Verify Signatures

Always verify webhook signatures to ensure they come from Stripe:

stripe.webhooks.constructEvent(req.body, sig, webhookSecret);

2. Return 2xx Quickly

Respond with a 2xx status code within 5 seconds:

app.post('/webhook', async (req, res) => {
  // Verify signature
  const event = stripe.webhooks.constructEvent(req.body, sig, secret);

  // Respond immediately
  res.json({ received: true });

  // Process asynchronously
  processStripeEvent(event).catch(console.error);
});

3. Handle Duplicates

Stripe may send the same webhook multiple times. Implement idempotency:

const processedEvents = new Set();

app.post('/webhook', (req, res) => {
  const event = stripe.webhooks.constructEvent(req.body, sig, secret);

  if (processedEvents.has(event.id)) {
    return res.json({ received: true });
  }

  processedEvents.add(event.id);
  // Process event...
});

4. Use Webhook Logs

Check Stripe's webhook logs to debug delivery issues:

  • Dashboard → Developers → Webhooks → [Your endpoint] → Recent events

Troubleshooting

Signature Verification Failed

Issue: Webhook Error: No signatures found matching the expected signature

Solution:

  • Verify you're using express.raw() middleware, not express.json()
  • Check that your webhook secret is correct
  • Make sure you're passing the raw request body to constructEvent

Webhook Not Received

Issue: Webhook sent from Stripe but not appearing in HooksDash

Solution:

  • Verify the endpoint URL in Stripe matches your HooksDash endpoint
  • Check that the endpoint is active in HooksDash
  • Look at Stripe's webhook logs for delivery status

Local Forwarding Not Working

Issue: Webhook appears in HooksDash but not forwarded to localhost

Solution:

  • Ensure the CLI tunnel is running: hooksdash forward --port 3000
  • Verify your local server is running
  • Check that the port number is correct

Resources