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

# Verify webhook signatures in Node and Python

> Check the OmniChar-Signature header with HMAC-SHA256 over the timestamp and raw body, reject old timestamps, and compare in constant time. Node and Python.

Every webhook is signed with your endpoint's secret. Check the signature on the raw request body, before parsing it: a re-serialised body will not match.

The header looks like `t=1762682980,v1=5f2c...`. Each `v1` is the hex HMAC-SHA256 of `"<t>.<raw body>"` with a signing secret. Accept the request if any `v1` matches your secret: for 24 hours after you rotate a secret, the header carries one `v1` for the new secret and one for the old. Reject a timestamp more than five minutes from your clock, so a captured request cannot be replayed later.

## Node

```js theme={null}
import { createHmac, timingSafeEqual } from 'node:crypto'

export function verifyOmniCharSignature(secret, rawBody, header, toleranceSeconds = 300) {
  const pairs = header.split(',').map((part) => part.split('='))
  const t = Number(pairs.find(([name]) => name === 't')?.[1])
  const signatures = pairs.filter(([name]) => name === 'v1').map(([, value]) => value ?? '')
  if (!Number.isFinite(t) || signatures.length === 0) return false
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false
  const expected = Buffer.from(createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'))
  return signatures.some((given) => {
    const candidate = Buffer.from(given)
    return candidate.length === expected.length && timingSafeEqual(candidate, expected)
  })
}
```

With Express, read the body raw for this route:

```js theme={null}
app.post('/omnichar/events', express.raw({ type: 'application/json' }), (req, res) => {
  const raw = req.body.toString('utf8')
  if (!verifyOmniCharSignature(process.env.OMNICHAR_WEBHOOK_SECRET, raw, req.get('OmniChar-Signature') ?? '')) {
    return res.status(400).end()
  }
  const event = JSON.parse(raw)
  // Deduplicate on event.id before acting.
  res.status(204).end()
})
```

## Python

```python theme={null}
import hashlib
import hmac
import time


def verify_omnichar_signature(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:
    pairs = [part.split("=", 1) for part in header.split(",") if "=" in part]
    timestamps = [value for name, value in pairs if name == "t"]
    signatures = [value for name, value in pairs if name == "v1"]
    try:
        t = int(timestamps[0])
    except (IndexError, ValueError):
        return False
    if abs(time.time() - t) > tolerance or not signatures:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, given) for given in signatures)
```

In Flask, pass `request.get_data()` as `raw_body` and `request.headers.get("OmniChar-Signature", "")` as `header`.

Answer quickly with a `2xx` and do slow work afterwards: a request that takes more than 10 seconds counts as failed and is retried.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.