Skip to main content

Signature Verification

Monk signs all webhook payloads so you can verify they originated from us. Always verify signatures before processing webhook data.

How Signatures Work

Each webhook includes an X-Monk-Signature header with this format:
t=1706540400,v1=5d41402abc4b2a76b9719d911017c592
PartDescription
tUnix timestamp (seconds) when the signature was generated
v1HMAC-SHA256 signature
The signature is computed over:
{timestamp}.{JSON payload}

Verification Steps

  1. Extract t (timestamp) and v1 (signature) from the header
  2. Check that the timestamp is within your tolerance window (e.g., 5 minutes)
  3. Compute the expected signature: HMAC-SHA256(secret, "{t}.{payload}")
  4. Compare signatures using constant-time comparison
Prevent replay attacks: Reject webhooks where the timestamp (t) is older than your tolerance window (recommended: 300 seconds).

Code Examples

import crypto from 'crypto';

function verifyWebhookSignature(
  payload, // Raw request body as string
  signatureHeader, // X-Monk-Signature header value
  secret, // Your webhook signing secret
  toleranceSeconds = 300
) {
  // Parse the signature header
  const parts = signatureHeader.split(',');
  const timestamp = parseInt(
    parts.find(p => p.startsWith('t='))?.slice(2) || '0'
  );
  const signature = parts.find(p => p.startsWith('v1='))?.slice(3) || '';

  // Check timestamp tolerance (prevent replay attacks)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - timestamp) > toleranceSeconds) {
    throw new Error('Webhook timestamp outside tolerance window');
  }

  // Compute expected signature
  const signedPayload = `${timestamp}.${payload}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Constant-time comparison
  const signatureBuffer = Buffer.from(signature, 'utf8');
  const expectedBuffer = Buffer.from(expected, 'utf8');

  if (signatureBuffer.length !== expectedBuffer.length) {
    return false;
  }

  return crypto.timingSafeEqual(signatureBuffer, expectedBuffer);
}

// Usage with Express.js
app.post(
  '/webhooks/monk',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const payload = req.body.toString();
    const signature = req.headers['x-monk-signature'];

    try {
      if (
        !verifyWebhookSignature(
          payload,
          signature,
          process.env.MONK_WEBHOOK_SECRET
        )
      ) {
        return res.status(401).send('Invalid signature');
      }

      const data = JSON.parse(payload);
      const event = req.headers['x-monk-event'];

      // Process the webhook
      console.log(`Received ${event}:`, data);

      res.status(200).send('OK');
    } catch (err) {
      console.error('Webhook error:', err.message);
      res.status(400).send('Webhook error');
    }
  }
);

Common Errors

Invalid Signature

The computed signature doesn’t match. Common causes:
  • Using the wrong signing secret (each endpoint has a unique secret)
  • Parsing or modifying the payload before verification
  • Encoding issues with the payload
Always verify against the raw request body before JSON parsing.

Timestamp Too Old

The webhook timestamp is outside your tolerance window. This could indicate:
  • A replay attack attempt
  • Significant clock skew between your server and Monk
  • A delayed retry being delivered
{
  "error": "Webhook timestamp outside tolerance window"
}

Missing Signature Header

The X-Monk-Signature header is missing. Ensure:
  • You’re receiving the request at the correct endpoint
  • Your proxy/load balancer isn’t stripping headers

Testing Webhooks

Local Development

Use a tunneling service to test webhooks locally:
  1. Start your local server
  2. Create a tunnel: ngrok http 3000
  3. Add the tunnel URL as a webhook endpoint in Monk
  4. Trigger events in your Monk sandbox

Webhook Logs (Coming Soon)

View delivery attempts and responses in Settings → Webhooks → Logs to debug issues.