# What Are Webhooks? A Simple Guide with Practical Examples

Software products often need to communicate with each other when specific events occur. 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.

In this tutorial, we explore **webhooks**, a powerful solution for event-driven automation that enables seamless communication between software systems.


## Event-driven automation

Assume you are using the following products:

* A [headless Content Management System (CMS)](https://blog.openreplay.com/five-headless-cms-to-try-in-2023/), such as [WordPress](https://wordpress.org/), [Strapi](https://strapi.io/), or [Directus](https://directus.io/). All headless CMS products offer an HTTP-based API to programmatically access content.

* A [Static Site Generator (SSG)](https://blog.openreplay.com/static-side-generation-vs-server-side-rendering/), such as [Jekyll](https://jekyllrb.com/), [Eleventy](https://www.11ty.dev/), or [Hugo](https://gohugo.io/) that builds a website using CMS data. A small web server can start the build process when it receives a request.

* An online newsletter management system. Most have HTTP-based APIs to programmatically manage recipients and send newsletters.

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.
1. 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](https://blog.openreplay.com/retrieving-data-efficiently--webhooks-vs-polling/):

* 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. They allow a **source** system to send a message to a **destination** system using the destination's web-based API (typically REST) when a specific event occurs. The **destination** system acts on that request and may run its own set of webhooks:

![webhook](https://hackmd.io/_uploads/r1PT7ftWkl.svg)

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 response expected, 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).
1. When it receives a webhook request from the CMS, the SSG returns a response 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).
1. 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 analyse 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](https://blog.openreplay.com/building-scalable-web-applications/), [performance](https://blog.openreplay.com/optimize-web-app-performance-and-loading-speed/), fault tolerance, availability, recoverability, [monitoring](https://blog.openreplay.com/importance-of-web-performance-metrics-and-monitoring/), and best practice techniques required by any robust API.

The source's webhook definition system should ensure:

* [Authorized personnel](https://blog.openreplay.com/web-security-through-device-authorization-and-authentication/) can create webhook definitions.
* It [logs and reports bad responses](https://blog.openreplay.com/from-log-management-to-observability/) to system administrators.


## 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](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map) that uses an event name as a key:

```js
// 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](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Set) as the value:

```js
  // 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:

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

  }
```

The `call(event)` method runs all `event` webhook [`Fetch` requests](https://blog.openreplay.com/fetch-vs-axios-which-is-the-best-library-for-making-http-requests-/) in parallel. [`Promise.allSettled()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled) returns a single Promise which resolves once all requests complete:


```js
  // 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](https://reqbin.com/):

```js
// 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:

```js
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:

```js
// 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](https://blog.openreplay.com/nosql-vs-sql--a-comparison/).


### 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:

* [reqbin.com](https://reqbin.com/): echoes webhook data back
* [webhook-test.com](https://webhook-test.com/): logs webhook calls
* [smee.io](https://smee.io/): relays calls between a source and localhost destinations


## 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/`:

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

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

It defines a default error return code and message:

```js
  // 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!)

```js
  // 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 which called the webhook. (It also disables HTTP caching.)

```js
  // 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:

```js
  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 request.
* Add incoming events to a processing queue.
* Allow webhooks to run when processing is complete.


### Testing APIs

[Testing an application's API](https://blog.openreplay.com/restful-api-end-to-end-testing/) that a webhook could call is the same as testing any other web-based API. You can use tools such as:

* [Jest for local testing](https://blog.openreplay.com/api-testing-with-jest/)
* [Cypress for Continuous Integration testing](https://blog.openreplay.com/continuous-integration-testing-for-apis-with-cypress/)
* [Postman](https://blog.openreplay.com/getting-started-with-postman/), [Hoppscotch](https://hoppscotch.io/), [Prestige](https://prestige.dev/), [REST test test](https://resttesttest.com/), or [curl](https://blog.openreplay.com/making-http-requests-with-curl/) to make HTTP requests and analyse the response.


## 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.

