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
403if 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:
- Parse the JSON.
- Ignore the event if you already processed this
data.transaction_idfor thisevent. - Optionally read the payment again (
GET /payments/{transaction_id}) and compare the amount, currency and reference with your order. - 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. |