Verifying webhooks - Grasshopper Labs API
Webhooks

Verifying webhooks

Every webhook delivery includes an HMAC-SHA256 signature so you can verify the request actually came from Grasshopper.

The signature header

Every webhook POST includes an X-Grasshopper-Signature header containing an HMAC-SHA256 hex digest of the raw request body, computed using your webhook secret.

X-Grasshopper-Signature: 7b8e3c2a1d4f5b6c8e9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e
X-Grasshopper-Timestamp: 1715534400
Content-Type: application/json
Get your secret

Your webhook secret is provided during webhook registration. Treat it like an API password. If it's leaked, request a rotation.

Verification code

const crypto = require('crypto');

function verifyWebhook(rawBody, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

// In your Express handler:
app.post('/webhook', (req, res) => {
  const sig = req.headers['x-grasshopper-signature'];
  if (!verifyWebhook(req.rawBody, sig, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  // process webhook...
  res.status(200).send('OK');
});
import hmac, hashlib

def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        raw_body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

# In your Flask/Django handler:
@app.route('/webhook', methods=['POST'])
def webhook():
    sig = request.headers.get('X-Grasshopper-Signature')
    if not verify_webhook(request.get_data(), sig, os.environ['WEBHOOK_SECRET']):
        return 'Invalid signature', 401
    # process webhook...
    return 'OK', 200
function verifyWebhook($rawBody, $signature, $secret) {
    $expected = hash_hmac('sha256', $rawBody, $secret);
    return hash_equals($expected, $signature);
}

// In your handler:
$body = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_GRASSHOPPER_SIGNATURE'];
if (!verifyWebhook($body, $sig, getenv('WEBHOOK_SECRET'))) {
    http_response_code(401);
    exit('Invalid signature');
}
// process webhook...

Use the raw body

Critical: HMAC must be computed against the exact bytes of the request body, before any JSON parsing or middleware re-serialization. If you let your framework parse the body before reading it, the bytes change and the signature will not match.

  • Express: use express.raw() or store req.rawBody before express.json() runs
  • Flask: use request.get_data() (not request.json)
  • Django: use request.body (not parsed forms)

Timestamp check (replay defense)

The X-Grasshopper-Timestamp header contains the Unix timestamp when the signature was computed. Reject requests with a timestamp more than 5 minutes from now to defend against replay attacks.

Use a timing-safe comparison

Never use == or === to compare signatures. A naive comparison leaks timing info that lets attackers learn the correct signature byte by byte. Always use:

  • Node.js: crypto.timingSafeEqual()
  • Python: hmac.compare_digest()
  • PHP: hash_equals()