Back

What are Webhooks? A guide with examples

What are Webhooks? A guide with examples

Software products often need to communicate with each other when specific events occur. For example, when someone buys an item from your online shop, it must inform the warehouse stock control system and the accounting package. A single product is unlikely to handle all your requirements, so you’ll need to integrate systems in some way. This tutorial explores webhooks, a powerful solution for event-driven automation that enables seamless communication between software systems.

Assume you are using the following products:

Each system operates independently and is unaware of the others. All offer a web-based API, but their installations may exist on separate servers in different locations.

When you publish a new post on the CMS, you want:

  1. The SSG to build the static site and include the new page.
  2. All newsletter subscribers to receive an email with a link to the new page.

Your first integration attempt could consider existing or custom-developed plugins. That may get you some of the way, but each system does not necessarily know about the state of another. The CMS cannot send a newsletter unless it’s certain the site update is live.

Alternatively, the SSG could poll the CMS for new content at regular intervals, but polling is not efficient:

  • Frequent checks are resource-intensive. Weeks could elapse between new posts.
  • Less-frequent checks slow publication. An “emergency” post could take hours or days to appear.

Ideally, you want one system to contact another when a specific event occurs — and that’s where webhooks come in.

What is a webhook?

A webhook is an event-driven HTTP request. Webhooks are a general technique rather than a strict specification. When a specific event occurs, they allow a source system to send a message to a destination system using the destination’s web-based API (typically REST). The destination system acts on that request and may run its own set of webhooks:

webhook

To support webhooks, a product will generally allow you to define the following information using a UI, programmatic methods, or both:

  • The event that triggers the webhook.
  • The destination system’s API URI.
  • The data payload and format.
  • Whether the call should be synchronous (the source system waits for a response) or asynchronous (the source system does not wait).
  • The expected response, failure handling, and retry options.

Much of this is optional. Simpler systems may allow you to define an event type and URI. The destination system must provide an HTTP-based API. Ideally, it will return an immediate response and queue the work for later asynchronous processing.

Webhooks can simplify the CMS requirements above:

  1. When someone publishes a new post, the CMS (source) triggers a webhook, which calls the SSG (destination).
  2. When it receives a webhook request from the CMS, the SSG returns a response and then runs a build process to pull data from the CMS and construct HTML pages. Once deployment is complete, the SSG (source) triggers a webhook which calls the newsletter system (destination).
  3. When it receives a webhook request from the SSG, the newsletter system returns a response and starts a process to email all subscribers. It may contact the CMS or analyze website feeds to fetch and format the latest content.

Webhooks trigger processing in another system. The source system does not need to know how or when the destination system handles that task. The SSG or newsletter systems could also use a webhook to notify the CMS when processing is complete.

When should you use webhooks?

Consider webhooks when:

  • There is no existing persistent message channel between two systems, i.e. they’re not already communicating via plugins or shared database connections.
  • You want to send one-way, one-to-one messages when specific events occur.
  • The source system is able to trigger URL requests when events occur.
  • The destination system has an HTTP-based API.

Webhooks are conceptually simple but allow you to define sophisticated event-based functionality across systems. However, they are often brittle and can experience connectivity problems, authentication failures, and timeouts like any HTTP request. For that reason, a webhook should only trigger processing. That processing could request the latest information from the source and other systems, but it should not solely rely on the data sent with the webhook. In other words, do not use webhook data as a synchronization mechanism!

Security implications

The destination system’s API should be secure and guard against accidental or malicious calls. General recommendations include:

  • Use HTTPS rather than HTTP for API URIs.
  • Verify the source of the message using authentication methods such as IP checking, API tokens, JWT, OAuth, etc.
  • Do not hard-code access tokens or passwords in the source files.
  • Validate the incoming data.
  • Consider rate limiting to ensure the API is not bombarded with requests.
  • Queue incoming data for processing later.

As well as the scalability, performance, fault tolerance, availability, recoverability, monitoring, and best practice techniques required by any robust API.

The source’s webhook definition system should ensure:

Implementing webhooks in your own system

You can create a basic webhook definition system in any application. The following Node.js example defines a Webhook class with a private #hook property set to a Map object that uses an event name as a key:

// webhook handler
class Webhook {

  // webhook storage Map
  #hook = new Map();

The add(event, api) method checks if an event key exists in the this.#hook Map. If not, it creates a new entry with a new Set object as the value:

  // add a new webhook for an event
  add(event, api) {

    // create a Set for a named event
    if (!this.#hook.has(event)) {
      this.#hook.set(event, new Set());
 }

An api webhook object (with .url and optional .method, .headers, and .body properties) is then added to the event Set:

    // store API object
    this.#hook.get(event).add(api);

 }

The call(event) method runs all event webhook Fetch requests in parallel. Promise.allSettled() returns a single Promise which resolves once all requests complete:

  // call webhooks when an event occurs
  async call(event) {

    // get hooks for event
    const hooks = this.#hook.get(event);

    // make fetch requests in parallel
    if (hooks) return await Promise.allSettled(

 [...hooks].map(api => fetch(api.url, {
        method: api.method || 'GET',
        headers: api.headers || {},
        body: api.body || ''
 }))

 );

 }

}

A Webhook object allows you to define any number of webhooks for a specific event. The following example registers a postnew event webhook for the online REST testing tool at reqbin.com:

// create webhooks
const webhook = new Webhook();

webhook.add('postnew', {
  url: 'https://reqbin.com/echo/post/json',
  method: 'POST',
  headers: {
    'Authorization': 'Bearer abc123',
    'Content-Type': 'application/json'
 },
  body: JSON.stringify({
    a: 1, b: 2, c: 3
 })
});

You can add further webhooks, such as calls to an SSG to rebuild a site when postnew and postdelete events occur:

webhook.add('postnew', {
  url: 'https://localhost:8001/build/',
  method: 'POST',
  headers: {
    'Authorization': 'Bearer xyz321'
 }
});

webhook.add('postdelete', {
  url: 'http://localhost:8001/build/',
  method: 'POST',
  headers: {
    'Authorization': 'Bearer xyz321'
 }
});

Note: Authorization bearer tokens have been hard-coded here, but you should fetch them from environment variables or similar private stores.

The application can now call all registered webhooks for a specific event, such as the publication of a new post:

// run postnew webhooks
const response = await webhook.call('postnew');

The response returns an array of result objects with two properties:

  • .status: either fulfilled or rejected
  • .value: the Fetch response.

Note that .status is rejected when a call fails — typically when the network is down or the domain is not valid. It will be fulfilled even when a Fetch result is not a 200 OK response, such as 400 Bad Request, 401 Unauthorized, 404 Not Found etc.

This is a basic example, but you could extend it to:

  • Respond to Fetch errors.
  • Retry failed attempts later.
  • Allow users to define webhooks in a UI and add them to a data store.

Testing webhook systems

To ensure your application calls webhooks as expected, it’s advisable to log all calls and responses. You could create your own server to accept incoming webhook requests and return specific responses or use services such as:

Handling incoming webhooks in your own system

If your system does not already provide a web-based API, you can create a basic one in Node.js. The following code starts a web server that handles authorized calls to https://localhost:8001/build/:

import http from 'node:http';
const port = 8001;

// web server
http.createServer(async (req, res) => {

It defines a default error return code and message:

  // default unauthorized response
  let retCode = 401;
  let retMsg = { msg: 'unauthorized' };

It defines a 200 OK HTTP return code and message when the request URL is /build/ and the HTTP header contains an appropriate authorization token. (Hard-coded here, but never do that in production code!)

  // valid call made?
  if (
    req?.url === '/build/' &&
    req?.headers?.authorization === 'Bearer xyz321'
 ) {

    retCode = 200;
    retMsg = { msg: 'build started' };

 }

The server now returns a result to the source system that called the webhook. (It also disables HTTP caching.)

  // return response
  res.writeHead(retCode, {
    'Content-Type': 'application/json',
    'Cache-Control': 'must-revalidate, max-age=0'
 });
  res.write(JSON.stringify( retMsg ));
  res.end();

Valid webhook calls trigger a process to run — such as building a website:

  if (retCode === 200) {
    // START PROCESS
    // ...
 }

}).listen(port);

This is a basic example, but you could extend it to:

  • Improve security to add further validation and authorization checks. Access tokens should never be hard-coded — use environment variables or similar options.
  • Rate limit incoming calls.
  • Limit calls, so a build must finish before a new one can start.
  • Handle different types of requests.
  • Add incoming events to a processing queue.
  • Allow webhooks to run when processing is complete.

Testing APIs

Testing an application’s API that a webhook could call is the same as testing any other web-based API. You can use tools such as:

Summary

Webhooks provide a simple mechanism to triggers calls to other systems when specific events occur. Adding a webhook interface to your product makes it compatible with any system which offers an HTTP API such as REST.

Scale Seamlessly with OpenReplay Cloud

Maximize front-end efficiency with OpenReplay Cloud: Session replay, performance monitoring and issue resolution, all with the simplicity of a cloud-based service.

OpenReplay