> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thunderphone.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook-Signaturen überprüfen

> Jeder Webhook und jede Tool-Anfrage von ThunderPhone wird signiert. Einmal überprüfen, überall wiederverwenden.

Jede Anfrage, die wir an Ihren Server senden — Webhook-Zustellungen und
Aufrufe von Tool-Endpunkten — enthält eine HMAC-SHA256-Signatur im
Header `X-ThunderPhone-Signature`. Implementieren Sie die Verifizierung einmal korrekt
und verwenden Sie denselben Helfer in jedem Handler.

## Der Algorithmus

1. Lesen Sie den **rohen** Anfragetextkörper — die exakten Bytes, die wir per POST an Sie gesendet haben.
2. Berechnen Sie `hmac_sha256(secret, body).hexdigest()`.
3. Vergleichen Sie die Signatur in **konstanter Zeit** mit `X-ThunderPhone-Signature`.
   (Ein naiver Stringvergleich gibt Timing-Informationen preis.)

Wir signieren exakt die Bytes, die wir übertragen. Daher funktioniert die Verifizierung des rohen Textkörpers
immer. Diese Bytes sind außerdem die **kanonische JSON-Serialisierung**
der Nutzlast — alphabetisch sortierte Schlüssel, kompakte Trennzeichen
(`,` und `:` ohne Leerzeichen), UTF-8. Das bietet Ihnen eine zweite, vollständig
gleichwertige Methode, wenn Ihr Framework nur geparstes JSON bereitstellt:
Serialisieren Sie kanonisch erneut und berechnen Sie dafür den HMAC.

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

Bevorzugen Sie den rohen Textkörper — das ist ein Schritt weniger und immun gegen Eigenheiten
beim JSON-Zahlen-Roundtrip in einigen Sprachen.

## Welches Secret?

| Quelle                                                                                      | Secret                                                                                                                           |
| ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| [Webhook-Endpunkt](/de/webhooks/endpoints) (`/v1/developer/webhook-endpoints`)              | Pro Endpunkt ein `secret` (48 Hex-Zeichen), das beim Erstellen einmal zurückgegeben wird                                         |
| [Legacy-Webhooks mit einzelner URL](/api-reference/organizations#legacy-single-url-webhook) | Pro Organisation ein `secret`, das bei `GET /v1/webhook` zurückgegeben wird                                                      |
| [Aufruf eines Tool-Endpunkts](/de/tools/overview) (direkter Aufruf Ihrer `endpoint.url`)    | Das **Webhook-Secret auf Organisationsebene** (dasselbe wie für den Legacy-Webhook mit einzelner URL) — kein Secret pro Endpunkt |

Speichern Sie das Secret in Ihrem Secret Manager oder einer Umgebungsvariable — committen Sie es niemals.

## Referenzimplementierungen

Alle vier verifizieren den rohen Anfragetextkörper:

<CodeGroup>
  ```python Python theme={null}
  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 "")
  ```

  ```javascript Node.js theme={null}
  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 Go theme={null}
  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 Ruby theme={null}
  require "openssl"

  def verify(body, signature, secret)
    expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
    Rack::Utils.secure_compare(expected, signature.to_s)
  end
  ```
</CodeGroup>

## Framework-spezifische Anbindung

<CodeGroup>
  ```python FastAPI theme={null}
  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}
  ```

  ```javascript Express theme={null}
  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);
    },
  );
  ```

  ```python Django theme={null}
  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})
  ```
</CodeGroup>

## Tool-Aufrufe verifizieren

Wenn der Agent eines Ihrer
[Funktionstools](/de/tools/overview) direkt aufruft (das Tool verfügt über einen
`endpoint`), enthält die Anfrage neben Ihren konfigurierten
`endpoint.headers` zwei ThunderPhone-Header:

* `X-ThunderPhone-Call-ID` — die numerische ID des aktiven Anrufs.
* `X-ThunderPhone-Signature` — HMAC-SHA256 mit Ihrem
  **organisationsweiten Webhook-Secret** als Schlüssel über die exakten
  Request-Body-Bytes.

Dieselbe Hilfsfunktion `verify()` funktioniert unverändert, mit zwei Besonderheiten:

1. **`GET`- / `DELETE`-Tools haben keinen Body.** Argumente werden als Query-
   Parameter übertragen, und die Signatur wird über die **leere Byte-
   Zeichenfolge** berechnet — also `verify(b"", sig, secret)` (Python) oder
   `verify(Buffer.alloc(0), sig, secret)` (Node). Hashen Sie **nicht** die
   Query-Zeichenfolge.
2. **Organisationen ohne konfigurierten Legacy-Webhook haben kein Organisations-Secret.**
   In diesem Fall enthalten Tool-Aufrufe nur `X-ThunderPhone-Call-ID` und keinen
   Signatur-Header. Konfigurieren Sie den Legacy-Webhook
   (`PUT /v1/webhook`), um ein Signatur-Secret zu erhalten, oder authentifizieren
   Sie Tool-Aufrufe über einen eigenen Header via `endpoint.headers`.

```python theme={null}
@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)
    ...
```

Der Tool-Versand im Webhook-**Modus** (Tools ohne `endpoint`, die über Ihren
Organisations-Webhook als `telephony.tool` / `web.tool` übermittelt werden) ist
ein gewöhnlicher signierter Webhook — das oben beschriebene Standardverfahren
gilt. Informationen zu beiden Anfrageformaten finden Sie unter
[Funktionstools](/de/tools/overview).

## Häufige Fallstricke

<AccordionGroup>
  <Accordion title="Erneutes Serialisieren mit Standardformatierung">
    Das Parsen des Bodys und erneute Ausgeben mit den
    Standardeinstellungen Ihrer JSON-Bibliothek (Leerzeichen nach `,` / `:`, in Einfügereihenfolge angeordnete Schlüssel) erzeugt
    andere Bytes und beschädigt den HMAC. Prüfen Sie den Roh-Body — oder falls Sie ihn erneut serialisieren müssen, entsprechen Sie exakt unserem kanonischen Format: sortierte
    Schlüssel, kompakte Trennzeichen, UTF-8.
  </Accordion>

  <Accordion title="Framework parst JSON automatisch">
    Die Middleware `express.json()` von Express verbraucht den Body-Stream,
    wodurch Sie die Rohbytes verlieren. Verwenden Sie gezielt `express.raw()` für die Webhook-Route,
    oder puffern Sie den Roh-Body in einer vorgeschalteten Middleware.
    Dasselbe gilt für NestJS / Koa — lesen Sie deren Dokumentation zum „raw body“.
  </Accordion>

  <Accordion title="Nicht timing-sicherer Vergleich">
    `expected === signature` in JS oder `expected == signature` in
    Python sind Vergleiche mit variabler Laufzeit. Verwenden Sie jeweils `crypto.timingSafeEqual`
    oder `hmac.compare_digest`. Der Leistungsunterschied
    ist vernachlässigbar.
  </Accordion>

  <Accordion title="Falsches Secret für Tool-Endpunkte">
    Direkte Aufrufe von Tool-Endpunkten werden mit dem **Webhook-Secret
    auf Organisationsebene** (`GET /v1/webhook`) signiert — nicht mit einem
    Endpunkt-spezifischen Secret aus `/v1/developer/webhook-endpoints`. Verwenden Sie dieselbe Funktion `verify()`
    erneut, stellen Sie jedoch sicher, dass Sie ihr auf Tool-Routen das Organisations-Secret übergeben.
  </Accordion>

  <Accordion title="Hashing des Query-Strings bei GET/DELETE-Tools">
    Bei Tool-Methoden ohne Body umfasst die Signatur den leeren Byte-String,
    wodurch ein universelles Verfahren erhalten bleibt: Bilden Sie den HMAC über den Roh-Request-Body,
    unabhängig davon, was er enthält. Das Hashing der URL oder des Query-Strings wird niemals übereinstimmen.
  </Accordion>

  <Accordion title="Bei Abweichung kein 401 zurückgeben">
    Die Rückgabe von 200 bei fehlgeschlagener Verifizierung macht den Handler zu einem
    Ziel für Replay-Angriffe. Antworten Sie immer mit einem Nicht-2xx-Status, wenn die Verifizierung fehlschlägt.
  </Accordion>
</AccordionGroup>

***

## Nächste Schritte

<CardGroup cols={2}>
  <Card title="Webhook-Übersicht" icon="bolt" href="/de/webhooks/overview">
    Zustellsemantik, Wiederholungsversuche, Quell-IP-Adressen.
  </Card>

  <Card title="Webhook-Endpunkte" icon="plug" href="/de/webhooks/endpoints">
    Verwalten Sie mehrere URLs, rotieren Sie Secrets.
  </Card>

  <Card title="Function Tools" icon="screwdriver-wrench" href="/de/tools/overview">
    Die beiden Pfade für Tool-Aufrufe und ihre Request-Formate.
  </Card>

  <Card title="Tool-Integrationen" icon="wrench" href="/de/guides/build-tool-integration">
    Erstellen Sie eine vollständige Tool-gestützte Integration von Anfang bis Ende.
  </Card>
</CardGroup>
