> ## Documentation Index
> Fetch the complete documentation index at: https://docs.notealy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Signing

> How to verify the X-Notealy-Signature header.

Every webhook delivery includes an `X-Notealy-Signature` header so your receiver can confirm Notealy sent it and that the payload was not tampered with in transit.

## Header format

```
X-Notealy-Signature: t=1714845660,v1=4f8a...c93b
```

* `t` — unix timestamp (seconds) at signing time
* `v1` — hex-encoded HMAC-SHA256 of `"{t}.{rawBody}"`, keyed by your subscription's signing secret

The secret is shown once when you create or rotate the subscription. Store it as a server-side secret.

## Verifying with `@notealy/sdk`

The TypeScript SDK ships the verifier as a tree-shakeable subpath, so you don't have to roll the HMAC math yourself:

```ts theme={null}
import { verifyWebhook } from '@notealy/sdk/webhooks'

const event = verifyWebhook({
  rawBody: await req.text(),           // Express: req.body as raw Buffer + .toString('utf8')
  header: req.headers.get('x-notealy-signature') ?? '',
  secret: process.env.NOTEALY_WEBHOOK_SECRET!,
})

switch (event.type) {
  case 'contact.created':
  case 'deal.won':
    // event.data is typed per event
    break
}
```

`verifyWebhook` throws `WebhookVerificationError` (with a discriminating `code`) on any mismatch — never returns a falsy value, so use a `try/catch`. The same function enforces a 5-minute timestamp tolerance by default; override with `options.toleranceSec`.

See [SDKs](/sdks#webhooks) for setup.

## Verifying manually (Node.js)

```ts theme={null}
import crypto from 'node:crypto'

function verifyNotealySignature(rawBody: string, header: string, secret: string) {
  // Parse `t=...,v1=...`
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
  const t = parts.t
  const v1 = parts.v1
  if (!t || !v1) return false

  // Reject signatures older than 5 minutes to prevent replay
  const ageSec = Math.floor(Date.now() / 1000) - Number(t)
  if (ageSec > 300) return false

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex')

  // Constant-time compare
  return crypto.timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex'))
}
```

A few practical notes:

* **Use the raw body**, not a re-serialized one. Express middlewares like `express.json()` consume the buffer; capture it with a `verify` callback or use `body-parser` raw mode for the webhook route.
* **Compare in constant time** with `timingSafeEqual` (or your language's equivalent) to avoid timing attacks.
* **Reject stale signatures** — we recommend a 5-minute tolerance window. Replay attempts older than that should be rejected even if the HMAC matches.
* **Rotate periodically**. Use **Rotate signing secret** in the dashboard; the new secret is shown once and old signatures stop validating immediately.
