Back to Blog
Tutorial5 min read2026-05-01

Building and Deploying a Webhook Listener on PandaStack

Webhooks from Stripe, GitHub, or Twilio need a persistent public endpoint. Here's how to build and host one on PandaStack in under 30 minutes.

# Building and Deploying a Webhook Listener on PandaStack

Webhooks come up constantly — Stripe sending payment events, GitHub firing on PR merges, Twilio posting SMS delivery receipts. They all need the same thing: a persistent public HTTPS endpoint that can receive POST requests and do something with them.

This guide uses Node.js/Express, but the same pattern works with any language.

The Basic Listener

// webhook.js
const express = require('express')
const crypto = require('crypto')
const app = express()

// Raw body needed for signature verification
app.use(express.raw({ type: 'application/json' }))

function verifyStripeSignature(payload, sig, secret) {
  const computedSig = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex')
  return crypto.timingSafeEqual(
    Buffer.from(`sha256=${computedSig}`),
    Buffer.from(sig)
  )
}

app.post('/webhooks/stripe', (req, res) => {
  const sig = req.headers['stripe-signature']
  
  if (!verifyStripeSignature(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET)) {
    return res.status(400).send('Invalid signature')
  }

  const event = JSON.parse(req.body)
  
  switch (event.type) {
    case 'payment_intent.succeeded':
      console.log('Payment succeeded:', event.data.object.id)
      // your business logic here
      break
    case 'customer.subscription.deleted':
      console.log('Subscription cancelled:', event.data.object.id)
      break
    default:
      console.log('Unhandled event:', event.type)
  }

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

app.listen(process.env.PORT || 3000)

Why Signature Verification Matters

Without signature verification, anyone who knows your webhook URL can post fake events and trigger your business logic. Always verify. Stripe, GitHub, and most other services include a signature header — check their docs for the specific header name and algorithm.

Dockerfile

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "webhook.js"]

Deploy on PandaStack

  1. 1Push to GitHub
  2. 2Dashboard → Projects → New Project → Container App
  3. 3Connect your repo
  4. 4Add environment variables:

- STRIPE_WEBHOOK_SECRET — from your Stripe dashboard

- PORT=3000

  1. 1PandaStack gives you a public HTTPS URL like https://my-webhook.pandastack.io

Register the URL with Stripe (or GitHub, etc.)

In Stripe dashboard: Developers → Webhooks → Add Endpoint, paste your URL.

For GitHub: Repo Settings → Webhooks → Add webhook, paste the URL, set content type to application/json.

Handling Slow Processing

If your webhook handler needs to do slow things (database writes, sending emails), return the 200 immediately and process asynchronously:

app.post('/webhooks/stripe', async (req, res) => {
  // verify first
  if (!isValid(req)) return res.status(400).end()
  
  res.json({ received: true }) // respond immediately
  
  // then process
  const event = JSON.parse(req.body)
  await processEvent(event) // runs after response is sent
})

This prevents Stripe from thinking your endpoint timed out and retrying the same event multiple times.

Monitoring Failed Webhooks

Set up a PandaStack alert for HTTP 4xx/5xx responses from your webhook container. If signature verification is failing consistently, something has changed in the signing secret — you want to know about that fast.

Dashboard → Monitoring → Alerts → HTTP Error Rate, select your webhook project.

Using PandaStack's Managed Redis for Deduplication

Webhook providers retry on failure. To avoid processing the same event twice, store the event ID in Redis:

const redis = require('redis')
const client = redis.createClient({ url: process.env.REDIS_URL })

app.post('/webhooks/stripe', async (req, res) => {
  const event = JSON.parse(req.body)
  const key = `webhook:processed:${event.id}`
  
  const already = await client.get(key)
  if (already) return res.json({ received: true }) // duplicate, skip
  
  await client.set(key, '1', { EX: 86400 }) // remember for 24h
  
  res.json({ received: true })
  await processEvent(event)
})

Provision a Redis instance under Databases → New Database → Redis in PandaStack and add the connection URL as REDIS_URL.

Full docs: [docs.pandastack.io](https://docs.pandastack.io).

Ready to deploy?

Start free on PandaStack — no credit card required.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also