How It Works
-
Extract Headers: Read the incoming webhook signature (
X-OneKhusa-Webhook-Signature) and webhook event code (OneKhusa-Webhook-Event) from the request headers. Webhook signature supports HMAC-SHA512 for payload hashing.
-
Read Raw Payload: Read the exact, unparsed request body string/bytes.
-
Compute Local Hash: Generate an HMAC-SHA512 hash using your OneKhusa webhook secret and the raw payload body.
-
Timing-Safe Comparison: Compare your computed signature against the header signature using a fixed-time equality check to protect against timing attacks.
-
Payload Check: When the request payload is validated and verified to be originating from OneKhusa, proceed to finalise the transaction in your system.
Find Webhook Secret
To find your webhook secret:
-
Log in to the OneKhusa Developer Portal.
-
Navigate to Developers → Webhooks.
-
Select Manage for the webhook you want to verify.
-
Select View.
-
The Webhook Secret will be displayed in the webhook details.
Important: Keep the webhook secret secure. Do not expose it in client-side applications, source code repositories, or logs.
Implementation Details
This section provides sample code for C#, PHP and JavaScript on how to programmatically verify webhook notifications locally.
1. C# (.NET Core)
Use HMACSHA512 alongside CryptographicOperations.FixedTimeEquals for constant-time comparison.
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
public static class WebhookSecurity
{
/// <summary>
/// Computes an HMAC-SHA512 signature for a payload string.
/// </summary>
public static string ComputeSignature(string payload, string secret)
{
byte[] secretBytes = Encoding.UTF8.GetBytes(secret);
byte[] payloadBytes = Encoding.UTF8.GetBytes(payload);
using (var hmac = new HMACSHA512(secretBytes))
{
byte[] hashBytes = hmac.ComputeHash(payloadBytes);
return Convert.ToHexString(hashBytes).ToLowerInvariant();
}
}
/// <summary>
/// Verifies an incoming HMAC-SHA512 header signature against the raw payload string.
/// </summary>
public static bool VerifySignature(string payload, string secret, string headerSignature)
{
if (string.IsNullOrEmpty(payload) || string.IsNullOrEmpty(secret) || string.IsNullOrEmpty(headerSignature))
return false;
string computedSignature = ComputeSignature(payload, secret);
byte[] computedBytes = Encoding.UTF8.GetBytes(computedSignature);
byte[] headerBytes = Encoding.UTF8.GetBytes(headerSignature.ToLowerInvariant());
return CryptographicOperations.FixedTimeEquals(computedBytes, headerBytes);
}
}
2. PHP
Use hash_hmac() to compute the hash and hash_equals() for timing-safe comparison.
<?php
class WebhookSecurity
{
/**
* Computes an HMAC-SHA512 signature.
*/
public static function computeSignature(string $payload, string $secret): string
{
return hash_hmac('sha512', $payload, $secret);
}
/**
* Verifies the signature using a timing-safe comparison.
*/
public static function verifySignature(string $payload, string $secret, string $headerSignature): bool
{
if (empty($payload) || empty($secret) || empty($headerSignature)) {
return false;
}
$computedSignature = self::computeSignature($payload, $secret);
return hash_equals($computedSignature, strtolower($headerSignature));
}
}
?>
3. JavaScript (Node.js / Express)
Use crypto.createHmac() and crypto.timingSafeEqual() with Buffer instances.
const crypto = require('crypto');
class WebhookSecurity {
/**
* Computes an HMAC-SHA512 signature.
*/
static computeSignature(payload, secret) {
return crypto
.createHmac('sha512', secret)
.update(payload, 'utf8')
.digest('hex');
}
/**
* Verifies the signature using timing-safe buffer comparison.
*/
static verifySignature(payload, secret, headerSignature) {
if (!payload || !secret || !headerSignature) {
return false;
}
const computedSignature = this.computeSignature(payload, secret);
const computedBuffer = Buffer.from(computedSignature, 'utf8');
const headerBuffer = Buffer.from(headerSignature.toLowerCase(), 'utf8');
if (computedBuffer.length !== headerBuffer.length) {
return false;
}
return crypto.timingSafeEqual(computedBuffer, headerBuffer);
}
}
module.exports = WebhookSecurity;
Crucial Rules for Local Verification
⚠️ Always read the raw request body string prior to JSON parsing. Re-serializing parsed JSON alters whitespace or field order, causing signature checks to fail.
⚠️ Always use constant-time string equality functions (FixedTimeEquals, hash_equals, or timingSafeEqual) to prevent timing attacks.