With an API call, your code asks another service for data whenever it wants: that is pull. With a webhook, the service sends an HTTP POST to your URL the moment something happens: that is push. Most integrations use both, a webhook to hear about a change quickly and an API call to fetch the full, current record.
| API request (pull) | Webhook (push) | |
|---|---|---|
| Who starts it | Your code | The other service |
| Timing | When you ask, or on a polling timer | Right after the event |
| You need | An API key and a client | A public HTTPS endpoint |
| Proving the sender | The provider checks your key | You check the provider's signature |
| When things fail | You see the error and retry | The provider retries; you must handle duplicates |
| Best for | Reading or changing data on demand | Reacting to events you did not cause |
Updated
Polling an API every minute to see if a payment cleared wastes requests and still arrives late. A webhook flips the direction, but it also moves responsibility onto you: your endpoint has to be public, prove each message is genuine, answer fast and cope with the same event arriving twice. This page covers each of those jobs with code you can copy.
Paste a body and a secret to compute the signature a provider would send, then paste the header you received to compare. Everything runs in your browser with the Web Crypto API; nothing is sent anywhere.
express.raw keeps the body as bytes so the signature matches. This header format follows GitHub's sha256= convention; adapt the header name for other providers.
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.WEBHOOK_SECRET;
app.post("/webhooks/github", express.raw({ type: "application/json" }), (req, res) => {
const sent = req.get("X-Hub-Signature-256") || "";
const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
const a = Buffer.from(sent);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);
const event = JSON.parse(req.body.toString("utf8"));
queue.add(event); // do the slow work later
res.sendStatus(204); // answer quickly
});A unique index on the event id makes a second delivery fail the insert, so the work runs once no matter how many times the event arrives.
ON CONFLICT DO NOTHING returns no row for a duplicate, which your code treats as already handled.
create table processed_events (
event_id text primary key,
received_at timestamptz not null default now()
);
-- inside the worker, in the same transaction as the real work:
insert into processed_events (event_id) values ($1)
on conflict (event_id) do nothing
returning event_id;Polling is simpler to host but slower and chattier. Keep a cursor so each poll only asks for what is new.
let since = new Date(Date.now() - 60_000).toISOString();
setInterval(async () => {
const res = await fetch("https://api.example.com/orders?updated_since=" + encodeURIComponent(since), {
headers: { Authorization: "Bearer " + process.env.API_KEY },
});
const { data } = await res.json();
for (const order of data) handle(order);
if (data.length) since = data[data.length - 1].updated_at;
}, 60_000);
An API request is a question your code asks: give me order 42, list today's signups, create this customer. You choose when to ask and you get an answer right away, including a clear error if something went wrong.
The cost is that you only learn about changes when you ask. To notice new events you have to poll on a timer, and most polls come back empty.
A webhook is the provider calling you. When the event happens, it POSTs a JSON description of it to the URL you registered, usually within seconds.
Because anyone on the internet can POST to that URL, the body is untrusted until its signature checks out. Because networks fail, the same event may arrive more than once or out of order. A good receiver is built for both from day one.
A common, sturdy pattern treats the webhook as a doorbell. It tells you which object changed; you then call the API to read that object's current state and act on it. This sidesteps ordering problems and stale payloads, and a daily poll catches anything the doorbell missed.
Loosely, yes. It is an HTTP request with a JSON body, just sent in the opposite direction. People sometimes call it a reverse API. The difference that matters is who starts the conversation and who has to be online to receive it.
Use webhooks when the provider offers them for the events you need and you can host a public HTTPS endpoint. Poll when there is no webhook, when you are behind a firewall, or for a small number of checks where a minute of delay is fine.
Most providers send an HMAC SHA-256 of the body in a header, computed with a secret only you and they know. GitHub, for example, uses X-Hub-Signature-256 with a sha256= prefix; Stripe uses Stripe-Signature with a timestamp. Recompute it and compare before trusting anything.
Most providers retry failed deliveries with growing gaps between attempts, for a period that depends on the provider. Stripe, for instance, keeps retrying live events for up to three days. After that the event is only available through their API or dashboard.
Providers wait only a short time for a reply. If your code sends emails or calls other services first, the delivery can time out and be retried, even though you already acted on it. Queue the job, return 200, and process afterwards.
Related: Stripe webhooks guide, Discord webhook guide, CORS explained, HTTP status codes, API key generator.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.