Signature Verification
Verify that webhook payloads are genuinely from GriffNode
Every webhook request includes an X-GriffNode-Signature header containing an HMAC-SHA256 signature of the raw request body, signed with your webhook secret. Always verify this before processing the event.
Never skip signature verification.
Without it, anyone can send fake events to your endpoint. Always verify before acting on a webhook.
How It Works
- GriffNode computes
HMAC-SHA256(webhook_secret, raw_request_body) - The hex digest is sent as
X-GriffNode-Signature: sha256=<hex> - Your server recomputes the same signature and compares
Headers Sent on Every Webhook
| Header | Example | Description |
|---|---|---|
X-GriffNode-Signature | sha256=a3f9b2... | HMAC-SHA256 of raw body |
X-GriffNode-Event | payment.completed | Event type |
X-Webhook-ID | WHK-A1B2C3D4 | Your webhook endpoint ID |
Content-Type | application/json | Always JSON |
Verification — Node.js
Node.js (Express)
const crypto = require('crypto');
function verifyWebhookSignature(rawBody, signatureHeader, secret) {
// signatureHeader is: "sha256=abc123..."
const received = signatureHeader.replace('sha256=', '');
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody) // rawBody must be Buffer or raw string, NOT parsed JSON
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(received, 'hex'),
Buffer.from(expected, 'hex')
);
}
// Express example — use express.raw() to preserve the raw body
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-griffnode-signature'];
const secret = process.env.WEBHOOK_SECRET;
if (!verifyWebhookSignature(req.body, signature, secret)) {
return res.status(400).send('Invalid signature');
}
const event = JSON.parse(req.body);
console.log('Event:', event.event, 'TxID:', event.transaction_id);
// Handle the event
switch (event.event) {
case 'payment.completed':
fulfillOrder(event.transaction_id);
break;
case 'payment.expired':
cancelOrder(event.transaction_id);
break;
}
res.status(200).json({ received: true });
});
Verification — Python
Python (Flask)
import hmac
import hashlib
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = 'your_webhook_secret'
def verify_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
received = signature_header.replace('sha256=', '')
expected = hmac.new(
secret.encode('utf-8'),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(received, expected)
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-GriffNode-Signature', '')
raw_body = request.get_data() # raw bytes, before parsing
if not verify_signature(raw_body, signature, WEBHOOK_SECRET):
return jsonify({'error': 'Invalid signature'}), 400
event = request.json
print(f"Event: {event['event']}, TxID: {event['transaction_id']}")
if event['event'] == 'payment.completed':
fulfill_order(event['transaction_id'])
elif event['event'] == 'payment.expired':
cancel_order(event['transaction_id'])
return jsonify({'received': True}), 200
Common Mistakes
Use the raw request body, not the parsed JSON. Parsing and re-serializing changes whitespace and key ordering, producing a different signature.
Responding to Webhooks
Your endpoint must return a 2xx status code within 15 seconds. Return 200 as quickly as possible — do any slow processing asynchronously. If a 2xx is not received, the delivery is logged as failed.