> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.formantai.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.formantai.com/_mcp/server.

# Signature Verification

> Verify FormantAI webhook requests with HMAC-SHA256.

Every webhook request includes headers that identify and sign the event.

| Header                   | Description                                     |
| ------------------------ | ----------------------------------------------- |
| `X-FormantAI-Event-Id`   | Unique event delivery ID.                       |
| `X-FormantAI-Event-Type` | Event type such as `call.completed`.            |
| `X-FormantAI-Signature`  | HMAC-SHA256 signature, prefixed with `sha256=`. |
| `X-FormantAI-Timestamp`  | Unix timestamp when the request was sent.       |
| `Content-Type`           | `application/json`.                             |

## How signatures are computed

FormantAI signs the raw request body using the webhook target secret.

```text
X-FormantAI-Signature = sha256=HMAC_SHA256(raw_request_body, webhook_secret)
```

Always verify against the raw body bytes before parsing JSON.

## Node.js / Express

```javascript
const crypto = require("crypto");
const express = require("express");

const app = express();
const WEBHOOK_SECRET = process.env.FORMANT_WEBHOOK_SECRET;

app.post(
  "/webhooks/formant",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.header("X-FormantAI-Signature") || "";
    const expected = "sha256=" + crypto
      .createHmac("sha256", WEBHOOK_SECRET)
      .update(req.body)
      .digest("hex");

    const valid = crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expected)
    );

    if (!valid) return res.status(401).send("Invalid signature");

    const event = JSON.parse(req.body.toString("utf8"));
    console.log(event.event_type, event.event_id);
    res.status(204).send();
  }
);
```

## Python / FastAPI

```python
import hashlib
import hmac
import json
import os

from fastapi import FastAPI, Header, HTTPException, Request

app = FastAPI()
WEBHOOK_SECRET = os.environ["FORMANT_WEBHOOK_SECRET"]

@app.post("/webhooks/formant")
async def formant_webhook(
    request: Request,
    x_formantai_signature: str = Header(default=""),
):
    body = await request.body()
    expected = "sha256=" + hmac.new(
        WEBHOOK_SECRET.encode("utf-8"),
        body,
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(x_formantai_signature, expected):
        raise HTTPException(status_code=401, detail="Invalid signature")

    event = json.loads(body)
    return {"received": True, "event_id": event["event_id"]}
```

## Best practices

* Reject missing signatures.
* Use constant-time comparison.
* Verify before parsing JSON or doing business logic.
* Store `event_id` and ignore duplicates.
* Keep webhook secrets out of logs and repositories.
* Rotate secrets if they are exposed.