# Verifying signatures

> Check the HMAC before you trust a payload, and use the raw body when you do it

Source: https://www.hey.support/docs/developer-guide/webhooks/signature

---

Your endpoint URL is public, so anyone who learns it can POST to it. Verify the signature before you act on a delivery.

## How it works

Each endpoint has a signing secret. We compute `HMAC-SHA256(raw_body, secret)` and send it as:

```
X-HeySupport-Signature: sha256=<hex>
```

Recompute it with your own copy of the secret and compare. If they differ, the request did not come from us.

## Node

```js
import crypto from 'node:crypto';

export function verify(rawBody, signatureHeader, secret) {
  const expected =
    'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');

  const a = Buffer.from(signatureHeader ?? '');
  const b = Buffer.from(expected);
  // Lengths must match before timingSafeEqual, which throws otherwise.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

## Express

```js
import express from 'express';

const app = express();

// express.raw, NOT express.json: see below.
app.post(
  '/hey-support',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    if (!verify(req.body, req.get('X-HeySupport-Signature'), process.env.HS_WEBHOOK_SECRET)) {
      return res.sendStatus(401);
    }
    const event = JSON.parse(req.body.toString('utf8'));
    // Acknowledge fast, work afterwards.
    res.sendStatus(200);
    void handle(event);
  },
);
```

## Next.js route handler

```ts
export async function POST(req: Request) {
  const raw = await req.text();
  if (!verify(raw, req.headers.get('x-heysupport-signature'), process.env.HS_WEBHOOK_SECRET!)) {
    return new Response('bad signature', { status: 401 });
  }
  const event = JSON.parse(raw);
  return Response.json({ ok: true });
}
```

## Use the raw body

<Callout type="warn">
  Sign against the exact bytes we sent, never a re-serialized object. `JSON.parse` followed by `JSON.stringify` can reorder keys, change number formatting and drop whitespace, each of which produces a different HMAC. This is by far the commonest reason a working secret appears to fail.
</Callout>

Most frameworks parse JSON for you by default, which destroys the raw body before your handler sees it. Turn that off for the webhook route specifically, as above.

## Compare in constant time

Use `timingSafeEqual` or your language's equivalent rather than `===`. A plain string comparison returns early on the first differing byte, which leaks the correct signature a byte at a time.

## Rotating the secret

Rotate a signing secret from the same settings page. For 24 hours we sign with both the new and the previous secret, so deliveries already in flight keep verifying while you deploy the new value. Verify against either during that window, then drop the old one.

<NextSteps>
  <IconCard icon="refresh-cw" title="Delivery and retries" href="/docs/developer-guide/webhooks/delivery" description="Acknowledge within 10 seconds and dedupe on event_id." />

  <IconCard icon="webhook" title="Webhooks overview" href="/docs/developer-guide/webhooks" description="The envelope, the headers and every event." />
</NextSteps>
