Webhooks

Verifying signatures

Every webhook is signed with HMAC-SHA256. Verify before processing.

Headers

HeaderValue
X-Harpoon-Signaturesha256=<hex>
X-Harpoon-TimestampUnix timestamp at send time
X-Harpoon-Webhook-IDDelivery ID (also in the body as webhook_id)

Algorithm

  1. Reject if timestamp is more than 5 minutes from now.
  2. Build <timestamp>.<raw_body>.
  3. Compute HMAC-SHA256(signed_payload, secret) and hex-encode.
  4. Compare in constant time against the value in X-Harpoon-Signature (after stripping sha256=).

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