Tranzak Docs
tranzak.co Dashboard
Docs  /  Webhooks

Verify the signature

Verify the HMAC-SHA256 signature of every Tranzak webhook on the raw request body, with examples in PHP, Node.js, Python, Go, Ruby and Java.

Every webhook carries an X-Tranzak-Signature header. Always verify it before you trust the request.

How the signature is built

signature = hex( HMAC-SHA256( key = TRANZAK_WEBHOOK_SECRET, message = the RAW request body bytes ) )
  • The key is the webhook secret of your website (whsec_…).
  • The message is the raw body, exactly as received. Do not parse and re-serialise the JSON before you compute the HMAC.
  • Compare with a constant-time function. Reject the request with 403 if it does not match, then parse the JSON.

Warning If your framework parses the body before your handler runs, make sure you can still read the raw bytes. In Express, use express.raw({ type: 'application/json' }) on the webhook route.

Examples

$raw = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_TRANZAK_SIGNATURE'] ?? '';
$ok = hash_equals(hash_hmac('sha256', $raw, $secret), $signature);
const crypto = require('crypto');

// rawBody is a Buffer (Express: express.raw({ type: 'application/json' }))
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const ok = signature.length === expected.length &&
  crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
import hmac, hashlib

ok = hmac.compare_digest(
    hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest(),
    signature,
)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(rawBody)
ok := hmac.Equal([]byte(hex.EncodeToString(mac.Sum(nil))), []byte(signature))
ok = Rack::Utils.secure_compare(
  OpenSSL::HMAC.hexdigest('SHA256', secret, raw_body),
  signature
)
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(UTF_8), "HmacSHA256"));
boolean ok = MessageDigest.isEqual(
    HexFormat.of().formatHex(mac.doFinal(rawBody)).getBytes(UTF_8),
    signature.getBytes(UTF_8));

After the signature check

A valid signature proves the request came from Tranzak. Then:

  1. Parse the JSON.
  2. Ignore the event if you already processed this data.transaction_id for this event.
  3. Optionally read the payment again (GET /payments/{transaction_id}) and compare the amount, currency and reference with your order.
  4. Update your order in an idempotent way and answer 200.

Troubleshooting

Symptom Likely cause
The signature never matches The body was parsed and re-serialised, or an extra newline or whitespace was added. Use the raw bytes.
It matches in tests but not in production The production secret differs from the test one. Each website has its own webhook secret.
No webhook arrives The URL is not public https://, your endpoint answers non-2xx, or the payment is a MonCash test payment, which fires no sandbox webhook.