Skip to main content

Overview

All Midbound webhooks are signed using the Standard Webhooks specification. Each webhook endpoint has its own signing secret, generated when you create the endpoint.

Signature Headers

Every webhook request includes three signature headers:
HeaderDescription
webhook-idUnique message identifier (e.g., evt_01HXYZ...)
webhook-timestampUnix timestamp in seconds
webhook-signatureHMAC-SHA256 signature with version prefix

Signature Format

The webhook-signature header contains a versioned signature:
webhook-signature: v1,K2jXNdR8s7fG5hY...
The signature is computed as:
base64(HMAC-SHA256("{webhook-id}.{webhook-timestamp}.{payload}"))

Verification Steps

  1. Extract the timestamp and signature from the headers
  2. Construct the signed payload: {webhook-id}.{webhook-timestamp}.{raw_body}
  3. Compute the expected signature using your signing secret
  4. Compare signatures using a timing-safe comparison
  5. Optionally reject timestamps older than 5 minutes to prevent replay attacks

Code Examples

import crypto from "node:crypto";

function verifyWebhook(
  payload: string,
  headers: {
    "webhook-id": string;
    "webhook-timestamp": string;
    "webhook-signature": string;
  },
  secret: string
): boolean {
  const { "webhook-id": msgId, "webhook-timestamp": timestamp, "webhook-signature": signature } = headers;

  // Check timestamp is not too old (5 minutes)
  const now = Math.floor(Date.now() / 1000);
  if (now - parseInt(timestamp) > 300) {
    return false;
  }

  // Extract signature (remove "v1," prefix)
  const expectedSig = signature.split(",")[1];

  // Compute signature
  const signedPayload = `${msgId}.${timestamp}.${payload}`;
  const rawSecret = secret.startsWith("whsec_") ? secret.slice(6) : secret;
  const computedSig = crypto
    .createHmac("sha256", rawSecret)
    .update(signedPayload)
    .digest("base64");

  // Timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(expectedSig),
    Buffer.from(computedSig)
  );
}

Using Standard Webhooks Libraries

Since Midbound follows the Standard Webhooks specification, you can use any compatible library:
npm install standardwebhooks
import { Webhook } from "standardwebhooks";

const wh = new Webhook("whsec_your_signing_secret");

try {
  const payload = wh.verify(rawBody, {
    "webhook-id": headers["webhook-id"],
    "webhook-timestamp": headers["webhook-timestamp"],
    "webhook-signature": headers["webhook-signature"],
  });
  // Handle verified payload
} catch (err) {
  // Signature verification failed
}

Security Best Practices

  • Always verify signatures before processing webhook payloads
  • Reject old timestamps to prevent replay attacks (recommended: 5 minutes)
  • Use HTTPS for your webhook endpoints
  • Store secrets securely using environment variables or secret managers
  • Rotate secrets periodically via the Midbound Console