Stop 14 · Payments & Third-Party IntegrationsTopics · 5/6

Webhooks

Part of the Full Stack Developer Roadmap

In short

Asking a payment API 'paid yet?' every five seconds wastes everyone's time. With a webhook, the payment provider calls your server the moment the money arrives.

Receiving real-time notifications from third-party services (Stripe, GitHub) via your own backend endpoint — the reverse of a normal API call, where the external service becomes the caller.

The blob character sips chai on a sofa as a delivery person arrives at the door.

Think of it like this

The delivery guy rings the bell

You don't stand at the gate every two minutes waiting for a parcel. You sit with your chai, and the delivery person rings the bell when he arrives. A webhook is that bell: you share a URL, and the other service calls it when something happens.

Watch it happen

A payment succeeds

Payment app
Your server
Database

POST /webhooks/payments

1/5A payment succeeds. The provider sends the event to the URL you registered.

The big ideas

What you need to know

They call you

You expose an HTTPS endpoint. The other service POSTs JSON to it whenever an event happens.

Always verify the signature

Anyone can POST to your URL. Check the provider's HMAC signature before trusting the body.

Expect duplicates

Providers retry on failure, so one event can arrive twice. Store event ids and ignore repeats.

Answer fast

Return a 2xx within a few seconds and push slow work to a queue, or the provider marks the delivery failed.

See it in code

Walk through the code

ts
1import crypto from "node:crypto";2 3app.post("/webhooks/payments", express.raw({ type: "application/json" }), (req, res) => {4  const expected = crypto5    .createHmac("sha256", process.env.WEBHOOK_SECRET!)6    .update(req.body)7    .digest("hex");8  const got = String(req.header("x-signature") ?? "");9 10  if (got.length !== expected.length ||11      !crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))) {12    return res.sendStatus(401);13  }14  queue.add("payment-event", JSON.parse(req.body.toString()));15  res.sendStatus(200);16});

1/4express.raw keeps the exact bytes. Signatures are computed on the raw body, not on parsed JSON.

The header name and format differ by provider, so check their docs. queue is any job queue, like BullMQ.

Save yourself the pain

Common mistakes

Oops

Processing a webhook without checking its signature.

Do this instead

Anyone who finds the URL could fake a 'payment succeeded'. Verify the signature and reject on mismatch.

Oops

Doing slow work like sending emails before replying.

Do this instead

The provider may time out and retry, causing duplicates. Acknowledge fast, then process in a background job.

Your mission

Catch a real webhook

~30 min

Quick check

Why should a webhook handler remember the event ids it has processed?

Go deeper

  1. 1Build an endpoint that receives and verifies a webhook from a service like Stripe.
  2. 2Learn to verify webhook authenticity using a signature header, not just trusting the payload.
  3. 3Test webhook handling locally using a tool like the Stripe CLI or ngrok.

More topics at this stop

Stuck on this topic? Ask an Insider

Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.

Download
InsideEdge