← All Posts
Security

Webhook Security 101: Verifying Signed Payloads from Paychainly

May 21, 2026· 1 min read

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 OK within 25 seconds or Paychainly will retry.
← Back to Blog
webhooksHMACsecuritysignature verificationNode.js