HMAC-SHA256
Instant Event Notification
Webhooks & Signature Verification
HivePay sends real-time HTTP POST notifications to your server whenever an important event happens (e.g. when a customer approves a USSD push deposit or when a payout completes).
Event Types
| Event | Trigger Condition |
|---|---|
| transaction.success | Customer approved collection prompt or a payout was delivered successfully. |
| transaction.failed | Customer entered incorrect PIN, prompt timed out, or payout was rejected by carrier. |
Signature Header: X-HivePay-Signature
Every webhook request dispatched by HivePay includes an X-HivePay-Signature header with two key components:
X-HivePay-Signature: t=1726245000,v=3a7b9c48f2e18d6...
t: Unix epoch timestamp when the webhook was sent. Used to reject replay attacks older than 5 minutes.v: The hexadecimal HMAC-SHA256 signature calculated fromt + "." + rawBodyusing your secret key.
Verification Code Examples
Select your language below to view a complete, production-ready webhook signature verification handler:
<?php
function verifyHivePayWebhook($payloadString, $signatureHeader, $webhookSecret) {
// 1. Parse t and v from header: t=1726245000,v=3a7b9c...
$parts = explode(',', $signatureHeader);
$timestamp = explode('=', $parts[0] ?? '')[1] ?? '';
$receivedSig = explode('=', $parts[1] ?? '')[1] ?? '';
// 2. Protect against replay attacks (ignore requests older than 5 mins)
if (abs(time() - (int)$timestamp) > 300) {
return false;
}
// 3. Compute expected HMAC-SHA256 signature
$signedPayload = $timestamp . '.' . $payloadString;
$expectedSig = hash_hmac('sha256', $signedPayload, $webhookSecret);
// 4. Secure constant-time string comparison
return hash_equals($expectedSig, $receivedSig);
}
// Controller entry point:
$rawPayload = file_get_contents('php://input');
$signatureHeader = $_SERVER['HTTP_X_HIVEPAY_SIGNATURE'] ?? '';
$secret = getenv('HIVEPAY_WEBHOOK_SECRET');
if (verifyHivePayWebhook($rawPayload, $signatureHeader, $secret)) {
$event = json_decode($rawPayload, true);
if ($event['status'] === 'success') {
// Mark customer order as paid
$orderId = $event['reference'];
}
http_response_code(200);
echo json_encode(['received' => true]);
} else {
http_response_code(400);
echo 'Invalid signature';
}
const express = require('express');
const crypto = require('crypto');
const app = express();
// Use express.raw to preserve exact payload bytes for signature calculation
app.post('/webhook/hivepay', express.raw({ type: 'application/json' }), (req, res) => {
const signatureHeader = req.headers['x-hivepay-signature'];
const webhookSecret = process.env.HIVEPAY_WEBHOOK_SECRET;
if (!signatureHeader) return res.status(400).send('Missing signature header');
const [tPart, vPart] = signatureHeader.split(',');
const timestamp = tPart.split('=')[1];
const receivedSig = vPart.split('=')[1];
// Prevent replay attacks (5 minute threshold)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
return res.status(400).send('Webhook timestamp expired');
}
const rawBody = req.body.toString('utf8');
const expectedSig = crypto
.createHmac('sha256', webhookSecret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
if (crypto.timingSafeEqual(Buffer.from(receivedSig), Buffer.from(expectedSig))) {
const event = JSON.parse(rawBody);
console.log('Payment verified! Event:', event.reference, event.status);
res.status(200).json({ received: true });
} else {
res.status(400).send('Signature mismatch');
}
});
import hmac
import hashlib
import time
import os
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhook/hivepay', methods=['POST'])
def handle_webhook():
sig_header = request.headers.get('X-HivePay-Signature', '')
secret = os.getenv('HIVEPAY_WEBHOOK_SECRET', '')
if not sig_header:
return "Missing signature header", 400
parts = dict(x.split('=') for x in sig_header.split(','))
timestamp = int(parts.get('t', 0))
received_sig = parts.get('v', '')
# Replay attack protection (5 minutes)
if abs(time.time() - timestamp) > 300:
return "Expired signature timestamp", 400
raw_body = request.get_data()
signed_payload = f"{timestamp}.".encode('utf-8') + raw_body
expected_sig = hmac.new(secret.encode('utf-8'), signed_payload, hashlib.sha256).hexdigest()
if hmac.compare_digest(expected_sig, received_sig):
event = request.get_json()
print("Payment confirmed:", event['reference'], event['status'])
return jsonify(received=True), 200
return "Invalid signature", 400