Nomod
Webhooks/Verifying webhook signatures

Webhooks

Verifying webhook signatures

Every request Nomod sends to your endpoint is signed. Verifying the signature protects your endpoint from fake or altered payloads.

Each webhook request includes three headers used for verification:

  • svix-id — unique identifier for the webhook message
  • svix-timestamp — Unix timestamp (seconds since epoch) of when the message was sent
  • svix-signature — Base64-encoded HMAC-SHA256 signature of the payload

Your webhook's signing secret is available in Settings > Tools & customisations > Apps & APIs > Webhooks. Select your webhook and copy the value from the Signing secret field.

Verifying a signature using Svix libraries

The Svix library handles header parsing, timestamp validation, and signature comparison for you. It is Svix's recommended verification method.

  1. Install the Svix library
Install
npm install svix
# or
yarn add svix
  1. Pass the raw request body and headers to the verify function. Svix handles the verification logic for you.
import { Webhook } from "svix";
const secret = "whsec_<your_signing_secret>";
// These headers are sent by Nomod with every webhook
const headers = {
  "svix-id": "<svix-id from request>",
  "svix-timestamp": "<svix-timestamp from request>",
  "svix-signature": "<svix-signature from request>",
};
const payload = "<raw request body>";
const wh = new Webhook(secret);
// Throws on error, returns the verified content on success
const msg = wh.verify(payload, headers);

If verification succeeds, the function returns the verified message payload. If it fails, it throws an exception. Respond with HTTP 400 in that case.

Verifying a signature manually

If you cannot use the Svix library, you can verify the signature manually using HMAC-SHA256.

  1. Extract the three svix headers from the incoming request: svix-id, svix-timestamp, and svix-signature. If any of these headers are missing, return a 400 response and reject the request.

  2. Construct the signed string by concatenating svix-id, svix-timestamp, and the raw request body, separated by dots: signedContent = svix-id + "." + svix-timestamp + "." + rawBody

  3. Compute HMAC-SHA256 using the base64 portion of your signing secret (the part after the whsec_ prefix) as the key:

    const crypto = require('crypto');
    const signedContent = `${svix_id}.${svix_timestamp}.${rawBody}`;
    const secret = "whsec_<your_signing_secret>";
    const secretBytes = Buffer.from(secret.split('_')[1], "base64");
    const signature = crypto
      .createHmac('sha256', secretBytes)
      .update(signedContent)
      .digest('base64');
    
  4. Compare your computed signature against the value in svix-signature. The header may contain multiple space-delimited signatures, each prefixed with v1,. Strip the prefix from each and compare against your computed signature using constant-time comparison. Return a 400 response if no signature matches.

    svix-signature: v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=
    
  5. Validate the timestamp. Compare svix-timestamp against your server's current time and reject any request where the difference exceeds five minutes. This prevents replay attacks.

Nomod