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
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 storereq.rawBodybeforeexpress.json()runs - Flask: use
request.get_data()(notrequest.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()