Skip to content

What Is a Webhook? A Developer’s Guide

Sep 27, 2026 · Web Development

A webhook is a user-defined HTTP callback: you register a public URL with a service, and whenever a specific event occurs there, the service sends an HTTP POST request carrying a JSON payload to your URL. Instead of repeatedly asking for updates, your app is told the instant something changes.

If you have ever wired up a payment provider, a Git host, or a chat platform, you have almost certainly met webhooks. They are one of the most useful patterns in modern web development, yet they are easy to misunderstand because they invert the usual flow of a request. This guide explains what a webhook is, how it works end to end, how it compares to APIs and polling, and the practical concerns, security, retries, and testing, that separate a toy integration from a reliable one.

How do webhooks work?

The core idea is simple: a webhook flips the direction of a normal API call. Rather than your application initiating a request to fetch data, the provider initiates a request to you. Your server becomes the endpoint, and the provider becomes the client. The whole lifecycle has four stages.

1. You register an endpoint and subscribe to events

You expose a publicly reachable URL, for example https://api.yourapp.com/webhooks/payments, and tell the provider about it. You then subscribe to the specific events you care about, such as payment.completed or order.created. Subscribing narrowly matters: you only want to receive the events you will actually act on.

2. An event happens and the provider sends a POST

When a matching event occurs, the provider constructs an HTTP POST request and delivers it to your URL. The body is almost always JSON, and the request includes headers that identify the event and, in well-designed systems, prove its authenticity. A typical payload looks like this:

POST /webhooks/payments HTTP/1.1
Host: api.yourapp.com
Content-Type: application/json
Webhook-Id: evt_1a2b3c4d
Webhook-Signature: t=1706120000,v1=5257a869e7ec...

{
  "id": "evt_1a2b3c4d",
  "type": "payment.completed",
  "created": 1706120000,
  "data": {
    "payment_id": "pay_98xy",
    "amount": 4200,
    "currency": "usd",
    "status": "succeeded"
  }
}

Note the shape, which is fairly universal: an event id, a type naming what happened, a created timestamp, and a nested data object with the details. (Field names are illustrative; every provider names them a little differently.)

3. Your server processes and responds fast

Your endpoint receives the request, verifies it, and acknowledges receipt by returning a 200 OK quickly, ideally within a couple of seconds. The golden rule is to respond first and do the heavy work afterward. Acknowledge the delivery, push the event onto a queue or background job, and return immediately. If you run slow database writes, send emails, or call other APIs before responding, you risk timing out, and a timeout usually triggers a retry.

4. The provider retries on failure

If your endpoint returns a non-2xx status or does not respond in time, most providers retry, typically on an exponential backoff schedule. That single behavior has consequences for correctness that we return to below.

Webhooks vs APIs vs polling, what’s the difference?

These three terms are often confused because they all move data between systems over HTTP. The difference is about who initiates the request and when.

A traditional REST API is a pull model: your app makes a request whenever it wants data, and gets a response. Polling is simply calling that API on a timer, asking “has anything changed yet?” over and over. A webhook is a push model: you register once, and the provider sends you data only when a real event occurs. You do not ask; you get told.

Aspect Webhook (push) Polling (pull on a timer) Direct API call (pull on demand)
Who initiates The provider calls you You call the provider repeatedly You call the provider once, when needed
Latency Near real time As slow as your poll interval Immediate for that one request
Efficiency One call per real event Many empty calls when nothing changed Efficient, but only reflects that moment
Needs a public URL? Yes, you host an endpoint No No
Best for Reacting to events as they happen When you cannot expose a URL Fetching current state on demand

Polling wastes work: if nothing changed since the last check, the request returns nothing and the compute was spent for no reason. Worse, if something changes just after a poll, you do not learn about it until the next interval. Webhooks avoid both problems by delivering exactly one call per event, with minimal delay.

They are not mutually exclusive, though. The most robust integrations use both: webhooks as the primary, real-time channel, and periodic polling as a reconciliation fallback that catches anything a missed or dropped delivery left behind. Use a direct API call when you simply need the full current state right now, use polling when you cannot host a public endpoint, and use webhooks when you need to react to events promptly and can accept an incoming request.

How do you secure a webhook?

A webhook endpoint is a public URL, which means anyone who discovers it can POST to it. Without verification, an attacker could forge a payment.completed event and trick your system into shipping goods for free. Securing a webhook comes down to answering three questions: is this request genuinely from the provider, is it fresh, and have I already processed it?

Verify the HMAC signature

The standard mechanism is an HMAC signature over the raw request body using a shared secret that only you and the provider know. When the provider sends a webhook, it computes HMAC-SHA256(secret, raw_body) (often prefixing a timestamp) and puts the result in a header, for example X-Hub-Signature-256 on GitHub-style systems or a compound t=...,v1=... header on Stripe-style ones. Your server recomputes the same HMAC and compares.

Two details are easy to get wrong and both break verification or security:

  • Hash the raw body, before parsing. If you parse the JSON and re-serialize it, whitespace and key order can change, and your computed digest will no longer match. Capture the exact bytes you received.
  • Compare in constant time. Use a timing-safe comparison such as crypto.timingSafeEqual in Node, hmac.compare_digest in Python, or Rack::Utils.secure_compare in Ruby, so an attacker cannot infer the correct signature byte by byte from response timing.

A minimal Node.js check looks like this:

const expected = crypto
  .createHmac('sha256', secret)
  .update(rawBody)              // the raw bytes, not parsed JSON
  .digest('hex');

const ok = crypto.timingSafeEqual(
  Buffer.from(expected),
  Buffer.from(receivedSignature)
);
if (!ok) return res.status(400).send('bad signature');

Reject stale requests to stop replays

A valid, signed request that an attacker captured can be resent later, this is a replay attack. The defense is to fold a timestamp into the signed payload and reject any request whose timestamp is outside a small tolerance window. Stripe-style libraries default to rejecting anything older than about five minutes (300 seconds). If the provider does not sign a timestamp, you can rely more heavily on idempotency instead.

Layer in the basics

Signature verification is the core, but add the usual hardening too: serve the endpoint over HTTPS only, treat the shared secret like any other credential, and where possible allowlist the provider’s published IP ranges. Signature checking, replay prevention, idempotency, and ordinary input validation are separate problems, and a production-grade handler needs all of them.

Why do webhooks arrive more than once, and how do you handle it?

Retries are a feature, not a bug, but they change how you must write your handler. When your endpoint errors, times out, or a network hiccup loses your 200 response, the provider assumes delivery failed and sends the event again. A common schedule retries at roughly 5 seconds, 30 seconds, 2 minutes, 10 minutes, and an hour after the first attempt, backing off exponentially so it does not hammer a struggling server.

The practical result: the same event will sometimes be delivered twice, or more. Your handler must therefore be idempotent, meaning processing the same event repeatedly has the same effect as processing it once. The standard approach is to record each event’s unique id in a table with a uniqueness constraint. When an event arrives, check whether you have seen that ID; if so, acknowledge with 200 and stop. If not, process it and store the ID. This makes duplicate deliveries harmless.

Two related habits round this out. First, do not assume events arrive in order, retries and parallel delivery can reorder them, so use the timestamps or sequence data in the payload rather than trusting arrival order. Second, always return 2xx the moment you have safely stored the event, and do the real processing asynchronously, so a slow downstream system never causes an unnecessary retry storm.

How do you test and debug webhooks?

Testing webhooks is awkward for one reason: the provider needs a public URL, but your code runs on localhost. There are a few reliable ways around this.

  • Tunnels. Tools like ngrok, Cloudflare Tunnel, or localtunnel expose your local server at a temporary public URL. You point the provider’s webhook at that URL and receive real deliveries straight to your machine, breakpoints and all.
  • Request bins. A request-bin service gives you a throwaway URL that captures and displays every incoming request, headers, body, and timing. It is the fastest way to see exactly what a provider sends before you write any handling code.
  • Provider CLIs and dashboards. Many providers offer a CLI or a dashboard that can send sample events on demand and replay past deliveries. Replay is invaluable: you can re-trigger a real event against your handler as many times as you need, which also happens to be the perfect way to test idempotency.

When something breaks in production, work through the delivery in order. Check the provider’s delivery log first, it usually shows the response code and body your endpoint returned. A 4xx often means a failed signature check or bad parsing; a 5xx or timeout points at your own code or a slow dependency. Log the raw body and the headers you received (never the secret) so you can reproduce the exact bytes. And to test retry logic without waiting for real intervals, make your retry scheduling injectable in tests rather than sleeping through the real backoff.

Putting it together

A webhook is nothing more than an HTTP POST the provider sends you when an event happens, but building one you can trust means treating four concerns as first-class: verify every request with an HMAC signature and a constant-time compare, reject stale requests to block replays, make your handler idempotent so duplicate deliveries are safe, and acknowledge fast while processing in the background. Get those right and webhooks become one of the cleanest, most efficient ways to keep systems in sync, no polling, no wasted calls, just a message the moment something actually changes.

Frequently asked questions

What is a webhook in simple terms?

A webhook is a user-defined HTTP callback. You register a public URL with a service, and whenever a chosen event happens there, the service sends an HTTP POST request with a JSON payload to your URL. Instead of you repeatedly asking 'has anything changed?', the service tells you the moment it does.

What is the difference between a webhook and an API?

A regular API call is a request you initiate to pull data on demand. A webhook is a message a provider pushes to you when an event occurs, so your app acts as the server receiving the call. Most services use both: a REST API to read and write, and webhooks to notify you of changes without polling.

Are webhooks secure, and how do I verify them?

A raw webhook endpoint is just a public URL, so you must verify authenticity. The common method is an HMAC signature: the provider hashes the raw request body (often with a timestamp) using a shared secret and sends the digest in a header. You recompute the HMAC and compare with a constant-time check, rejecting anything that fails or is too old.

Why do webhooks send duplicate events?

Providers retry delivery when your endpoint is slow, errors, or times out, and networks can cause a response to be lost even after you processed the event. This means the same event can arrive more than once, so your handler must be idempotent, usually by storing each event ID and skipping any you have already handled.

How do I test a webhook on my local machine?

Because a webhook needs a publicly reachable URL, expose your local server with a tunnel such as ngrok or Cloudflare Tunnel, or use a request-bin service to capture and inspect payloads. Many providers also offer a CLI or dashboard to send test events and replay past deliveries so you can debug without waiting for real ones.

Contact: contact@quikconsolecom.net