Skip to content

Join the Seedly owners community →

API Fundamentals

Webhooks

Receiving real-time notifications

Written by 12 min read1 activity
Sprout, your presenter

Sprout presents

Polling an API every few seconds wastes an absurd number of requests. I did the arithmetic. Webhooks simply call you when something happens.Polling an API every few seconds wastes an absurd number of requests. I did the arithmetic. Webhooks simply call you when something happens.

Sprout relaxes in an armchair as a doorbell rings and a parcel arrives on the doorstep
Webhooks call you when something happens, no checking needed

So far you've learned how to call APIs. You send a request, you get a response. But sometimes you want it the other way around... you want the server to tell YOU when something happens. That's what webhooks are for.

What is a Webhook?#

A webhook is basically a reverse API call. Instead of you asking the server "Did anything happen?", the server tells you on its own, "Something just happened!"

Think about text notifications. You don't sit there asking your phone "Did I get a message?" every ten seconds. Your phone just lets you know when one shows up. Webhooks work the same way.

Why Use Webhooks?#

Without webhooks, you'd have to keep checking for updates over and over.

You: Any new orders?
Server: No.
You: Any new orders now?
Server: No.
You: How about now?
Server: No.
You: Now?
Server: Yes, here's one!

That's called polling. It burns resources and it misses real-time updates (it's also the toddler-in-the-backseat approach to software).

Here's the webhook version.

(You set up a webhook and wait)
Server: Hey! New order just came in! Here are the details.

WAY better.

How Webhooks Work#

Step 1. You Create an Endpoint#

First you create a URL on your own server that can receive data. That's your webhook endpoint.

https://yourapp.com/webhooks/payments

Step 2. You Register the Webhook#

You tell the other service about your endpoint. "When X happens, send data to this URL."

Step 3. Something Happens#

An event happens. Maybe a customer pays, or a user signs up, or a file finishes processing.

Step 4. The Server Calls Your Endpoint#

The service sends an HTTP POST request to your URL with the details of what happened.

Step 5. You Handle the Data#

Your endpoint takes that data and does something with it. Maybe it sends an email, updates a database, or kicks off another action.

Real-World Examples#

Payment Notifications#

Here's what happens when a customer pays through Stripe.

  1. Customer enters credit card
  2. Stripe processes the payment
  3. Stripe sends a checkout.session.completed webhook to your server, shaped like this one (trimmed WAY down, the real thing has a ton more fields)
{
  "id": "evt_1Abc23",
  "object": "event",
  "type": "checkout.session.completed",
  "created": 1760000000,
  "data": {
    "object": {
      "id": "cs_test_a1b2c3",
      "object": "checkout.session",
      "amount_total": 5999,
      "currency": "usd",
      "customer": "cus_abc123",
      "customer_details": { "email": "[email protected]" },
      "payment_status": "paid",
      "metadata": { "orderId": "order_456" }
    }
  }
}
  1. Your server marks the order as paid

A couple things worth noticing. amount_total is in cents, so 5999 means $59.99. created is a Unix timestamp in seconds. And metadata is stuff YOU attached when you created the checkout (like your own order ID), which Stripe hands right back to you here.

New User Signups#

Here's what happens when someone creates an account through a third-party service.

  1. User signs up
  2. The auth service sends a webhook like this (a simplified example, every service shapes theirs a lil different)
{
  "type": "user.created",
  "user": {
    "id": "user_xyz789",
    "email": "[email protected]",
    "createdAt": "2024-01-15T10:30:00Z"
  }
}
  1. Your server creates a profile for the new user

Shipping Updates#

And here's one for when a package changes status.

  1. Package arrives at a facility
  2. Shipping company sends a webhook like this (also a simplified example)
{
  "type": "package.in_transit",
  "tracking": "1Z999AA10123456784",
  "location": "Chicago, IL",
  "timestamp": "2024-01-15T10:30:00Z"
}
  1. Your app updates the order status and notifies the customer

Receiving a Webhook#

Here's a simple webhook endpoint in JavaScript (using Node.js with Express) that handles the Stripe payment from above. For now we're skipping the security check so you can see the bones of it. Don't ship it like this, we fix that in the next section.

app.post("/webhooks/payments", express.json(), (request, response) => {
  const event = request.body;
 
  if (event.type === "checkout.session.completed") {
    // Payment was successful
    const session = event.data.object;
    markOrderAsPaid(session.metadata.orderId);
    sendConfirmationEmail(session.customer_details.email);
  }
 
  // Always respond with 200 to acknowledge receipt
  response.status(200).send("OK");
});

The main things to notice are pretty simple.

  • The endpoint receives a POST request
  • The body holds the event data, and type tells you what happened
  • You do something with that data (markOrderAsPaid and sendConfirmationEmail are your own functions)
  • You respond with 200 to confirm you got it

Webhook Security#

Sprout compares a parcel's wax seal with a matching brass stamp in his hand
Verify the signature so you only trust real webhooks

Anybody could try to send fake data to your webhook. So you need to check that a webhook really came from the real service.

Signature Verification#

Most services sign their webhooks. They include a special header that proves the request is legit, and their SDK gives you a function to check it. Here's the same endpoint with Stripe's check bolted on.

const express = require("express");
const Stripe = require("stripe");
 
const app = express();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET; // starts with whsec_
 
// express.raw() keeps the body exactly as Stripe sent it.
// The signature check fails if the body gets parsed or changed first.
app.post(
  "/webhooks/payments",
  express.raw({ type: "application/json" }),
  (request, response) => {
    const signature = request.headers["stripe-signature"];
    let event;
 
    try {
      event = stripe.webhooks.constructEvent(request.body, signature, endpointSecret);
    } catch (err) {
      return response.status(400).send(`Webhook Error: ${err.message}`);
    }
 
    if (event.type === "checkout.session.completed") {
      const session = event.data.object;
      markOrderAsPaid(session.metadata.orderId);
    }
 
    response.status(200).send("OK");
  }
);

Verify the Source#

Some services publish a list of IP addresses their webhooks come from. You can check that requests actually come from those IPs.

Common Webhook Events#

Here are some events you might end up handling. These names are generic examples. Every service picks its own names, so always check their docs. Stripe for example uses checkout.session.completed, invoice.payment_failed, customer.subscription.deleted and charge.refunded for the payment stuff below.

Payments#

  • payment.completed - Customer paid successfully
  • payment.failed - Payment was declined
  • subscription.cancelled - Customer cancelled their subscription
  • refund.created - A refund was processed

Users#

  • user.created - New user signed up
  • user.updated - User changed their profile
  • user.deleted - User deleted their account

Messages#

  • message.received - New message came in
  • message.delivered - Your message was delivered
  • message.read - Recipient read your message

Files#

  • file.uploaded - File upload completed
  • file.processed - File processing finished (like video encoding)

Best Practices#

Respond Quickly#

Webhook senders expect a fast answer. Do the bare minimum, then respond with 200. If there's heavy processing to do, save the data and deal with it in a background job.

app.post("/webhooks/orders", express.json(), async (request, response) => {
  // Save to process later
  await saveToQueue(request.body);
 
  // Respond immediately
  response.status(200).send("OK");
});

Handle Retries#

If your endpoint returns an error (or never answers), most services will retry. Stripe keeps retrying for up to three days in live mode. Your code should be fine getting the same event more than once, and the easy way is to save each event's ID and skip any ID you've already handled.

Log Everything#

Keep a log of every webhook you receive. When something goes wrong, you'll want to see exactly what came in.

I learned this one the hard way. A client's Outlook update silently dropped 49 form-fill notifications. Deliverability said "delivered," and NOTHING hit the inbox. When the thing in the middle swears everything's fine, your own logs are the only receipts you've got.

TL;DR#

  • Webhooks are reverse API calls, where the server calls you
  • They tell you right away when events happen
  • You create an endpoint URL to receive webhook data
  • The service sends POST requests with the event details
  • Always verify webhooks are legit (signature checking)
  • Respond fast with 200 to confirm you got it
  • Webhooks are what make real-time features possible

What's Next?#

You've learned how to use APIs and receive webhooks. Now it's time to make your own. In the next lesson you'll build a simple API endpoint from scratch...

This lesson ends with a short activity.