Why Signature Verification Matters
Without signature verification, any attacker who knows your webhook URL can POST a fake deposit_detected event and receive goods without paying. Signature verification proves the payload came from Paychainly.
How Paychainly Signs Webhooks
The signature is computed as:
HMAC-SHA256(secret, "event|txHash|from|to|amount|blockNumber|timestamp|userId")
The header is X-Paychainly-Signature: sha256=<hex>.
Node.js / TypeScript
import crypto from 'crypto';
export function verifyWebhook(
rawBody: string,
signature: string,
secret: string,
): boolean {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature),
);
}
Python
import hmac, hashlib
def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
PHP
function verifyWebhook(string $rawBody, string $signature, string $secret): bool {
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signature);
}
Go
func VerifyWebhook(body []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}
Best Practices
- Always verify before updating order status.
- Store raw request body — parsed JSON byte order may differ.
- Rotate webhook secrets periodically via dashboard.
- Respond with
200 OKwithin 25 seconds or Paychainly will retry.