Verifying Webhook Signature

Webhook Headers

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 be the same when the same webhook is being resent (e.g., due to a previous failure).
  • svix-timestamp: 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 composed by concatenating the id, timestamp, and payload, separated by the full-stop character (.). In code, it would look something like:

const signedContent = `${headers['svix-id']}.${headers['svix-timestamp']}.${JSON.stringify(payload)}`;

Where payload is the raw body of the request (we recommend you use JSON.stringify). The signature is sensitive to any changes, so even a small change in the body will cause the signature to be completely different. This means you should not change the body in any way before verifying.

Determining the Expected Signature

Walapay uses an HMAC with SHA-256 to sign its webhooks.

To calculate the expected signature, you should HMAC the signedContent (from above) using the base64 portion of your signing secret (this is the part after the whsec_ prefix) as the key.

For example, given the secret:

whsec_MfKKr9g8GKYq7wJP0B1PLPZtOzLaLaSw

You will want to use:

MfKKr9g8GKYq7wJP0B1PLPZtOzLaLaSw

Here’s how you can calculate the signature in Node.js:

const crypto = require('crypto');

const signedContent = `${headers['svix-id']}.${headers['svix-timestamp']}.${JSON.stringify(payload)}`;
const secret = "whsec_5WbX5kEWLlfzsGNjH64I8lOOqUB6e8FH";

// Decode the base64 portion of the secret
const secretBytes = Buffer.from(secret.split('_')[1], "base64");

// Generate the HMAC signature
const signature = crypto
  .createHmac("sha256", secretBytes)
  .update(signedContent)
  .digest("base64");

console.log(signature);

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

Signature Format

The svix-signature header is composed of a list of space-delimited signatures and their corresponding version identifiers. The signature list is most commonly of length one, though 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.

Verifying the Timestamp

As mentioned above, Walapay also sends the timestamp of the attempt in the svix-timestamp header. You should compare this timestamp against your system timestamp and make sure it’s within your tolerance to prevent timestamp attacks.

Example Signatures

Here is an example you can use to verify you implemented everything correctly. Please note that this may fail verification due to the timestamp being old.

  • Secret:

    whsec_plJ3nmyCDGBKInavdOK15jsl
  • Payload:

    '{"event_type":"ping","data":{"success":true}}';
  • Message ID:

    msg_id = "msg_loFOjxBNrRLzqYUf";
  • Timestamp:

    timestamp = "1731705121";
  • Generated Signature:

    signature = "v1,rAvfW3dJ/X/qxhsaXPOyyCGmRKsaKWcsNccKXlIktD0=";