Verify a webhook
Use this helper in a Node.js receiver. Download verify-webhook.mjs. It uses only the built-in cryptography module.
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verifyWebhook({ rawBody, signature, eventId, secret, now = Date.now() }) { if (!Buffer.isBuffer(rawBody) || rawBody.length > 256 * 1024) throw new Error('Invalid webhook body') if (typeof secret !== 'string' || !secret) throw new Error('Missing signing secret') if (typeof signature !== 'string' || typeof eventId !== 'string' || !eventId) throw new Error('Missing webhook headers') const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(signature) if (!match) throw new Error('Invalid signature header') const timestamp = Number(match[1]) if (!Number.isSafeInteger(timestamp) || !Number.isFinite(now) || Math.abs(now / 1000 - timestamp) > 300) { throw new Error('Webhook timestamp outside tolerance') } const expected = createHmac('sha256', secret).update(`${match[1]}.`).update(rawBody).digest() if (!timingSafeEqual(expected, Buffer.from(match[2], 'hex'))) throw new Error('Invalid webhook signature') const event = JSON.parse(rawBody.toString('utf8')) if (!event || typeof event !== 'object' || event.id !== eventId) throw new Error('Event ID mismatch') return event}Connect it to your HTTP handler
Section titled “Connect it to your HTTP handler”Read the request body as a Buffer, with a size limit enforced while reading. Do not let JSON middleware transform it before verification. Pass the raw body, X-EthicsPortal-Signature, X-EthicsPortal-Event-Id and your secret to verifyWebhook.
const event = verifyWebhook({ rawBody, signature: request.headers['x-ethicsportal-signature'], eventId: request.headers['x-ethicsportal-event-id'], secret: process.env.ETHICSPORTAL_WEBHOOK_SECRET})Catch verification failures and return a non-2xx response. Do not log the secret or full body. A valid signature authenticates a delivery; it does not make the delivery unique.
Persist before acknowledging
Section titled “Persist before acknowledging”In your application’s database, insert the verified event into a durable inbox with a unique event ID. On a duplicate, return 2xx without inserting or applying it again. Return 2xx for a new event only after the inbox transaction commits. If storage is unavailable, return a failure status.
A worker can then process the inbox with idempotent business operations. This helper deliberately handles verification only; the durable inbox belongs in your application. Delivery and retry semantics.