POST we send to your server — webhooks, tool-endpoint
invocations — carries an HMAC-SHA256 signature in the
X-ThunderPhone-Signature header. Get the verification right once
and plug the same helper into every handler.
The algorithm
- Read the raw request body. Not
request.json(), not anything that re-serializes — the raw bytes we POSTed to you. - Compute
hmac_sha256(secret, body).hexdigest(). - Compare in constant time against
X-ThunderPhone-Signature. (Naive string compare leaks timing information.)
Which secret?
| Source | Secret |
|---|---|
Webhook endpoint (/v1/developer/webhook-endpoints) | Per-endpoint secret returned once at create |
| Legacy single-URL webhook | Per-org secret returned on GET /v1/webhook |
| Tool-integration endpoint | Same secret as the webhook endpoint that delivered the telephony.incoming / web.incoming event — requests share the signing key |
Reference implementations
Framework-specific wiring
Common pitfalls
Using parsed JSON instead of raw body
Using parsed JSON instead of raw body
JSON.parse(body) and re-serialize changes the byte sequence
(whitespace, key order, number formatting), which breaks the
HMAC. Always verify against the raw request body you received
before any parsing.Framework auto-parses JSON
Framework auto-parses JSON
Express’s
express.json() middleware consumes the body stream
and you lose the raw bytes. Use express.raw() on the webhook
route specifically, or buffer the raw body in a pre-middleware.
Same story for NestJS / Koa — check their “raw body” docs.Timing-unsafe comparison
Timing-unsafe comparison
expected === signature in JS or expected == signature in
Python are timing-variable comparisons. Use crypto.timingSafeEqual
or hmac.compare_digest respectively. The performance difference
is nil.Wrong secret for tool endpoints
Wrong secret for tool endpoints
When the agent calls one of your tool endpoints, the signing key
is the same secret as the webhook endpoint that delivered
telephony.incoming or web.incoming — not a separate tool
secret. Reuse the same verify() function.Not returning 401 on mismatch
Not returning 401 on mismatch
Returning 200 on failed verification makes the handler a replay
target. Always respond non-2xx if verification fails.
Next steps
Webhooks overview
Delivery semantics, retries, source IPs.
Webhook endpoints
Manage multiple URLs, rotate secrets.
Tool integrations
Apply the same verification pattern to tool endpoints.