Webhooks
Verifying signatures
Every webhook is signed with HMAC-SHA256. Verify before processing.
Headers
| Header | Value |
|---|---|
X-Harpoon-Signature | sha256=<hex> |
X-Harpoon-Timestamp | Unix timestamp at send time |
X-Harpoon-Webhook-ID | Delivery ID (also in the body as webhook_id) |
Algorithm
- Reject if
timestampis more than 5 minutes from now. - Build
<timestamp>.<raw_body>. - Compute
HMAC-SHA256(signed_payload, secret)and hex-encode. - Compare in constant time against the value in
X-Harpoon-Signature(after strippingsha256=).
Node.js
JavaScript
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, timestamp, secret) {
if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > 300) {
throw new Error('Webhook timestamp too old');
}
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${payload}`)
.digest('hex');
const received = signature.replace('sha256=', '');
if (!crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected))) {
throw new Error('Invalid webhook signature');
}
return true;
}
app.post('/webhooks/harpoon', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-harpoon-signature'];
const timestamp = parseInt(req.headers['x-harpoon-timestamp'], 10);
const payload = req.body.toString();
verifyWebhookSignature(payload, signature, timestamp, process.env.WEBHOOK_SECRET);
// process event
});Python
Python
import hmac, hashlib, time
def verify_webhook_signature(payload: str, signature: str, timestamp: int, secret: str) -> bool:
if abs(int(time.time()) - timestamp) > 300:
raise ValueError('Webhook timestamp too old')
expected = hmac.new(
secret.encode(),
f"{timestamp}.{payload}".encode(),
hashlib.sha256,
).hexdigest()
received = signature.replace('sha256=', '')
if not hmac.compare_digest(received, expected):
raise ValueError('Invalid webhook signature')
return True