Skip to main content

Webhook Signature Verification

This guide is for receivers of Paysense webhooks. It explains how to verify that a delivery genuinely came from Paysense and has not been tampered with or replayed.

Paysense follows the same signing approach as Stripe and GitHub: the delivery carries a timestamp and an HMAC-SHA256 signature computed over the timestamp and the raw request body together.

Prerequisites​

ItemDescription
Webhook subscriptionA subscription configured against your business, with your endpoint URL registered.
Subscription secretThe Secret configured on your webhook subscription. This is the signing key. Signatures are only sent when a secret is configured.
Raw request bodyYour endpoint must be able to read the exact bytes of the request body, before any re-serialisation or parsing.

Headers sent with every delivery​

HeaderDescription
X-Webhook-TimestampUnix time in seconds when the delivery was signed. Sent on every delivery and generated fresh on every attempt, including retries.
X-Webhook-Signature-256sha256=<hex> — HMAC-SHA256 over "{timestamp}.{rawBody}" using your subscription secret. Present when the subscription has a secret configured. Verify this.
X-Webhook-EventThe event type.
X-Webhook-Entity-IdThe ID of the entity the event relates to.
X-Webhook-Subscription-IdThe subscription that produced this delivery.
X-Webhook-Business-IdThe business the event belongs to.
X-Webhook-Log-IdA per-attempt delivery log ID. It changes on every attempt — do not use it for deduplication.
note

The signing secret is the Secret configured on your webhook subscription. If no secret is configured, X-Webhook-Signature-256 is not sent and deliveries cannot be verified.

How to verify​

  1. Read the raw body exactly as received — do not re-serialise or pretty-print it. The signature is computed over the exact bytes sent.
  2. Read X-Webhook-Timestamp and X-Webhook-Signature-256.
  3. Check freshness. Reject the request if |now - timestamp| is more than your tolerance (300 seconds / 5 minutes is the usual choice). This rejects replayed old requests. The tolerance also absorbs small clock differences between your server and Paysense.
  4. Recompute the signature: HMAC-SHA256(secret, "{timestamp}.{rawBody}"), hex-encode it, and prefix sha256=.
  5. Compare in constant time against X-Webhook-Signature-256. Never use a plain string equality check — use a constant-time comparison to avoid timing attacks.

Example (C#)​

// tsHeader     = X-Webhook-Timestamp
// sigHeader = X-Webhook-Signature-256
// rawBodyBytes = the exact bytes of the request body
// secret = your subscription secret

if (!long.TryParse(tsHeader, out var ts) ||
Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ts) > 300)
return Unauthorized(); // stale or missing timestamp

var signed = Encoding.UTF8.GetBytes($"{ts}.").Concat(rawBodyBytes).ToArray();
var expected = "sha256=" + Convert.ToHexString(
HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), signed)).ToLowerInvariant();

if (!CryptographicOperations.FixedTimeEquals(
Encoding.ASCII.GetBytes(expected), Encoding.ASCII.GetBytes(sigHeader)))
return Unauthorized(); // signature mismatch

Example (Node.js)​

const crypto = require("crypto");

function verify(rawBody, tsHeader, sigHeader, secret) {
const ts = Number(tsHeader);
if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false;

const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");

const a = Buffer.from(expected);
const b = Buffer.from(sigHeader ?? "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Security model​

  • Verify every delivery. Reject any request whose signature is missing, stale, or does not match. An unverified request must not be processed.
  • Keep the secret server-side. The subscription secret is the signing key. Never expose it to browser or mobile code — anyone holding it can forge deliveries.
  • Constant-time comparison only. A plain equality check leaks timing information that can be used to recover a valid signature.
  • Sign over raw bytes. Verification must run against the exact body received. Re-serialising the payload changes the bytes and breaks the signature.