Verificați semnăturile webhook
Fiecare solicitare webhook și de instrument trimisă de ThunderPhone este semnată. Verificați semnătura o dată folosind rețeta de aici, apoi reutilizați aceeași verificare pentru fiecare endpoint pe care îl rulați.
Fiecare solicitare pe care o trimitem către serverul dumneavoastră — livrări de webhook și
apelări ale endpointurilor de instrumente — include o semnătură HMAC-SHA256 în antetul
X-ThunderPhone-Signature. Implementați verificarea corect o singură dată și
integrați același ajutor în fiecare handler.
Algoritmul
- Citiți corpul brut al solicitării — octeții exacți pe care vi i-am trimis prin POST.
- Calculați
hmac_sha256(secret, body).hexdigest(). - Comparați în timp constant cu
X-ThunderPhone-Signature. (O comparație simplă de șiruri expune informații de temporizare.)
Semnăm exact octeții pe care îi transmitem, astfel încât verificarea corpului brut
funcționează întotdeauna. Acești octeți reprezintă și serializarea JSON canonică
a încărcăturii utile — chei sortate alfabetic, separatori compacți
(, și : fără spații), UTF-8. Aceasta vă oferă o a doua metodă, complet
echivalentă, atunci când frameworkul dumneavoastră expune doar JSON-ul analizat:
reserializați canonic și calculați HMAC pentru acesta.
# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")Preferați corpul brut — este cu un pas mai puțin și nu este afectat de particularitățile conversiei repetate a numerelor JSON în unele limbaje.
Ce secret?
| Sursă | Secret |
|---|---|
Endpoint webhook (/v1/developer/webhook-endpoints) | secret per endpoint (48 de caractere hexazecimale) returnat o singură dată la creare |
| Webhook moștenit cu un singur URL | secret per organizație returnat la GET /v1/webhook |
Apelare endpoint de instrumente (apel direct către endpoint.url al dumneavoastră) | Secretul webhook la nivel de organizație (același ca pentru webhookul moștenit cu un singur URL) — nu un secret per endpoint |
Stocați secretul în managerul dumneavoastră de secrete sau într-o variabilă de mediu — nu îl comiteți niciodată.
Implementări de referință
Toate cele patru verifică corpul brut al solicitării:
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 "")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),
);
}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))
}require "openssl"
def verify(body, signature, secret)
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
Rack::Utils.secure_compare(expected, signature.to_s)
endIntegrare specifică frameworkului
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}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);
},
);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})Verificarea apelurilor de instrumente
Atunci când agentul invocă direct unul dintre
instrumentele dumneavoastră de funcții (instrumentul are un
endpoint), solicitarea include două antete ThunderPhone, alături de
endpoint.headers configurat de dumneavoastră:
X-ThunderPhone-Call-ID— ID-ul numeric al apelului activ.X-ThunderPhone-Signature— HMAC-SHA256, cu cheia reprezentată de secretul webhook la nivel de organizație, aplicat asupra octeților exacți ai corpului solicitării.
Același ajutor verify() funcționează fără modificări, cu două particularități:
- Instrumentele
GET/DELETEnu au corp. Argumentele sunt transmise ca parametri de interogare, iar semnătura este calculată asupra șirului de octeți gol — deciverify(b"", sig, secret)(Python) sauverify(Buffer.alloc(0), sig, secret)(Node). Nu calculați hash-ul șirului de interogare. - Organizațiile fără un webhook legacy configurat nu au un secret de organizație. În acest caz, apelurile de instrumente includ doar
X-ThunderPhone-Call-IDși niciun antet de semnătură. Configurați webhook-ul legacy (PUT /v1/webhook) pentru a obține un secret de semnare sau autentificați apelurile de instrumente cu propriul antet prinendpoint.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)
...Expedierea instrumentelor în modul webhook (instrumente fără un endpoint, livrate
către webhook-ul organizației dumneavoastră ca telephony.tool / web.tool) este un webhook semnat obișnuit — se aplică rețeta standard de mai sus. Consultați
Instrumente de funcții pentru ambele forme de solicitare.
Capcane frecvente
Reserializarea cu formatarea implicită
Analizarea corpului și serializarea lui din nou cu valorile
implicite ale bibliotecii JSON (spații după , / :, chei în ordinea inserării) produce
octeți diferiți și compromite HMAC-ul. Verificați corpul brut — sau, dacă
trebuie să îl reserializați, respectați exact forma noastră canonică: chei
sortate, separatori compacți, UTF-8.
Frameworkul analizează automat JSON-ul
Middleware-ul express.json() din Express consumă fluxul corpului
și pierdeți octeții bruți. Utilizați express.raw() în mod specific pe ruta
webhookului sau stocați în buffer corpul brut într-un pre-middleware.
La fel și pentru NestJS / Koa — consultați documentația lor despre „corpul brut”.
Comparație nesigură din perspectiva timpului
expected === signature în JS sau expected == signature în
Python sunt comparații cu durată variabilă. Utilizați crypto.timingSafeEqual
sau hmac.compare_digest, respectiv. Diferența de performanță
este nulă.
Secret greșit pentru endpointurile de instrumente
Apelurile directe către endpointurile de instrumente sunt semnate cu secretul
webhook la nivel de organizație (GET /v1/webhook) — nu cu vreun secret per-endpoint
din /v1/developer/webhook-endpoints. Reutilizați aceeași funcție verify(),
dar asigurați-vă că îi transmiteți secretul organizației pe rutele instrumentelor.
Hashuirea șirului de interogare pentru instrumentele GET/DELETE
Pentru metodele de instrumente fără corp, semnătura acoperă șirul de octeți gol, păstrând o rețetă universală: aplicați HMAC corpului brut al cererii, indiferent care este acesta. Hashuirea URL-ului sau a șirului de interogare nu va corespunde niciodată.
Nereturnarea codului 401 la nepotrivire
Returnarea codului 200 când verificarea eșuează transformă handlerul într-o țintă pentru reluări. Răspundeți întotdeauna cu un cod care nu este 2xx dacă verificarea eșuează.
Pașii următori
Semantica livrării, reîncercări, IP-uri sursă.
Gestionați mai multe URL-uri, rotiți secretele.
Cele două căi de invocare a instrumentelor și formatele cererilor acestora.
Construiți cap-coadă o integrare completă bazată pe instrumente.