5 Best Practices for Creating Webhooks

Tips for ensuring your webhooks are secure and efficient

Nina Yang, Solutions Architect
Feb 24th, 2022
Share

Webhooks have become an industry standard for enabling different pieces of software to communicate with each other. Since webhooks actively send data and alerts whenever triggered, they enhance your integration capability with your clients. Rather than waiting on requests from your clients, you can send data about events as they happen. Your clients don’t need to poll you or set up third-party listening services. Instead, they simply provide an endpoint to call when you have something for them.

For example, a webhook could trigger a Slackbot when a service onboards a new user. It could also page a DevOps team when a monitoring tool detects an outage. This event-driven approach to API development enables you to respond to mission-critical events immediately.

How can you make sure that your webhooks are secure and efficient? Let’s review some of the best practices you should consider when creating your own webhooks.

1: Verify endpoints

You want to reassure your clients that they are receiving data from you and not bad actors. Your clients will provide an endpoint to which you can POST and get a timely response status of 200. You should encourage them to verify the event you are sending before processing the data.

app.post("/service/webhook", async (req, res) => {
 // Verify event
 // Store event
 // Send 200 response
 res.status(200).send()
 // Do some work
});

An easy way to provide your clients with some level of security is to use an API token for validation. Generate a non-guessable token for each client and have them verify that the payload you’re sending them contains a parameter with the same value.

For webhooks carrying more sensitive data, you can use signature validation to ensure data integrity. Signature validation also involves creating a shared secret between you and the client, but you will be further using this secret to hash the webhook body and sending the result as a header in your request.

import crypto from "crypto"

async function generateAndAddSignatureHeader({body, options, client}) {
 // Store your secrets in a secure place
 const clientSecret = await getClientSecret(client);
 const signature = crypto.createHmac('sha256', clientSecret).update(body).digest('base64')

 options.headers["X-Webhook-Signature"] = signature

 return {body, options}
}

On receipt, the client creates the hash on their end using the shared secret and compares it to your header value.

import crypto from "crypto"

function validateSignature(req, res, next) {
 // Get the reader from the incoming webhook
 const sigHeader = req.headers["X-Webhook-Signature"]
 const signature = crypto.createHmac('sha256', process.env.SHARED_SECRET).update(req.body).digest('base64')

 if (signature !== sigHeader) {
 // Webhook hasn't been signed properly.
 res.status(401).send({ message: "Webhook is not properly signed"})
 }

 next()
}

Providing security around your webhooks allows clients to be more confident of the integrity of the data received. This confidence can inspire them to trust this event layer and develop even more exciting features with your product.

2: Perform error handling

You live globally connected, but sometimes those connections don’t work. Even services with a billion users can go offline. You want to ensure your webhooks don’t make it more challenging for clients to recover.

What should you do when your webhook doesn’t reach its destination? First, you can ensure the event isn’t lost and that you have stored it in a queue so you can retry.

While retrying the events in the queue, you can follow a back-off strategy. The most common strategy is to exponentially back off. This stops your webhooks from causing an unintentional denial of service attack on your clients. A non-queue-based system might look like this:

const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))

async function callWithRetry(fn, depth = 0) {
 try {
 return await fn()
 } catch(error) {
 if (depth > 7) {
 // Flag and log
 throw error
 }

 await wait(2 ** depth * 10)

 return callWithRetry(fn, depth + 1)
 }
}

As mentioned, you’d be better off using a queue-based system as it is more memory-safe. In this example, though, you are logging and moving on after seven tries.

As well as logging particular events that have failed, you can log specific endpoints that have failed on repeated consecutive occasions. While you can keep collecting and storing incoming events, you should stop triggering the endpoint and alert your clients to a possible problem in their API.

3: Avoid sending too much data

As you send data to your clients to describe events, try not to send too much data. For one, it makes it easier to maintain SOC2 and other compliance standards. To be compliant, organizations must maintain a list of “sub-processors” when sending data to third-party systems, as well as notify customers when that list changes. It’s much easier to update and manage this list when you avoid sending new sensitive data to third parties.

Additionally, keep in mind that the consumer can always request more information. Data that can change between the event creation and webhook receipt is one type you may want to minimize. For example, you may send an event with a new user’s email or phone number that your client stores and processes. By the time the client processes the event and needs to send an SMS to the user, the user may have already changed their phone number in your system.

Use webhooks to send non-sensitive information or notification of state changes. If you want to provide updates regarding sensitive data, don’t include the data itself. Instead, send an event notification with relevant UUIDs and labels to allow the client to request more.

Has a new user signed up for an account? Send {"event_type": "new_user_signup"} along with the UUID. If your client needs more information, they can request it more securely from your system.

If a client has received a parcel, send {"event_type": "parcel_received"} along with the UUID. Don’t send the address, package contents, or user details. Again, if the client needs that information, they can request it more securely now that they know it’s available.

4: Perform logging

Webhooks are part of an event-driven architecture. With these events, you should be able to trace a user through your system from account creation to parcel delivery. Equally, when debugging or auditing, it’s helpful to have an in-depth log for each event that passes through your system, and a dashboard for clients to view the information themselves.

When logging the event, along with the payload and endpoint triggered, you should log the time and necessary retries. It’s also helpful to log how long the round-trip took to help with internal monitoring and client optimization.

A detailed dashboard of event logs, a visual explorer, and the ability to send test requests are great to have. Each adds value to clients by enabling them to review the events to look for issues or improve the process on their end.

5: Provide documentation

Documenting your webhook payload structure, along with steps and sample code to verify the payloads, will make it easier for clients to integrate with your service. You should also provide instructions on how to set up and manage webhooks in your product.

Conclusion

As more services offer a webhook-based approach to interaction, your clients will expect similar timely updates and alerts from your product. With these best practices, you can make sure that adding webhooks to your platform provides your clients with the information they need while keeping their platform secure.

Transposit allows users to receive webhooks from any service and makes it seamless to perform any payload validation or processing needed. You can see more in the Webhooks section of our documentation.

Share