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
| Part | Description |
|---|
t | Unix timestamp (seconds) when the signature was generated |
v1 | HMAC-SHA256 signature |
The signature is computed over:
{timestamp}.{JSON payload}
Verification Steps
- Extract
t (timestamp) and v1 (signature) from the header
- Check that the timestamp is within your tolerance window (e.g., 5 minutes)
- Compute the expected signature:
HMAC-SHA256(secret, "{t}.{payload}")
- 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"
}
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:
- Start your local server
- Create a tunnel:
ngrok http 3000
- Add the tunnel URL as a webhook endpoint in Monk
- Trigger events in your Monk sandbox
Webhook Logs (Coming Soon)
View delivery attempts and responses in Settings → Webhooks → Logs to debug issues.