How It Works

  1. 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.
  2. Read Raw Payload: Read the exact, unparsed request body string/bytes.
  3. Compute Local Hash: Generate an HMAC-SHA512 hash using your OneKhusa webhook secret and the raw payload body.
  4. Timing-Safe Comparison: Compare your computed signature against the header signature using a fixed-time equality check to protect against timing attacks.
  5. 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:
  1. Log in to the OneKhusa Developer Portal.
  2. Navigate to Developers → Webhooks.
  3. Select Manage for the webhook you want to verify.
  4. Select View.
  5. 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.