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

# Verify signatures

> Check that a tool call or webhook came from your voice agent.

Every [tool call](https://docs.talking-avatar.dev/api-reference/tool-call-request.md) and [post-call webhook](https://docs.talking-avatar.dev/api-reference/call-ended-event.md) is signed with the agent's **signing secret**, the [Standard Webhooks](https://www.standardwebhooks.com) way. Find the secret on the agent's **Webhook** tab. It starts with `whsec_`, and each agent has its own.

## Headers

| Header | Value |
| - | - |
| `webhook-id` | A unique message ID |
| `webhook-timestamp` | Seconds since the epoch |
| `webhook-signature` | `v1,` + the base64 HMAC-SHA256 signature |

The signature is computed over `{webhook-id}.{webhook-timestamp}.{body}`, keyed with the base64-decoded part of the secret after `whsec_`.

## With a library (recommended)

Standard Webhooks libraries check the signature and refuse requests more than 5 minutes old.

```js Node.js theme={null}
// npm install standardwebhooks
import { Webhook } from "standardwebhooks";

const webhook = new Webhook(process.env.AGENT_SIGNING_SECRET); // whsec_…
// Throws if the signature is wrong or the request is too old
const payload = webhook.verify(rawBody, request.headers);
```

```python Python theme={null}
# pip install standardwebhooks
from standardwebhooks.webhooks import Webhook

webhook = Webhook(os.environ["AGENT_SIGNING_SECRET"])  # whsec_…
payload = webhook.verify(raw_body, headers)
```

Libraries exist for many other languages too: see [standardwebhooks.com](https://www.standardwebhooks.com).

## By hand

```js Node.js theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret, headers, rawBody) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // older than 5 minutes

  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest("base64");

  return headers["webhook-signature"].split(" ").some((part) => {
    const [version, signature] = part.split(",");
    return version === "v1" && signature?.length === expected.length && timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  });
}
```

## Common mistakes

* **Verifying the parsed body.** Use the raw request body, exactly as received. Re-serialised JSON won't match.
* **Using another agent's secret.** Each agent has its own.
* **After rotating.** Calls that start after you click **Rotate secret** use the new secret; calls already in progress keep the old one until they end. Accept both briefly while you update your servers.


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