تخطَّ إلى المحتوى

Verify webhook signatures

هذا المحتوى غير متوفر بلغتك بعد.

Your webhook URL is a public endpoint. Without a signature check, anyone who learns it can post whatever they like, and your system will believe an order was delivered, a customer replied, or a campaign finished.

Every delivery is signed with the secret shown once when the webhook was created.

X-KM-Timestamp: 1755648000
X-KM-Signature: v1=4a3f8c2e91b7d0...

The signature is HMAC-SHA256(secret, "<timestamp>.<raw body>"), hex encoded.

The timestamp is inside the signed string on purpose. Signing only the body produces a signature that stays valid forever, so anyone who captured one delivery could replay it whenever they liked and your endpoint could not tell. With the timestamp bound in, a replay is detectable by age.

import crypto from 'node:crypto'
const TOLERANCE_SECONDS = 300
export function verify(rawBody, headers, secret) {
const timestamp = headers['x-km-timestamp']
const signature = (headers['x-km-signature'] ?? '').replace(/^v1=/, '')
if (!timestamp || !signature) return false
// Reject anything older than five minutes, which is what makes the
// timestamp worth signing.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest('hex')
// Constant time: a plain === leaks how much of the signature matched.
const a = Buffer.from(signature, 'hex')
const b = Buffer.from(expected, 'hex')
return a.length === b.length && crypto.timingSafeEqual(a, b)
}

Second most common: comparing with ==. Use a constant-time comparison. A normal string comparison returns faster the earlier it finds a difference, which leaks enough to forge a signature byte by byte.

PUT /v1/webhooks/{id} with a new secret. It takes effect on the next delivery, so accept both the old and the new one for a few minutes if you cannot deploy the change atomically.

X-Webhook-Signature: sha256=<hex> signs the body alone, without a timestamp. It is still sent so that consumers already verifying it keep working. New integrations should use X-KM-Signature: without a timestamp there is no way to detect a replay.