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.
Session Replay for Developers
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — an open-source session replay suite for developers. It can be self-hosted in minutes, giving you complete control over your customer data.
Happy debugging! Try using OpenReplay today.
Assume you are using the following products:
- A headless Content Management System (CMS), such as WordPress, Strapi, or Directus. All headless CMS products offer an HTTP-based API to programmatically access content.
- A Static Site Generator (SSG), such as Jekyll, Eleventy, or Hugo 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:
- The SSG to build the static site and include the new page.
- 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:
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:
- When someone publishes a new post, the CMS (source) triggers a webhook, which calls the SSG (destination).
- 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).
- 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:
- Authorized personnel can create webhook definitions.
- It logs and reports bad responses 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 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
: eitherfulfilled
orrejected
-
.value
: theFetch
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:
- reqbin.com: echoes webhook data back
- webhook-test.com: logs webhook calls
- 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/
:
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:
- Jest for local testing
- Cypress for Continuous Integration testing
- Postman, Hoppscotch, Prestige, REST test test, or 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.
Top comments (0)