Verifying webhooks

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

  • svix-id: The unique message identifier for the webhook message. This identifier is unique across all messages but will remain the same when the same webhook is being resent (e.g., due to a previous failure).

  • svix-timestamp: The timestamp in seconds since the epoch.

  • svix-signature: The Base64-encoded list of signatures, space-delimited.

Constructing the Signed Content

The content to sign is created by concatenating the ID, timestamp, and payload, separated by a period (.). In code, it would look something like this:

const signedContent = `${svix_id}.${svix_timestamp}.${body}`;

Where body is the raw body of the request. The signature is highly sensitive to any changes, so even a minor modification in the body will result in a completely different signature. Therefore, you should not alter the body in any way before verifying it.

Determining the Expected Signature

Txn uses HMAC with SHA-256 to sign its webhooks.

To calculate the expected signature, you should HMAC the signedContent (constructed as described above) using the Base64-decoded portion of your signing secret (the part after the whsec_ prefix) as the key. For example, if your secret is whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw, you should use MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw as the key.

Here's an example of hot to calcialte the signature in Ruby on Rails:

Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest.new("sha256"), Base64.decode64(secret), "#{msgId}.#{timestamp}.#{payload}")).strip

Here’s an example of how to calculate the signature in Node.js:

const crypto = require('crypto');

const signedContent = `${svix_id}.${svix_timestamp}.${body}`;
const secret = "whsec_5WbX5kEWLlfzsGNjH64I8lOOqUB6e8FH";

// Base64 decode the secret key
const secretBytes = Buffer.from(secret.split('_')[1], "base64");
const signature = crypto
  .createHmac('sha256', secretBytes)
  .update(signedContent)
  .digest('base64');

console.log(signature);

The generated signature should match one of the signatures sent in the svix-signature header.

The svix-signature header contains a list of space-delimited signatures with their corresponding version identifiers. The list usually contains one signature, but there can be multiple. For example:

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

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

Security Note: Use a constant-time string comparison method to compare signatures and prevent timing attacks.

Verify Timestamp

As mentioned above, Txn includes the timestamp of the attempt in the svix-timestamp header. Compare this timestamp against your system's timestamp to ensure it falls within your acceptable tolerance range, helping to prevent timestamp attacks.

Last updated