Never process a payment without verifying the webhook signature. This guide shows exactly how to do HMAC-SHA256 verification in Node.js, Python, PHP, and Go.
Every webhook Bekena sends includes an X-Bekena-Sig header: an HMAC-SHA256 signature computed from the raw request body using your webhook secret. Verifying this signature before processing any payment event is not optional — it's what prevents an attacker from sending fake payment.confirmed events to your endpoint and fraudulently getting your products delivered.
How the Signature Works
Bekena computes: HMAC-SHA256(rawBody, webhookSecret) and sends the hex digest in X-Bekena-Sig. Your code must compute the same HMAC using your webhook secret (from the Bekena dashboard) and compare it to the header. If they match, the webhook is authentic.
Note: Critical: Use the raw request body bytes for HMAC computation, not a parsed JSON object. Parsing changes whitespace and key ordering, producing a different hash.
Node.js / TypeScript
typescript
import crypto from 'crypto';
function verifyWebhook(rawBody: Buffer, signature: string, secret: string): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
// Use timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(signature, 'hex')
);
}
// Express middleware
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-bekena-sig'] as string;
if (!verifyWebhook(req.body, sig, process.env.BEKENA_WEBHOOK_SECRET!)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body.toString());
// process event...
res.json({ received: true });
});
Python
python
import hmac
import hashlib
def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode('utf-8'),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
# Flask example
@app.route('/webhook', methods=['POST'])
def webhook():
sig = request.headers.get('X-Bekena-Sig', '')
if not verify_webhook(request.get_data(), sig, WEBHOOK_SECRET):
return jsonify(error='Invalid signature'), 401
event = request.get_json()
# process event...
return jsonify(received=True)
PHP
php
function verifyWebhook(string $rawBody, string $signature, string $secret): bool {
$expected = hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signature);
}
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_BEKENA_SIG'] ?? '';
if (!verifyWebhook($rawBody, $signature, $_ENV['BEKENA_WEBHOOK_SECRET'])) {
http_response_code(401);
exit('Invalid signature');
}
$event = json_decode($rawBody, true);
// process $event...
Common Mistakes to Avoid
- ✓Using string comparison (==) instead of constant-time comparison — always use timingSafeEqual or equivalent
- ✓Parsing JSON before computing HMAC — always use the raw bytes
- ✓Not checking the webhook secret is set in your environment — fail loudly if missing
- ✓Ignoring the event type — check event.type before processing
- ✓Not returning 200 immediately — process webhooks async and respond fast to prevent retries
Idempotency: Handle Duplicate Deliveries
Bekena retries failed webhook deliveries up to 10 times with exponential backoff. Your handler must be idempotent — processing the same payment.confirmed event twice should be safe. Use the event.data.order_id as an idempotency key and skip processing if the order is already marked as paid.