What is a webhook

A webhook is an HTTP POST request that a service sends to a URL you own when an event happens, such as a successful payment. You get notified right away, without polling the API over and over.

Updated 5 min read

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
On this page

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.

PollingYour appServiceanything new?noanything new?noanything new?yesanything new?noeventdelayWebhookYour appServiceno requestseventPOST200
With polling you ask and only find out on the next round; with a webhook the service tells you right away with a POST.

How does a webhook work?#

The flow has three steps:

  1. You register a URL with the sending service (for example https://yourapp.com/webhooks/payments) and pick which events you want to receive.
  2. When one of those events happens, the service sends an HTTP POST request to your URL. The body is usually JSON with the event type and the data.
  3. Your server answers with a 2xx status 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:

json
{
  "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.

js
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:

bash
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.timingSafeEqual instead 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:

  1. Deploy the Node.js application from the dashboard, the CLI, GitHub or the VS Code extension. The squarecloud.app config file is optional: the dashboard generates it during the deploy, or you can write it at the project root.
  2. Set WEBHOOK_SECRET in the application's environment variables. The change takes effect on the next restart, with no rebuild.
  3. 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

Learn more

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.

Next articleAPI gateway vs load balancerA load balancer spreads traffic across copies of the same app. An API gateway is the front door that authenticates, limits and routes calls. Learn when to use each.