Signature Verification

View as Markdown

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

HeaderDescription
X-FormantAI-Event-IdUnique event delivery ID.
X-FormantAI-Event-TypeEvent type such as call.completed.
X-FormantAI-SignatureHMAC-SHA256 signature, prefixed with sha256=.
X-FormantAI-TimestampUnix timestamp when the request was sent.
Content-Typeapplication/json.

How signatures are computed

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

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

Always verify against the raw body bytes before parsing JSON.

Node.js / Express

1const crypto = require("crypto");
2const express = require("express");
3
4const app = express();
5const WEBHOOK_SECRET = process.env.FORMANT_WEBHOOK_SECRET;
6
7app.post(
8 "/webhooks/formant",
9 express.raw({ type: "application/json" }),
10 (req, res) => {
11 const signature = req.header("X-FormantAI-Signature") || "";
12 const expected = "sha256=" + crypto
13 .createHmac("sha256", WEBHOOK_SECRET)
14 .update(req.body)
15 .digest("hex");
16
17 const valid = crypto.timingSafeEqual(
18 Buffer.from(signature),
19 Buffer.from(expected)
20 );
21
22 if (!valid) return res.status(401).send("Invalid signature");
23
24 const event = JSON.parse(req.body.toString("utf8"));
25 console.log(event.event_type, event.event_id);
26 res.status(204).send();
27 }
28);

Python / FastAPI

1import hashlib
2import hmac
3import json
4import os
5
6from fastapi import FastAPI, Header, HTTPException, Request
7
8app = FastAPI()
9WEBHOOK_SECRET = os.environ["FORMANT_WEBHOOK_SECRET"]
10
11@app.post("/webhooks/formant")
12async def formant_webhook(
13 request: Request,
14 x_formantai_signature: str = Header(default=""),
15):
16 body = await request.body()
17 expected = "sha256=" + hmac.new(
18 WEBHOOK_SECRET.encode("utf-8"),
19 body,
20 hashlib.sha256,
21 ).hexdigest()
22
23 if not hmac.compare_digest(x_formantai_signature, expected):
24 raise HTTPException(status_code=401, detail="Invalid signature")
25
26 event = json.loads(body)
27 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.