Documentation Index
Fetch the complete documentation index at: https://docs.monk.com/llms.txt
Use this file to discover all available pages before exploring further.
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.