# Verify a webhook

Source: https://ethicsportal.eu/developers/examples/verify-webhook/

Use this helper in a Node.js receiver. [Download verify-webhook.mjs](https://ethicsportal.eu/developers/examples/verify-webhook.mjs). It uses only the built-in cryptography module.

verify-webhook.mjs

```
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

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

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](https://ethicsportal.eu/developers/webhooks/).
