Webhook Guide

Stripe Webhook Testing

Payments and checkout event webhooks

Quick path: CLI helper commands

Use these first to avoid setup mistakes, then follow the full provider steps below.

instatunnel webhook init --provider stripe --port 4242 --path /webhooks/stripe
instatunnel webhook verify --provider stripe --secret-env STRIPE_WEBHOOK_SECRET
instatunnel webhook test --provider stripe

If you run into provider-specific issues, use the full checklist sections below.

1. Local app setup

Create a local webhook endpoint at: /webhooks/stripe

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

const app = express()
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.header('stripe-signature') || ''
  try {
    const event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET!
    )
    console.log('stripe event:', event.type)
    res.status(200).send('ok')
  } catch (err) {
    console.error('invalid signature', err)
    res.status(400).send('invalid signature')
  }
})

app.listen(3000, () => console.log('listening on :3000'))

2. Run InstaTunnel command

instatunnel 3000 --subdomain stripe-dev

Keep a fixed subdomain so your provider dashboard URL does not keep changing.

3. Provider config fields to paste

FieldValueWhere/notes
Endpoint URL{{WEBHOOK_URL}}Stripe Dashboard > Developers > Webhooks
Events to sendcheckout.session.completed, payment_intent.succeededLimit to only what your app handles
Signing secretwhsec_...Copy from endpoint details and set STRIPE_WEBHOOK_SECRET
API versionLatest stableUse one version consistently in staging and prod

Use the helper below to generate exact values with your chosen subdomain and path.

One-Click Webhook Setup Helper

Generate copy-ready tunnel command, webhook URL, and provider config values.

Run InstaTunnel

instatunnel 3000 --subdomain stripe-dev

Webhook URL

https://stripe-dev.instatunnel.my/webhooks/stripe
Provider fieldValue to pasteNotes
Endpoint URL
https://stripe-dev.instatunnel.my/webhooks/stripe
Stripe Dashboard > Developers > Webhooks
Events to send
checkout.session.completed, payment_intent.succeeded
Limit to only what your app handles
Signing secret
whsec_...
Copy from endpoint details and set STRIPE_WEBHOOK_SECRET
API version
Latest stable
Use one version consistently in staging and prod

Tip: keep one stable subdomain per provider to avoid reconfiguring dashboards.

4. Send test event

  1. Open Stripe Dashboard > Developers > Webhooks > your endpoint.
  2. Use "Send test webhook" for a handled event type.
  3. Confirm your local logs show the event and a 2xx response.
# Optional Stripe CLI flow
stripe listen --forward-to localhost:3000/webhooks/stripe
stripe trigger checkout.session.completed

5. Verify signature

Verify this header on every request: stripe-signature

// Stripe verifies with raw request body + endpoint secret
const event = stripe.webhooks.constructEvent(
  req.body,
  req.header('stripe-signature')!,
  process.env.STRIPE_WEBHOOK_SECRET!
)

6. Retries and idempotency

  • Store Stripe event IDs and ignore duplicates.
  • Return 2xx quickly, then process asynchronously.
  • If you return non-2xx, Stripe retries with backoff.

7. Common failures and quick fixes

400 invalid signature

Use raw body middleware and confirm STRIPE_WEBHOOK_SECRET value.

Timed out endpoint

Acknowledge quickly with 200, move heavy work to a queue.

No events received

Check endpoint URL and selected events in Stripe dashboard.