A webhook listener is the server or process that waits for incoming webhook requests and handles them. It binds to a port, accepts POST requests on a route such as /webhooks, verifies each request, passes the event to your code and replies with a 2xx status. The listener is the running program; the webhook endpoint is the route it serves; the webhook URL is the public address providers send to.
A minimal webhook listener
Node.js with Express:
import express from 'express'
const app = express()
app.post('/webhooks', express.raw({ type: '*/*' }), (req, res) => {
if (!verifySignature(req.body, req.headers)) return res.sendStatus(401)
const event = JSON.parse(req.body)
queue.add(event) // process later
res.sendStatus(204) // acknowledge fast
})
app.listen(3000, () => console.log('listening on :3000'))
Python with Flask:
from flask import Flask, request
app = Flask(__name__)
@app.post("/webhooks")
def webhooks():
raw = request.get_data() # raw bytes for signature checks
if not verify_signature(raw, request.headers):
return "", 401
enqueue(raw)
return "", 204
app.run(port=3000)
How a webhook listener works
- It listens. The process binds to a port, such as
3000, and waits for HTTP connections. - A provider sends a request. When an event happens, the provider sends an HTTPS
POSTto the public URL, and a reverse proxy, load balancer or tunnel passes it to the listener. - It verifies the request. It checks the webhook signature against the raw body using the webhook secret.
- It acknowledges. It stores or queues the payload and returns
200or204within a few seconds. - A worker processes the event. Slow work like sending email or updating several systems happens outside the request.
Listener vs endpoint vs URL
| Term | What it is | Example |
|---|---|---|
| Webhook URL | The public address given to the provider | https://api.example.com/webhooks |
| Webhook endpoint | The route that handles it | POST /webhooks |
| Webhook listener | The running server process | A Node process on port 3000 |
Some CLIs use "listen" for a forwarder: stripe listen receives Stripe events and forwards them to your local listener. That's a forwarding tool, not the listener itself.
Running a listener on localhost
A listener on localhost:3000 works for local requests, but providers can't reach it from the internet. You have three options:
- Deploy it to a server with a public HTTPS address.
- Open a tunnel to it, which exposes the whole port publicly. See tunnel.
- Forward webhooks to it. With webhook forwarding, only the webhooks reach your machine:
relay forward --bucket my-app http://localhost:3000/webhooks
The bucket's URL is permanent, so you configure the provider once. Details are in receiving webhooks on localhost.
Common mistakes
- Parsing the body before verifying it. Body-parsing middleware changes the bytes and breaks signature checks. Read the raw body on webhook routes.
- Doing slow work in the request. If the listener takes too long, the provider times out, often seen as a 504 Gateway Timeout, and retries, so the same event is processed twice.
- Assuming each event arrives once. Retries mean duplicates. Deduplicate on the event ID. See idempotency.
- Binding to
127.0.0.1inside a container. A listener in Docker that binds to127.0.0.1can't be reached from outside the container. Bind to0.0.0.0and publish the port. - No plan for downtime. Deploys and crashes happen. If the provider's retry window is short, events sent while the listener was down are lost. A gateway with durable retries stores them and delivers once the listener is back.
