Skip to main content

Receiving webhooks

With your application set up to receive webhook messages, you can now start processing the messages sent by EventsAir. Your application's endpoint URL must be accessible via the internet so that EventsAir can send messages to it.

info

During development and testing, you can use tools like ngrok to expose your local development server to the internet.

When your application receives a webhook message, you should:

  1. Verify the message signature and timestamp
  2. Process the message
  3. Return an HTTP 2xx (status code 200-299) response to the webhook message request within 15 seconds, or return a response status code that indicates a failure condition
note

If your application framework enables cross-site request forgery (CSRF) protection by default, you should disable CSRF protection for your endpoint.

Verifying the message signature and timestamp

Since your application's endpoint URL is available on the internet, it might accept any HTTP POST request - including requests from unknown sources and attackers. To ensure that the message is from EventsAir, you should verify the message signature.

Similarly, another potential security issue is a replay attack where an attacker intercepts a valid webhook message (including the signature), and re-transmits it to your endpoint. This payload will pass signature validation, and will therefore be acted upon. To mitigate against replay attacks:

  • Always use transport layer security (TLS) with your endpoints
  • Implement idempotent message handling in your application
  • Verify that the timestamp in the message is within an acceptable range (e.g. within the 5 minutes before or after the current time). This requires your server's clock to be synchronised and accurate, and it's recommended that you use Network Time Protocol (NTP) to achieve this.
  • Consider implementing a firewall that only allows IP addresses on the allow list to access your endpoint

The following sections describe how to verify the message signature and timestamp.

Using Standard Webhooks libraries to verify webhooks

Standard Webhooks is an initiative to standardize the way webhooks are published and consumed. Standard Webhooks is a set of open source tools and guidelines to send webhooks easily, securely and reliably.

Included in these tools are libraries to verify webhook message signatures. EventsAir recommends using the Standard Webhooks libraries to verify webhook messages.

The following examples use C# with .NET and TypeScript/JavaScript with Node.js but you can find libraries for other languages in the Standard Webhooks GitHub repository.

First, install the Standard Webhook library for your language and runtime environment:

At the time of writing, a NuGet package for Standard Webhooks is not available. You can clone the Standard Webhooks GitHub repository and build the StandardWebhooks C# project, then include this library in your project for signature validation.

Then verify webhooks using the code below. The payload is the raw string body of the request, and the headers are the HTTP headers passed in the request. You can retrieve the signing secret for your endpoint using the webhookSubscriptionSigningSecret query (see Retrieving the signing secret for a webhook subscription for more details).

using StandardWebhooks;
using System.Net;

// These were all sent from the server
var headers = new WebHeaderCollection();
headers.Set("Webhook-Id", "msg_p5jXN8AQM9LWM0D4loKWxJek");
headers.Set("Webhook-Timestamp", "1614265330");
headers.Set("Webhook-Signature", "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=");
var payload = "{\"test\": 2432232314}";

var wh = new Webhook("whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw/Je4ZJEGP1QFb");

// Throws on error
wh.Verify(payload, headers);
Use the raw request body

The payload must be the raw HTTP request body (not a parsed as JSON and converted to a string). Some frameworks may modify the body contents through parsing which will affect the computed signature of the message.

Manually verifying webhooks

info

EventsAir recommends using the Standard Webhooks libraries to verify webhooks.

These instructions can be used in situations where the Standard Webhooks libraries are not available for your language or runtime environment.

Each webhook call includes three headers with information used for verification:

  • Webhook-Id: the unique message identifier for the webhook message. This identifier is unique across all messages, but will be the same when the same webhook is being resent (e.g. due to a previous failure).
  • Webhook-Timestamp: timestamp in seconds since epoch.
  • Webhook-Signature: the Base64 encoded list of signatures (space delimited).

Constructing the signed content

The content to sign is composed by concatenating the id, timestamp and payload, separated by the full-stop character (.). In code, it will look something like:

const signedContent = '${webhook_id}.${webhook_timestamp}.${body}'

Where body is the raw body of the request. The signature is sensitive to any changes, so even a small change in the body will cause the signature to be different than expected. Always use the raw body content for verification.

Determining the expected signature

Webhook messages are signed using an HMAC with SHA-256.

The signing secret is required in order to calculate the expected signature for your message. You can retrieve the signing secret for your endpoint using the webhookSubscriptionSigningSecret query (see Retrieving the signing secret for a webhook subscription for more details).

To calculate the expected signature, generate an HMAC using the signedContent from above using the Base64 portion of the signing secret (this is the part after the whsec_ prefix) as the key.

For example, given the secret whsec_u+HNWZY35B1hWe0LVRvkeY1vqXz+nghH you will want to use u+HNWZY35B1hWe0LVRvkeY1vqXz+nghH.

As an example, you can calculate the signature in Node.js as follows:

const crypto = require('crypto')

const signedContent = '${webhook_id}.${webhook_timestamp}.${body}'
const secret = 'whsec_u+HNWZY35B1hWe0LVRvkeY1vqXz+nghH'

// Need to Base64 decode the secret
const secretBytes = new Buffer(secret.split('_')[1], 'base64')
const signature = crypto.createHmac('sha256', secretBytes).update(signedContent).digest('base64')
console.log(signature)

This generated signature should match one of the ones sent in the webhook-signature header.

The webhook-signature header is composed of a list of space-delimited signatures and their corresponding version identifiers. The signature list most commonly only has one element although there could be any number of signatures. For example:

v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE= v1,bm9ldHUjKzFob2VudXRob2VodWUzMjRvdWVvdW9ldQo= v2,MzJsNDk4MzI0K2VvdSMjMTEjQEBAQDEyMzMzMzEyMwo=

Make sure to remove the version prefix and delimiter (e.g. v1,) before verifying the signature.

Please note that to compare the signatures it's recommended to use a constant-time string comparison method in order to prevent timing attacks.

Verify timestamp

The timestamp of the webhook is set in the webhook-timestamp header. You should compare this timestamp against your system timestamp and make sure it's within your tolerance in order to prevent timestamp attacks.

Idempotency

Applications receiving webhooks should consider implementing idempotent behavior in response to webhook messages, meaning that the same message can be processed multiple times and the result will be the same. This is important because webhook messages can be sent multiple times due to network issues or other problems.

If your endpoints are idempotent, any webhook requests will only be processed once, even if they are received multiple times. This can be done either implicitly (e.g. the content of the webhook is such that processing it multiple times doesn't make sense) or explicitly, which means adding a special ID for each message which the consumer can validate against.

You can use the Webhook-Id HTTP header in the webhook message or the correlationId property in the message payload to implement idempotency.

IP allow list

If your webhook receiving endpoint URL is hosted behind a firewall or NAT, you may need to allow traffic from our sending IP addresses.

The following IP addresses are used by EventsAir to send webhook messages:

44.228.126.217
50.112.21.217
52.24.126.164
54.148.139.208
52.215.16.239
54.216.8.72
63.33.109.123

Troubleshooting

This section covers common issues encountered when consuming webhooks.

Webhook messages are not being received

Use the following checklist to troubleshoot issues receiving webhooks:

  • Ensure that the endpoint URL for your webhook subscription is deployed and accessible from the internet (or you're using a service like ngrok to connect to your internal application).
  • Verify that the endpoint is not blocking incoming requests from the EventsAir IP addresses.
  • Check that the endpoint is not returning an error status code (e.g. 4xx or 5xx).
  • Ensure that the endpoint is returning a response within 15 seconds. Consider having your endpoint simply receive the message and add it to a queue to be processed asynchronously so you can respond promptly and avoid getting timed out.
  • Verify that CSRF protection is disabled for the endpoint in your application.
  • Ensure that your webhook subscription is enabled.
  • Check that your webhook subscription does not include a filter that might be excluding the messages you expect to receive.
  • Check that your webhook subscription includes the event types you expect to receive.

Verifying the webhook signature fails

When verifying the signature of a webhook message, ensure that you use the raw request body. Some application frameworks may modify the body contents through parsing which will affect the computed signature of the message.

Ensure that you are using the correct signing secret for the webhook subscription: each subscription (and therefore each endpoint URL) has its own signing secret.

Verifying the webhook timestamp fails

Be sure to use the webhook-timestamp HTTP header of the incoming request to verify the timestamp of the message. Do not use the timestamp value of the message payload as this is the time that the activity occurred in EventsAir, not the time the webhook message was sent.

Ensure that your server's clock is synchronized and accurate. It's recommended that you use Network Time Protocol (NTP) to achieve this.