ThunderPhone 2.0 já está no ar.Comece por conta própria, a partir de 2¢/min.Leia o anúncio

Operations

Verificar assinaturas de webhook

Todo webhook e solicitação de ferramenta enviados pelo ThunderPhone são assinados. Verifique a assinatura uma vez com a receita abaixo e reutilize a mesma verificação em todos os endpoints que você executar.

Todas as solicitações que enviamos ao seu servidor — entregas de webhook e invocações de endpoint de ferramenta — incluem uma assinatura HMAC-SHA256 no cabeçalho X-ThunderPhone-Signature. Faça a verificação corretamente uma vez e use o mesmo auxiliar em todos os manipuladores.

O algoritmo

  1. Leia o corpo bruto da solicitação — os bytes exatos que enviamos por POST para você.
  2. Calcule hmac_sha256(secret, body).hexdigest().
  3. Compare em tempo constante com X-ThunderPhone-Signature. (Uma comparação ingênua de strings vaza informações de tempo.)

Assinamos exatamente os bytes que transmitimos, portanto verificar o corpo bruto sempre funciona. Esses bytes também são a serialização JSON canônica da carga — chaves ordenadas alfabeticamente, separadores compactos (, e : sem espaços), UTF-8. Isso oferece uma segunda receita, totalmente equivalente, quando seu framework expõe apenas JSON analisado: serialize novamente de forma canônica e aplique HMAC a isso.

# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")

Prefira o corpo bruto — é uma etapa a menos e evita particularidades de conversão de ida e volta de números JSON em algumas linguagens.

Qual segredo?

OrigemSegredo
Endpoint de webhook (/v1/developer/webhook-endpoints)secret por endpoint (48 caracteres hexadecimais), retornado uma única vez na criação
Webhook legado de URL únicasecret por organização retornado em GET /v1/webhook
Invocação de endpoint de ferramenta (chamada direta para seu endpoint.url)O segredo de webhook no nível da organização (o mesmo do webhook legado de URL única) — não um segredo por endpoint

Armazene o segredo no seu gerenciador de segredos ou em uma variável de ambiente — nunca o confirme em um commit.

Implementações de referência

Todas as quatro verificam o corpo bruto da solicitação:

Python
import hashlib
import hmac
 
 
def verify(body: bytes, signature: str, secret: str) -> bool:
    """Constant-time HMAC-SHA256 verification."""
    expected = hmac.new(
        secret.encode("utf-8"),
        body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature or "")
Node.js
import crypto from "node:crypto";
 
export function verify(body, signature, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");
  if (!signature || expected.length !== signature.length) return false;
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature),
  );
}
Go
package webhook
 
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
)
 
func Verify(body []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signature))
}
Ruby
require "openssl"
 
def verify(body, signature, secret)
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
  Rack::Utils.secure_compare(expected, signature.to_s)
end

Integração específica do framework

FastAPI
from fastapi import FastAPI, HTTPException, Request
 
app = FastAPI()
 
@app.post("/thunderphone-webhook")
async def hook(request: Request):
    body = await request.body()           # raw bytes, NOT request.json()
    sig = request.headers.get("X-ThunderPhone-Signature", "")
    if not verify(body, sig, SECRET):
        raise HTTPException(status_code=401)
 
    import json
    event = json.loads(body)
    # … dispatch on event["type"] …
    return {"ok": True}
Express
import express from "express";
 
const app = express();
 
app.post(
  "/thunderphone-webhook",
  // IMPORTANT: parse as raw; do NOT use express.json() here.
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sig = req.header("X-ThunderPhone-Signature") || "";
    if (!verify(req.body, sig, process.env.WEBHOOK_SECRET)) {
      return res.sendStatus(401);
    }
    const event = JSON.parse(req.body.toString("utf8"));
    // … dispatch on event.type …
    res.sendStatus(204);
  },
);
Django
import json
 
from django.http import JsonResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
 
 
@csrf_exempt
@require_POST
def hook(request):
    body = request.body  # raw bytes
    sig = request.headers.get("X-ThunderPhone-Signature", "")
    if not verify(body, sig, SECRET):
        return HttpResponseForbidden("invalid signature")
    event = json.loads(body)
    # … dispatch on event["type"] …
    return JsonResponse({"ok": True})

Verificação de chamadas de ferramentas

Quando o agente invoca diretamente uma das suas ferramentas de função (a ferramenta tem um endpoint), a solicitação inclui dois cabeçalhos do ThunderPhone além dos seus endpoint.headers configurados:

  • X-ThunderPhone-Call-ID — o ID numérico da chamada ativa.
  • X-ThunderPhone-Signature — HMAC-SHA256, com chave definida pelo seu segredo de webhook no nível da organização, sobre os bytes exatos do corpo da solicitação.

O mesmo auxiliar verify() funciona sem alterações, com duas particularidades:

  1. As ferramentas GET / DELETE não têm corpo. Os argumentos são enviados como parâmetros de consulta, e a assinatura é calculada sobre a string de bytes vazia — ou seja, verify(b"", sig, secret) (Python) ou verify(Buffer.alloc(0), sig, secret) (Node). Não faça hash da string de consulta.
  2. Organizações sem um webhook legado configurado não têm um segredo da organização. Nesse caso, as chamadas de ferramentas incluem apenas X-ThunderPhone-Call-ID e nenhum cabeçalho de assinatura. Configure o webhook legado (PUT /v1/webhook) para obter um segredo de assinatura ou autentique as chamadas de ferramentas com seu próprio cabeçalho via endpoint.headers.
@app.post("/tools/search-appointments")
async def tool(request: Request):
    body = await request.body()  # b"" for GET/DELETE tools
    sig = request.headers.get("X-ThunderPhone-Signature", "")
    call_id = request.headers.get("X-ThunderPhone-Call-ID", "")
    if not verify(body, sig, ORG_WEBHOOK_SECRET):
        raise HTTPException(status_code=401)
    args = json.loads(body)
    ...

O despacho de ferramentas no modo webhook (ferramentas sem um endpoint, entregues ao webhook da sua organização como telephony.tool / web.tool) é um webhook assinado comum — a receita padrão acima se aplica. Consulte Ferramentas de função para conhecer os dois formatos de solicitação.

Armadilhas comuns

Reserializar com formatação padrão

Analisar o corpo e serializá-lo novamente com as configurações padrão da sua biblioteca JSON (espaços após , / :, chaves na ordem de inserção) produz bytes diferentes e invalida o HMAC. Verifique o corpo bruto — ou, se precisar reserializá-lo, corresponda exatamente à nossa forma canônica: chaves ordenadas, separadores compactos, UTF-8.

O framework analisa JSON automaticamente

O middleware express.json() do Express consome o fluxo do corpo e você perde os bytes brutos. Use express.raw() especificamente na rota de webhook ou armazene o corpo bruto em buffer em um pré-middleware. O mesmo ocorre com NestJS / Koa — consulte a documentação sobre "corpo bruto".

Comparação insegura em relação ao tempo

expected === signature em JS ou expected == signature em Python são comparações com tempo variável. Use crypto.timingSafeEqual ou hmac.compare_digest, respectivamente. A diferença de desempenho é nula.

Segredo incorreto para endpoints de ferramentas

Chamadas diretas a endpoints de ferramentas são assinadas com o segredo de webhook no nível da organização (GET /v1/webhook) — não com qualquer segredo por endpoint de /v1/developer/webhook-endpoints. Reutilize a mesma função verify(), mas certifique-se de fornecer a ela o segredo da organização nas rotas de ferramentas.

Gerar hash da string de consulta em ferramentas GET/DELETE

Para métodos de ferramentas sem corpo, a assinatura abrange a string de bytes vazia, mantendo uma única receita universal: aplique HMAC ao corpo bruto da solicitação, seja ele qual for. Gerar hash da URL ou da string de consulta nunca corresponderá.

Não retornar 401 em caso de divergência

Retornar 200 quando a verificação falha torna o manipulador um alvo de repetição. Sempre responda com um status diferente de 2xx se a verificação falhar.


Próximas etapas