Learning objectives
After reading this article you will be able to:
- Understand what a webhook is and how it differs from polling an API
- Receive a webhook and verify its HMAC signature in Node.js
- Avoid the usual mistakes: slow responses, duplicate events and unprotected URLs
Say you sell a product and want to activate the customer's account as soon as they pay. You can ask the payment gateway every ten seconds whether the payment has arrived, or you can ask it to tell you when it happens. That second option is a webhook.
Webhooks are everywhere: payments, GitHub notifications, Telegram bot messages, forms, monitoring alerts. Knowing how they work saves you pointless requests and heads off a few security problems.
How does a webhook work?#
The flow has three steps:
- You register a URL with the sending service (for example
https://yourapp.com/webhooks/payments) and pick which events you want to receive. - When one of those events happens, the service sends an HTTP
POSTrequest to your URL. The body is usually JSON with the event type and the data. - Your server answers with a
2xxstatus code to confirm it got it.
If you do not answer with 2xx, the sender counts it as a failed delivery. Stripe, for example, also treats 3xx redirects as failures.
A typical event looks like this:
{
"id": "evt_123",
"type": "payment.succeeded",
"data": { "order_id": "ord_42", "amount": 4990 }
}Field names change from one service to another. The idea stays the same: a POST with an event ID, a type and the data.
What is the difference between a webhook and polling?#
With polling, your app asks at regular intervals whether there is anything new. It is simple, but most answers are "nothing new": you burn requests and learn about each event up to one full interval late.
With a webhook, the notification arrives when the event happens, and only then. There is less traffic and less latency. In exchange, your server has to be reachable from the internet and ready to accept requests from third parties.
In practice you combine them: the webhook notifies you, and if you suspect you missed one, a periodic job queries the REST API to reconcile.
How do you receive a webhook in Node.js?#
This example uses Express 5 with ES modules ("type": "module" in package.json) and verifies the event signature. The scheme is the one GitHub uses: an HMAC-SHA256 of the body, computed with a shared secret and sent in the X-Hub-Signature-256 header with the sha256= prefix. Other services use different header names and formats (Stripe uses Stripe-Signature, which also includes a timestamp), so always check their documentation.
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.WEBHOOK_SECRET;
function isValidSignature(body, header) {
// if the Content-Type does not match, express.raw leaves req.body undefined
if (!header || !Buffer.isBuffer(body)) return false;
const expected = Buffer.from(
"sha256=" + createHmac("sha256", SECRET).update(body).digest("hex")
);
const received = Buffer.from(header);
// timingSafeEqual throws if the lengths differ
return received.length === expected.length && timingSafeEqual(received, expected);
}
// express.raw keeps the exact body as bytes, which the signature needs
app.post("/webhooks/payments", express.raw({ type: "application/json" }), (req, res) => {
if (!isValidSignature(req.body, req.get("X-Hub-Signature-256"))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
console.log("Event received:", event.type);
res.sendStatus(200); // answer first, process later
});
app.listen(process.env.PORT ?? 3000);To test it without relying on an external service, compute the signature yourself:
BODY='{"id":"evt_123","type":"payment.succeeded"}'
SIGNATURE="sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | awk '{print $NF}')"
curl -i -X POST http://localhost:3000/webhooks/payments \
-H "Content-Type: application/json" \
-H "X-Hub-Signature-256: $SIGNATURE" \
-d "$BODY"If you change a single letter of the body without recomputing the signature, you get 401.
Why do you have to verify the signature?#
Your URL is public. Without verification, anyone who knows it can send a fake "payment succeeded" event and get your system to activate an account without charging. Stripe's documentation warns about exactly that.
Three rules for verifying signatures:
- Use the raw body. If a middleware parses the JSON into an object and serializes it again, the bytes change and the signature no longer matches. Stripe lists this as a frequent cause of verification failures.
- Compare in constant time. GitHub recommends
crypto.timingSafeEqualinstead of==to avoid timing attacks. - Keep the secret in an environment variable, never in the code or the repository.
Some senders include a timestamp in the signed content. Stripe does, and its libraries reject events more than five minutes off by default, which blocks replay attacks. As an extra layer, some senders publish a list of source IPs you can allow in your firewall.
What mistakes are common with webhooks?#
Taking too long to answer. If you do the heavy work before replying, the sender can mark the delivery as timed out and send it again. Stripe recommends returning the 2xx before any complex logic and processing through an asynchronous queue.
Processing the same event twice. Senders can deliver an event more than once, especially when they retry. Store the ID of every processed event and drop the repeats. That makes your handler idempotent.
Assuming an order. Stripe states plainly that it does not guarantee delivering events in the order they were generated. If you need the current state, fetch it from the API instead of trusting the arrival order.
Accepting every event type. Subscribe only to the ones you use. Fewer events, less load and less room for errors.
Testing only in production. Use a tunnel such as ngrok or the sender's CLI, and check the deliveries dashboard, which usually shows the status code and when the next retry will happen.
When should you not use a webhook?#
A webhook needs a server that is reachable and always up. If your client is a web page or a mobile app that needs live data, a persistent connection such as a WebSocket fits better. If the sender does not offer webhooks, or you need to read the full state every so often, polling is still a valid choice.
When losing an event is expensive, do not rely on webhooks alone: pair them with periodic reconciliation through the API.
Watch out for the name, too. Discord webhooks are a URL for posting messages to a channel: there you send, while here you receive.
How to do this on Square Cloud#
At its core, a webhook is an HTTP route in your web application with a public HTTPS URL. A web application on Square Cloud gets a *.squareweb.app subdomain with HTTPS on every plan, so you can register https://your-app.squareweb.app/webhooks/payments with the sender without setting up a proxy or certificates.
Steps for the example above:
- Deploy the Node.js application from the dashboard, the CLI, GitHub or the VS Code extension. The
squarecloud.appconfig file is optional: the dashboard generates it during the deploy, or you can write it at the project root. - Set
WEBHOOK_SECRETin the application's environment variables. The change takes effect on the next restart, with no rebuild. - Register the public URL with the sender and log every delivery in your code: the dashboard's real-time logs show whatever your application prints.
Applications are always on, with no sleep mode. That matters because a sender that gets no answer marks the delivery as failed. Telegram bots can also receive updates through a webhook instead of long polling, using an HTTPS subdomain.
See the API hosting page for runtimes, plans and limits.
API hosting
APIs in always-on containers, with no cold starts
FAQs
What is the difference between a webhook and an API?
With an API, you make the request when you want data. With a webhook, the other service calls you when something happens. They work together: the webhook notifies you and the API is how you fetch the details.
Does a webhook need HTTPS?
In production, yes. Stripe, for example, requires a public HTTPS URL for live endpoints. HTTPS also keeps the event content from being read along the way.
What happens if my server is down when a webhook arrives?
It depends on the sender. Many retry: in live mode Stripe retries for up to three days with exponential backoff. Others do not retry, so check their documentation and make sure you can reconcile through the API.
How do I test a webhook on my computer?
You need a public URL that reaches your machine. You can use a tunnel such as ngrok, or the sender's own tool, such as `stripe listen`, which forwards events to localhost.
Is a webhook the same as a WebSocket?
No. A webhook is a one-off HTTP request from the sender to your server. A WebSocket is a persistent, two-way connection you open between client and server.
More in APIs
See all 15 articles- How to host an APIHosting an API means running its HTTP server around the clock at a public address with HTTPS. Step-by-step with Node.js and Python, testing and common errors.
- How to host a Node.js appTo host a Node.js app, prepare package.json, read the port and secrets from the environment, pick an LTS version and run it with node. Step-by-step guide.
- How to host a FastAPI, Flask or Django appTo host a FastAPI, Flask or Django app, run it with a production server (Uvicorn or Gunicorn) on 0.0.0.0. Commands, Django settings and common errors included.
- How to host a Python app or scriptTo host a Python app or script, pin the version, isolate dependencies in a venv, read secrets from the environment and handle SIGTERM. Step-by-step guide.