ThunderPhone 2.0 er lansert.Kom i gang selv, fra 2 ¢/min.Les mer om lanseringen

Operations

Verifiser webhook-signaturer

Hver webhook- og verktøyforespørsel ThunderPhone sender, er signert. Verifiser signaturen én gang med oppskriften her, og bruk deretter den samme kontrollen på hvert endepunkt du kjører.

Hver forespørsel vi sender til serveren din — webhook-leveringer og kall til verktøyendepunkter — har en HMAC-SHA256-signatur i X-ThunderPhone-Signature-headeren. Få verifiseringen riktig én gang, og bruk den samme hjelpefunksjonen i alle behandlere.

Algoritmen

  1. Les den forespørselsteksten — de nøyaktige bytene vi POST-et til deg.
  2. Beregn hmac_sha256(secret, body).hexdigest().
  3. Sammenlign i konstant tid med X-ThunderPhone-Signature. (En naiv strengsammenligning lekker tidsinformasjon.)

Vi signerer nøyaktig bytene vi overfører, så verifisering av den rå forespørselsteksten fungerer alltid. Disse bytene er også den kanoniske JSON-serialiseringen av nyttelasten — nøkler sortert alfabetisk, kompakte skilletegn (, og : uten mellomrom), UTF-8. Det gir deg en annen, fullt ut tilsvarende metode når rammeverket ditt bare eksponerer tolket JSON: serialiser på nytt kanonisk og beregn HMAC for det.

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

Foretrekk den rå forespørselsteksten — det er ett trinn mindre og upåvirket av særegenheter ved tur-retur-konvertering av JSON-tall i enkelte språk.

Hvilken hemmelighet?

KildeHemmelighet
Webhook-endepunkt (/v1/developer/webhook-endpoints)secret per endepunkt (48 heksadesimale tegn) som returneres én gang ved opprettelse
Eldre webhook med én URLsecret per organisasjon som returneres ved GET /v1/webhook
Kall til verktøyendepunkt (direkte kall til din endpoint.url)Webhook-hemmeligheten på organisasjonsnivå (den samme som for den eldre webhooken med én URL) — ikke en hemmelighet per endepunkt

Lagre hemmeligheten i hemmelighetshåndtereren din eller i en miljøvariabel — aldri legg den inn i commit.

Referanseimplementasjoner

Alle fire verifiserer den rå forespørselsteksten:

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

Rammeverksspesifikk tilkobling

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

Verifisering av verktøykall

Når agenten kaller ett av funksjonsverktøyene dine direkte (verktøyet har et endpoint), inneholder forespørselen to ThunderPhone-headere sammen med den konfigurerte endpoint.headers:

  • X-ThunderPhone-Call-ID — den numeriske ID-en til den pågående samtalen.
  • X-ThunderPhone-Signature — HMAC-SHA256, med webhook-hemmeligheten på organisasjonsnivå som nøkkel, over de nøyaktige byteverdiene i forespørselsbrødteksten.

Den samme verify()-hjelperen fungerer uten endringer, med to særtilfeller:

  1. GET- / DELETE-verktøy har ingen brødtekst. Argumenter sendes som spørringsparametere, og signaturen beregnes over den tomme bytestrengen — altså verify(b"", sig, secret) (Python) eller verify(Buffer.alloc(0), sig, secret) (Node). Ikke hasj spørringsstrengen.
  2. Organisasjoner uten en konfigurert eldre webhook har ingen organisasjonshemmelighet. I så fall inneholder verktøykall bare X-ThunderPhone-Call-ID og ingen signatur-header. Konfigurer den eldre webhooken (PUT /v1/webhook) for å få en signeringshemmelighet, eller autentiser verktøykall med din egen header 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)
    ...

Verktøydistribusjon i webhook-modus (verktøy uten et endpoint, levert til organisasjonens webhook som telephony.tool / web.tool) er en vanlig signert webhook — standardoppskriften ovenfor gjelder. Se Funksjonsverktøy for begge forespørselsformatene.

Vanlige fallgruver

Serialisering på nytt med standardformatering

Å tolke kroppen og skrive den ut på nytt med JSON-bibliotekets standardinnstillinger (mellomrom etter , / :, nøkler i innsettingsrekkefølge) gir andre byte og ødelegger HMAC-en. Verifiser den rå kroppen — eller hvis du må serialisere på nytt, match vår kanoniske form nøyaktig: sorterte nøkler, kompakte skilletegn, UTF-8.

Rammeverket tolker JSON automatisk

Express-mellomvaren express.json() bruker opp kroppstrømmen, og du mister de rå bytene. Bruk express.raw() spesifikt på webhook-ruten, eller bufre den rå kroppen i en forhåndsmellomvare. Det samme gjelder NestJS / Koa — sjekk dokumentasjonen deres for "rå kropp".

Sammenligning som ikke er timingsikker

expected === signature i JS eller expected == signature i Python er sammenligninger med variabel timing. Bruk henholdsvis crypto.timingSafeEqual eller hmac.compare_digest. Ytelsesforskjellen er lik null.

Feil hemmelighet for verktøyendepunkter

Direkte kall til verktøyendepunkter signeres med webhook-hemmeligheten på organisasjonsnivå (GET /v1/webhook) — ikke med en hemmelighet per endepunkt fra /v1/developer/webhook-endpoints. Gjenbruk den samme verify()- funksjonen, men sørg for at du gir den organisasjonshemmeligheten på verktøyruter.

Hasher spørringsstrengen på GET/DELETE-verktøy

For verktøymetoder uten kropp dekker signaturen den tomme byte- strengen, slik at du beholder én universell oppskrift: HMAC den rå forespørselskroppen, uansett hva den er. Å hashe URL-en eller spørringsstrengen vil aldri samsvare.

Returnerer ikke 401 ved manglende samsvar

Å returnere 200 ved mislykket verifisering gjør behandleren til et mål for replay-angrep. Svar alltid med ikke-2xx hvis verifiseringen mislykkes.


Neste steg