← Back to all posts
(Updated June 2026)9 min readCentrali Team

What Is a Webhook? How They Work, With Examples

A webhook is an automated HTTP POST one app sends another the moment an event happens — no polling. How webhooks work, how they differ from an API, and how to receive and verify them, with examples.

Guides

TL;DR: A webhook is an automated message one app sends another the moment something happens — delivered as an HTTP POST request to a URL you control. Instead of your app repeatedly asking a service "anything new yet?" (polling), the service pushes the event to you the instant it occurs. That's why webhooks are often called "reverse APIs": with a normal API you make the request; with a webhook, the other system makes the request to you.

If you've ever set up "notify my app when a Stripe payment succeeds" or "ping this URL when someone opens a pull request," you've used a webhook.

What Is a Webhook?

A webhook is a user-defined HTTP callback. You give a third-party service a URL (the webhook endpoint), and when a specific event happens on their side, they send an HTTP request — almost always a POST — to that URL with a JSON description of what happened.

Three parts make up every webhook:

  1. The event — something happened: a payment succeeded, an order shipped, a file finished processing, a commit was pushed.
  2. The payload — a JSON body describing the event (the amount, the order ID, the commit SHA).
  3. The endpoint — your URL that receives the request and does something with it.

The defining trait: the provider initiates the request, not you. That single inversion is what makes webhooks efficient — you find out about events at the moment they happen, without burning requests checking for changes that haven't occurred.

How Do Webhooks Work?

The flow is short:

  1. You register a URL with the provider — in their dashboard or via their API — and tell them which events you care about.
  2. An event occurs on the provider's platform.
  3. The provider sends an HTTP POST to your URL with the event payload in the body.
  4. Your endpoint receives it, does whatever work it needs to (store it, alert someone, kick off a job), and responds with a 2xx status to acknowledge receipt.
  5. If you don't respond with 2xx, most providers retry — often several times over minutes or hours — because they assume the delivery failed.

That last point matters more than people expect. Webhooks are an at-least-once delivery system: you may receive the same event more than once, and you may receive events out of order. Good webhook handling is built around those realities, not against them.

Webhook vs API: What's the Difference?

They're not opposites — a webhook is an HTTP request, the same as an API call. The difference is who initiates it and when.

Polling an APIWebhook
Who makes the requestYou, on a scheduleThe provider, on an event
TimingWhenever you check (every 30s, 5m…)The instant the event happens
EfficiencyWasteful — most checks return "nothing new"Efficient — only fires on real events
LatencyAs slow as your poll intervalNear real-time
DirectionRequest → response (you pull)Push (they call you)

Rule of thumb: if you need to ask for data on demand, use the provider's API. If you need to react to something the moment it happens, use a webhook. Most real integrations use both.

A Real Webhook Example

Say you sell something and want to know when a payment succeeds. Stripe sends a webhook that looks roughly like this:

http
POST /your-endpoint HTTP/1.1
Host: yourapp.com
Content-Type: application/json
Stripe-Signature: t=1699900000,v1=5257a869e7ec...
{
"id": "evt_1NG8Du2eZvKYlo2C",
"type": "payment_intent.succeeded",
"created": 1699900000,
"data": {
"object": {
"id": "pi_3NG8Du2eZvKYlo2C",
"amount": 2000,
"currency": "usd",
"status": "succeeded"
}
}
}

Your endpoint reads type to know what happened (payment_intent.succeeded) and data.object for the details ($20.00 USD). The same shape applies everywhere: GitHub posts a push event with the commits, Shopify posts an orders/create event with the line items, Resend posts an email.delivered event with the message ID.

What's Inside a Webhook Request

Every webhook delivery has the same anatomy:

  • HTTP method — almost always POST.
  • Headers — content type (application/json), plus provider-specific headers. The important one is usually a signature header (Stripe-Signature, X-Hub-Signature-256, X-Shopify-Hmac-Sha256) used to prove the request is genuine.
  • Body — the JSON payload describing the event.

That signature header is not optional decoration — it's how you stop attackers from forging fake events to your public URL.

How to Receive a Webhook

A webhook endpoint is just a public route that accepts a POST. Here's a minimal receiver in Express:

javascript
import express from 'express';
import crypto from 'crypto';
const app = express();
// Capture the RAW body — you need the exact bytes to verify the signature
app.post('/webhooks/github', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.header('x-hub-signature-256') ?? '';
const expected =
'sha256=' +
crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
// Constant-time comparison — never use === on secrets
const a = Buffer.from(signature, 'utf8');
const b = Buffer.from(expected, 'utf8');
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// ...do something with the event...
res.status(200).send('ok'); // acknowledge fast, before heavy work
});

Two things trip everyone up here: you must verify against the raw request bytes (parsing to JSON first changes them and breaks the signature), and you should acknowledge with 2xx quickly — do the slow work afterward, or the provider will time out and retry.

Verifying Webhook Signatures

Because a webhook URL is public, anyone who finds it can POST to it. Signature verification is how you trust the payload. The provider signs each request with a shared secret using HMAC (usually SHA-256); you recompute the same HMAC over the raw body and compare. GitHub sends X-Hub-Signature-256, Stripe sends Stripe-Signature (with a timestamp to also block replay attacks), Shopify sends X-Shopify-Hmac-Sha256.

If the signatures don't match, reject the request. If you skip this step, your "payment succeeded" handler will happily process forged payments. For a deeper walkthrough of verifying signatures across providers, see Ingest Webhooks From Any Provider — GitHub as the Example.

The Hard Parts Nobody Warns You About

Receiving one webhook is easy. Running webhooks in production is where the work hides:

  • Retries and duplicates — at-least-once delivery means you'll get the same event twice. You need idempotency: dedupe by the event ID so processing it twice is harmless.
  • Out-of-order delivery — events don't always arrive in the order they happened. Don't assume sequence.
  • Storage — you usually want to keep events: for debugging, auditing, and replay. A webhook you only react to and throw away is a webhook you can't investigate when something breaks.
  • Replay — when a downstream job fails, you want to re-run the event, which means you needed to store it.
  • Observability — "did that event arrive?" should be answerable. Without a log of received deliveries, you're guessing.

This is why teams reach for a backend behind their webhook endpoint instead of a bare route that drops events on the floor.

Where Centrali Fits

Centrali is the backend for webhooks in and out: ingest third-party webhooks, store them as queryable data, and send your own — from one SDK. Instead of standing up a server, signature-verification code, a database, and a delivery log yourself, you point the provider at a Centrali HTTP trigger URL:

https://api.centrali.io/data/workspace/{your-workspace}/api/v1/http-trigger/github

Toggle signature validation on, and every incoming event is verified and stored as a record automatically. Then you query the stored events like any other data:

typescript
import { CentraliSDK } from '@centrali-io/centrali-sdk';
const centrali = new CentraliSDK({
workspaceId: 'your-workspace',
clientId: process.env.CENTRALI_CLIENT_ID,
clientSecret: process.env.CENTRALI_CLIENT_SECRET,
});
// Every push event you've ever received, queryable
const pushes = await centrali.queryRecords('github-events', {
'data.eventType': 'push',
});

Storage, idempotency, retries, and a delivery log come with it — the hard parts above, handled.

What's Next

Frequently Asked Questions

Is a webhook an API? A webhook uses the same technology as an API (an HTTP request), but the direction is reversed. With an API you call the provider; with a webhook the provider calls you when an event happens. Many people call webhooks "reverse APIs" for this reason.

What HTTP method do webhooks use? Almost always POST, with a JSON body. A few providers let you configure the method, but POST is the default everywhere.

What is a webhook endpoint? It's the URL you give the provider — a public route in your app that accepts the incoming POST request and processes the event.

Are webhooks secure? They can be, if you verify the signature on every request. Because the URL is public, you must confirm each delivery was genuinely signed by the provider (via HMAC) and reject anything that doesn't match.

Why use a webhook instead of polling an API? Polling wastes requests checking for changes that usually haven't happened, and adds latency equal to your poll interval. A webhook fires the instant the event occurs, so you react in near real-time with no wasted calls.

What is a webhook used for? Anything that should happen the moment an event occurs somewhere else: notifying your app when a payment succeeds, syncing data when an order ships, kicking off a build when code is pushed, alerting a channel when something fails, or logging events for audit and replay. If the trigger lives in another system and you need to react to it, that's a webhook.

What does a webhook payload look like? It's an HTTP POST with a JSON body describing the event — typically an event type, an ID, a timestamp, and the data that changed (an amount, an order, a commit SHA). Providers also send headers, most importantly a signature header (like Stripe-Signature or X-Hub-Signature-256) you use to verify the request is genuine.

What is a webhook URL? It's the public address you register with the provider so they have somewhere to send events — a route in your app (or a backend like Centrali) that accepts POST requests. You create the route, copy its URL into the provider's dashboard or API, and choose which events should be delivered to it.

Are webhooks synchronous or asynchronous? Asynchronous. The provider fires the request when the event happens, independent of anything you're doing, and expects a fast 2xx acknowledgement — not a finished result. Do the real work after you acknowledge; if you respond slowly or fail, most providers retry, which is why duplicate and out-of-order deliveries are normal.

Start ingesting webhooks with Centrali

Building something with Centrali and want to share feedback about this feature?

Email feedback@centrali.io