When the gateway sends a webhook to your endpoint, it signs the request using its private key. You must verify the X-SIGNATURE header using the gateway's public key (provided during onboarding) to confirm the request is authentic.
How It Works
Webhook signature verification is the inverse of Signature Generation:
| Step | Generating (your API calls) | Verifying (incoming webhooks) |
|---|---|---|
| Key used | Your private key | The gateway's public key |
| Action | Sign the request | Verify the signature |
| Purpose | Prove the request is from you | Confirm the request is from the gateway |
Verification Algorithm
- Extract the three headers from the incoming request:
X-SIGNATURE,X-TIMESTAMP,X-PARTNER-ID - Read the raw request body
- Compute:
SHA256(body)→ lowercase hex string - Build:
stringToVerify = POST:{webhook_path}:{payload_hash}:{X-TIMESTAMP} - Verify the signature using the gateway's public key with
RSA-SHA256
Code Examples
<?php
function verifyWebhookSignature(
string $webhookPath,
string $rawBody,
string $timestamp,
string $receivedSignature,
string $publicKeyPath
): bool {
// Step 1: Hash the raw request body
$payloadHash = strtolower(bin2hex(hash('sha256', $rawBody, true)));
// Step 2: Build the string to verify
$stringToVerify = "POST:{$webhookPath}:{$payloadHash}:{$timestamp}";
// Step 3: Load the gateway's public key
$publicKey = openssl_get_publickey('file://' . $publicKeyPath);
if (!$publicKey) {
throw new RuntimeException('Failed to load public key');
}
// Step 4: Verify the signature
$result = openssl_verify(
$stringToVerify,
base64_decode($receivedSignature),
$publicKey,
OPENSSL_ALGO_SHA256
);
return $result === 1;
}
// Usage in your webhook handler:
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$partnerId = $_SERVER['HTTP_X_PARTNER_ID'] ?? '';
// Validate partner ID matches your client key
if ($partnerId !== 'your-client-key') {
http_response_code(401);
exit('Invalid partner ID');
}
// Verify signature
$isValid = verifyWebhookSignature(
'/api/webhooks/payment', // Your webhook endpoint path
$rawBody,
$timestamp,
$signature,
'/path/to/gateway-public-key.pem'
);
if (!$isValid) {
http_response_code(401);
exit('Invalid signature');
}
// Signature is valid — process the webhook
$payload = json_decode($rawBody, true);
$event = $payload['event'];
$data = $payload['data'];
// Handle the event...
http_response_code(200);
echo 'OK';
const crypto = require('crypto');
const fs = require('fs');
function verifyWebhookSignature(webhookPath, rawBody, timestamp, receivedSignature, publicKeyPath) {
// Step 1: Hash the raw request body
const payloadHash = crypto.createHash('sha256').update(rawBody).digest('hex').toLowerCase();
// Step 2: Build the string to verify
const stringToVerify = `POST:${webhookPath}:${payloadHash}:${timestamp}`;
// Step 3: Load the gateway's public key
const publicKey = fs.readFileSync(publicKeyPath);
// Step 4: Verify the signature
const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(stringToVerify);
return verifier.verify(publicKey, receivedSignature, 'base64');
}
// Express.js example:
const express = require('express');
const app = express();
app.post('/api/webhooks/payment', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-signature'];
const timestamp = req.headers['x-timestamp'];
const partnerId = req.headers['x-partner-id'];
// Validate partner ID
if (partnerId !== 'your-client-key') {
return res.status(401).send('Invalid partner ID');
}
// Verify signature
const isValid = verifyWebhookSignature(
'/api/webhooks/payment',
req.body, // raw Buffer when using express.raw()
timestamp,
signature,
'./gateway-public-key.pem'
);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
// Signature is valid — process the webhook
const payload = JSON.parse(req.body);
console.log('Event:', payload.event);
console.log('Data:', payload.data);
res.status(200).send('OK');
});
import hashlib
import base64
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
def verify_webhook_signature(
webhook_path: str,
raw_body: bytes,
timestamp: str,
received_signature: str,
public_key_path: str
) -> bool:
# Step 1: Hash the raw request body
payload_hash = hashlib.sha256(raw_body).hexdigest().lower()
# Step 2: Build the string to verify
string_to_verify = f"POST:{webhook_path}:{payload_hash}:{timestamp}"
# Step 3: Load the gateway's public key
with open(public_key_path, "rb") as key_file:
public_key = serialization.load_pem_public_key(key_file.read())
# Step 4: Verify the signature
try:
public_key.verify(
base64.b64decode(received_signature),
string_to_verify.encode(),
padding.PKCS1v15(),
hashes.SHA256()
)
return True
except Exception:
return False
# Flask example:
from flask import Flask, request
app = Flask(__name__)
@app.route('/api/webhooks/payment', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-SIGNATURE', '')
timestamp = request.headers.get('X-TIMESTAMP', '')
partner_id = request.headers.get('X-PARTNER-ID', '')
if partner_id != 'your-client-key':
return 'Invalid partner ID', 401
is_valid = verify_webhook_signature(
'/api/webhooks/payment',
request.get_data(),
timestamp,
signature,
'./gateway-public-key.pem'
)
if not is_valid:
return 'Invalid signature', 401
payload = request.get_json()
print(f"Event: {payload['event']}")
return 'OK', 200
Security Checklist
- Verify
X-SIGNATUREon every incoming webhook using the gateway's public key - Validate
X-PARTNER-IDmatches your client key - Check
X-TIMESTAMPfreshness — reject requests older than 5 minutes to prevent replay attacks - Use HTTPS for your webhook endpoint
- Read the raw body for signature verification before parsing JSON — middleware that parses/re-serializes the body can alter it and break verification