Singapay Home Page
Logo Icon
  1. How to Validate Signature

How to Validate Signature

All webhook requests from Singa Payment Gateway include an X-Signature header generated using HMAC-SHA512. To verify that a request is authentic and has not been tampered with, reconstruct the signature on your side and compare it with the received value.


Signature Algorithm

StringToSign  = METHOD + ":" + ENDPOINT + ":" + ACCESS_TOKEN + ":" + SHA256(normalizedBody) + ":" + TIMESTAMP
Signature     = HMAC_SHA512(StringToSign, clientSecret)

Components

ComponentSourceDescription
METHODFixedAlways POST for webhooks
ENDPOINTYour webhook URL pathPath + query string, e.g. /webhook/va-transaction
ACCESS_TOKENAuthorization headerValue after Bearer prefix
SHA256(normalizedBody)Request bodySHA-256 hash of the JSON body after recursive key sorting
TIMESTAMPX-Timestamp headerUnix timestamp in seconds
clientSecretMerchant dashboardYour Client Secret (HMAC key)

Validation Steps

  1. Extract headers from the incoming request:

    • X-Signature — the signature to validate
    • X-Timestamp — Unix timestamp in seconds
    • Authorization — extract the token after Bearer
  2. Normalize the JSON body:

    • Parse the raw JSON into an object/array
    • Sort all keys recursively in ascending alphabetical order
    • Re-encode to JSON string (with unescaped unicode and slashes)
  3. Hash the normalized body with SHA-256 (hex digest)

  4. Build the string to sign:

    StringToSign = POST:/your/endpoint:accessToken:hashedBody:timestamp
    
  5. Compute HMAC-SHA512 of the string to sign using your clientSecret

  6. Compare the computed signature with X-Signature using a constant-time comparison


Example (PHP)

<?php
function validateWebhookSignature($requestBody, $headers, $clientSecret, $endpoint) {
    $receivedSignature = $headers['X-Signature'] ?? '';
    $timestamp = $headers['X-Timestamp'] ?? '';
    $authorization = $headers['Authorization'] ?? '';
    $accessToken = str_replace('Bearer ', '', $authorization);

    $bodyArray = json_decode($requestBody, true);
    if (json_last_error() !== JSON_ERROR_NONE) {
        return false;
    }

    // Sort keys recursively
    function sortRecursive(&$array) {
        ksort($array, SORT_STRING);
        foreach ($array as &$value) {
            if (is_array($value)) {
                sortRecursive($value);
            }
        }
    }
    sortRecursive($bodyArray);

    $normalizedJson = json_encode($bodyArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    $hashedBody = hash('sha256', $normalizedJson);

    $stringToSign = "POST:{$endpoint}:{$accessToken}:{$hashedBody}:{$timestamp}";
    $calculatedSignature = hash_hmac('sha512', $stringToSign, $clientSecret);

    return hash_equals($calculatedSignature, $receivedSignature);
}
?>

Formula Summary

hashedBody        = SHA256( json_encode( sortKeysRecursive( json_decode(body) ) ) )
stringToSign      = "POST" + ":" + endpoint + ":" + accessToken + ":" + hashedBody + ":" + timestamp
expectedSignature = HMAC_SHA512(stringToSign, clientSecret)

For complete implementation examples in PHP, Python, and Node.js, see the “How to Validate Signature” section on any individual webhook page (e.g. VA Transaction, Disbursement).