Verify a webhook
title: "Verify a webhook" description: "Validate the BchainPay-Signature header before trusting a webhook payload." section: "Webhooks" order: 2 updated: "2026-04-18" sourcePath: "content/docs/webhooks/verify.mdx"
Every BchainPay webhook is signed with HMAC-SHA256 using your per-merchant secret. Verify the signature on every request before acting on the payload.
The signature header
BchainPay-Signature: t=1714397200,v1=8b0a...e3t— Unix timestamp when we signed the request.v1— Hex-encoded HMAC-SHA256 of${t}.${rawBody}using your secret.
Verification recipe
import crypto from "node:crypto";
export function verify(rawBody: string, header: string, secret: string, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Replay protection
Reject any request whose t is more than 5 minutes from the current time. We retry failed deliveries with exponential backoff for 30 days, but each retry carries a fresh signature.
What if I miss a webhook?
Pull the latest state from GET /v1/payment-intents/:id. The API is the source of truth; webhooks are an optimization.