Webhooks

Instead of constantly asking "anything new?" — just get a tap on the shoulder when something happens.

The Simple Version

You order a package online. You could check the front door every 5 minutes to see if it arrived. That's annoying and wasteful — most of the time there's nothing there.

Or you could give the delivery driver your phone number and say: "Text me when you drop it off." Now you go about your day, and the moment the package arrives, you get a notification. No checking, no wasting time.

That's a webhook. It's one app saying to another: "When this thing happens, send me a message at this address." The address is a URL — a specific page on your server that's listening for incoming data.

The "checking every 5 minutes" approach is called polling. Webhooks replace that. Instead of you asking over and over, the other app tells you when something changes.

🔄
Polling
"Is there anything new?"
"No." × 99
"Yes!" × 1
Wastes requests
📩
Webhook
You wait quietly.
Something happens...
*knock knock* "Here's the data."
Instant & efficient
Watch a Webhook Fire — click the button
🛒
Stripe
Payment received
📦
🖥️
Your Server
/webhooks/stripe
Waiting for event...

How It Actually Works

The Setup

You register a webhook URL with the service you want to listen to. This is just an endpoint on your server — a route that accepts incoming HTTP POST requests.

# In Stripe's dashboard, you'd set:
# Webhook URL: https://your-app.com/webhooks/stripe
# Events: payment_intent.succeeded, charge.failed

When the event happens (someone pays, a form is submitted, a repo gets a push), the service sends a POST request to your URL with a JSON payload describing what happened.

What the Data Looks Like

Anatomy of a Webhook Request — scroll to reveal
Method
POST
Always POST — the sender is pushing data to you
URL
/webhooks/stripe
Your endpoint — where you're listening
Headers
Stripe-Signature: t=1234,v1=abc...
A signature to prove the request is legit (not spoofed)
Body
{ "type": "payment_intent.succeeded", "data": { ... } }
JSON payload — what happened and the relevant data
Response
200 OK
You reply with 200 to confirm you received it. Anything else = retry.

A Real Handler

Here's what a basic webhook endpoint looks like in Express (Node.js):

app.post('/webhooks/stripe', (req, res) => {
  const event = req.body;

  switch (event.type) {
    case 'payment_intent.succeeded':
      // Unlock the user's access
      grantAccess(event.data.object.customer);
      break;
    case 'charge.failed':
      // Notify the user
      sendFailureEmail(event.data.object.customer);
      break;
  }

  // Always respond 200 — otherwise the sender retries
  res.status(200).json({ received: true });
});

Real World Examples

Where You'll See Webhooks — click to explore
💳
Stripe
Payment succeeded, subscription cancelled, invoice created. Your app reacts to billing events without polling Stripe's API.
🐙
GitHub
Push to repo, PR opened, issue commented. Triggers CI/CD pipelines, deploys, or Slack notifications automatically.
💬
Telegram
New message, callback button pressed. Instead of polling getUpdates, Telegram pushes messages to your bot's URL.
📞
Twilio
Incoming call or SMS. Twilio hits your webhook to ask: "What should I do with this call?" You respond with instructions.
🛍️
Shopify
Order placed, product updated, refund issued. Your fulfillment system reacts in real time without scraping the store.
📧
SendGrid
Email delivered, opened, bounced, or marked spam. Track email engagement without repeatedly checking send status.

Security: Don't Trust Blindly

Anyone can send a POST request to your webhook URL. If you don't verify it, an attacker could fake events — like telling your app a payment succeeded when it didn't.

Webhook Security Checklist — click to check off
Verify signatures
Check the HMAC header against your secret key
Use HTTPS
Never expose a webhook over plain HTTP
Respond fast (200 OK)
Do heavy work async — just acknowledge receipt
Handle retries
Make your handler idempotent — same event twice = same result

Key Takeaways