ThunderPhone 2.0 este acum disponibil.Îl configurați singur, de la 2 ¢/min.Citiți anunțul

Operations

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

  1. Citiți corpul brut al solicitării — octeții exacți pe care vi i-am trimis prin POST.
  2. Calculați hmac_sha256(secret, body).hexdigest().
  3. 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 URLsecret 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:

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

Integrare specifică frameworkului

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})

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:

  1. Instrumentele GET / DELETE nu au corp. Argumentele sunt transmise ca parametri de interogare, iar semnătura este calculată asupra șirului de octeți gol — deci verify(b"", sig, secret) (Python) sau verify(Buffer.alloc(0), sig, secret) (Node). Nu calculați hash-ul șirului de interogare.
  2. 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 prin 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)
    ...

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