Webhooks
Signature Verification
Verify webhook signatures before processing events.
Every webhook includes an X-Halfin-Signature header. Always verify this signature before processing the event.
Signature format
X-Halfin-Signature: t=1735689900,v1=5d41402abc4b2a76b9719d911017c592| Part | Description |
|---|---|
t | Unix timestamp in seconds |
v1 | HMAC-SHA256 hex digest |
Algorithm
HMAC-SHA256(webhook_secret, "{timestamp}.{raw_body}")- Extract
tandv1from the header. - Compute
expected = HMAC-SHA256(secret, "{t}.{raw_body}"). - Compare
expectedwithv1using constant-time comparison. - Reject if
|now - t| > 300seconds.
With the SDK
import { verifySignature } from '@halfin/sdk-merchant';
const isValid = verifySignature({
signature: req.headers['x-halfin-signature'] as string,
body: rawBody,
secret: process.env.HALFIN_WEBHOOK_SECRET!,
toleranceSeconds: 300,
});Raw Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(signatureHeader: string, rawBody: Buffer, secret: string): boolean {
const parts = Object.fromEntries(
signatureHeader.split(',').map((part) => {
const [key, ...value] = part.split('=');
return [key, value.join('=')];
}),
);
const timestamp = parts.t;
const provided = parts.v1;
if (!timestamp || !provided) return false;
if (!/^\d+$/.test(timestamp) || !/^[0-9a-f]{64}$/i.test(provided)) return false;
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (age > 300) return false;
const expected = createHmac('sha256', secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest('hex');
return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(provided, 'hex'));
}Common mistakes
| Mistake | Fix |
|---|---|
| Parsing JSON before computing HMAC | Use the raw request body bytes |
Using === to compare signatures | Use constant-time comparison |
| Skipping timestamp checks | Validate t is within 300 seconds |
| Using the API key as the secret | Use the webhook secret from Settings -> Webhooks |