Every request sent by Zafepay to your webhook URL includes a signature header that allows you to verify the request is authentic and that the payload has not been tampered with.
Signature Header
X-Zafepay-Signature: sha256=
The signature is computed using HMAC-SHA256 over the raw request body, using the webhook's secret as the key. The secret is provided once when the webhook is created and is not retrievable afterwards.
Important: Store your webhook secret securely. If it is lost, you will need to delete and recreate the webhook to obtain a new one.
Verification Steps
- Read the raw request body as bytes — do not parse or re-serialize it first.
- Compute
HMAC-SHA256(secret, raw_body)and hex-encode the result. - Prepend
sha256=to the result. - Compare it to the value in the
X-Zafepay-Signatureheader using a constant-time comparison to prevent timing attacks. - If they match, the request is authentic. If not, reject it with a
401or403.
Code Examples
Important Notes
- Always use the raw request body for verification. Parsing the JSON and re-serializing it may produce a different byte sequence and cause verification to fail.
- Always use a constant-time comparison (timingSafeEqual, compare_digest, hash_equals). A regular string equality check (==) is vulnerable to timing attacks.
const crypto = require('crypto');
function verifyWebhook(rawBody, secret, signatureHeader) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader)
);
}
import hmac
import hashlib
def verify_webhook(raw_body: bytes, secret: str, signature_header: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)
function verifyWebhook(string $rawBody, string $secret, string $signatureHeader): bool {
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signatureHeader);
}