Verify Webhook Signature
To ensure that the webhooks received by your server truly originate from Gerbang Pay (and not from a malicious actor forging transaction data), we secure every webhook delivery using the HMAC-SHA256 algorithm.
All HTTP Request Body payloads are signed using your unique webhook_secret, and the result is attached to the X-Gerbang-Signature header.
Basic Verification Concepts
- Retrieve the raw string/bytes from the request body (before it is parsed into a JSON object).
- Retrieve the value of the
X-Gerbang-Signatureheader. - Remove the
sha256=prefix from the header; the remainder is the original hash (in hexadecimal format). - On your server side, calculate the HMAC-SHA256 of the raw body using your
webhook_secretkey. - Compare your server's calculated result with the hash from the header. If they match, the webhook is valid. If they differ, reject the webhook (HTTP 401/403).
⚠️ Very Important: You must use the request body in its raw form (raw buffer/string bytes). If your framework (e.g., Express.js) converts the body into a JSON object first, and then stringifies it again, the spacing can change so the HMAC result will never match (always invalid).
Implementation Examples in Various Languages
1. Node.js (Express)
For Express.js users, you need to bypass express.json() specifically for the webhook route so you can access its raw body.
javascript
const express = require('express');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET = 'your_dashboard_secret'; // Store in .env
// Save rawBody specifically for verification
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf;
}
}));
app.post('/webhook/gerbangpay', (req, res) => {
const signatureHeader = req.headers['x-gerbang-signature'];
if (!signatureHeader || !signatureHeader.startsWith('sha256=')) {
return res.status(401).send('Missing or invalid signature header');
}
// Remove the 'sha256=' text
const providedSignature = signatureHeader.replace('sha256=', '');
// Regenerate HMAC
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
hmac.update(req.rawBody);
const calculatedSignature = hmac.digest('hex');
// Compare using constant-time compare to prevent timing attacks
if (crypto.timingSafeEqual(Buffer.from(providedSignature), Buffer.from(calculatedSignature))) {
// Signature is valid!
const event = req.body;
console.log(`Received event: ${event.event_type} for transaction ${event.data.provider_order_id}`);
// TODO: Update status in your database
res.status(200).send('OK');
} else {
// Signature is invalid, reject!
console.error("Webhook signature mismatch!");
res.status(401).send('Invalid signature');
}
});
app.listen(3000, () => console.log('Server running'));2. Python (Flask)
python
from flask import Flask, request, jsonify
import hmac
import hashlib
app = Flask(__name__)
WEBHOOK_SECRET = 'your_dashboard_secret'.encode('utf-8')
@app.route('/webhook/gerbangpay', methods=['POST'])
def webhook_handler():
signature_header = request.headers.get('X-Gerbang-Signature', '')
if not signature_header.startswith('sha256='):
return jsonify({"error": "Invalid signature format"}), 401
provided_signature = signature_header.replace('sha256=', '')
# Get raw body
raw_body = request.get_data()
# Generate HMAC
calculated_signature = hmac.new(
WEBHOOK_SECRET,
msg=raw_body,
digestmod=hashlib.sha256
).hexdigest()
# Compare securely
if hmac.compare_digest(provided_signature, calculated_signature):
event = request.json
print(f"Valid event: {event.get('event_type')}")
return "OK", 200
else:
return "Invalid signature", 401
if __name__ == '__main__':
app.run(port=5000)3. PHP
php
<?php
$webhookSecret = 'your_dashboard_secret';
$signatureHeader = $_SERVER['HTTP_X_GERBANG_SIGNATURE'] ?? '';
if (strpos($signatureHeader, 'sha256=') !== 0) {
http_response_code(401);
die('Invalid signature header');
}
$providedSignature = substr($signatureHeader, 7); // Cut 'sha256='
// Get raw HTTP body
$rawBody = file_get_contents('php://input');
// Calculate HMAC
$calculatedSignature = hash_hmac('sha256', $rawBody, $webhookSecret);
// Use hash_equals to prevent timing attacks
if (hash_equals($calculatedSignature, $providedSignature)) {
// Valid
$event = json_decode($rawBody, true);
// Perform database update...
http_response_code(200);
echo "OK";
} else {
// Invalid
http_response_code(401);
die('Signature verification failed');
}4. Go (Golang)
go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"strings"
)
const WebhookSecret = "your_dashboard_secret"
func webhookHandler(w http.ResponseWriter, r *http.Request) {
signatureHeader := r.Header.Get("X-Gerbang-Signature")
if !strings.HasPrefix(signatureHeader, "sha256=") {
http.Error(w, "Invalid signature header", http.StatusUnauthorized)
return
}
providedSignature := strings.TrimPrefix(signatureHeader, "sha256=")
// Read raw body
rawBody, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Cannot read body", http.StatusInternalServerError)
return
}
defer r.Body.Close()
// Calculate HMAC
mac := hmac.New(sha256.New, []byte(WebhookSecret))
mac.Write(rawBody)
calculatedSignature := hex.EncodeToString(mac.Sum(nil))
// Compare securely
if hmac.Equal([]byte(providedSignature), []byte(calculatedSignature)) {
// Valid
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
} else {
// Invalid
http.Error(w, "Invalid signature", http.StatusUnauthorized)
}
}
func main() {
http.HandleFunc("/webhook/gerbangpay", webhookHandler)
http.ListenAndServe(":8080", nil)
}