> ## 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.

# Webhookhandtekeningen verifiëren

> Elke webhook- en toolaanvraag van ThunderPhone is ondertekend. Verifieer één keer; hergebruik overal.

Elk verzoek dat we naar je server sturen — webhookleveringen en
aanroepen van tool-eindpoints — bevat een HMAC-SHA256-handtekening in de
header `X-ThunderPhone-Signature`. Implementeer de verificatie één keer correct
en gebruik dezelfde helper in elke handler.

## Het algoritme

1. Lees de **onbewerkte** requestbody — de exacte bytes die we naar je POSTen.
2. Bereken `hmac_sha256(secret, body).hexdigest()`.
3. Vergelijk in **constante tijd** met `X-ThunderPhone-Signature`.
   (Een naïeve tekenreeksvergelijking lekt timinginformatie.)

We ondertekenen exact de bytes die we verzenden, dus het verifiëren van de
onbewerkte body werkt altijd. Die bytes zijn ook de **canonieke JSON-serialisatie**
van de payload — sleutels alfabetisch gesorteerd, compacte scheidingstekens
(`,` en `:` zonder spaties), UTF-8. Dit biedt je een tweede, volledig
gelijkwaardige aanpak wanneer je framework alleen geparseerde JSON beschikbaar
maakt: serialiseer canoniek opnieuw en bereken daarover de HMAC.

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

Geef de voorkeur aan de onbewerkte body — dat is één stap minder en voorkomt
problemen met JSON-getallen die in sommige talen opnieuw worden omgezet.

## Welk secret?

| Bron                                                                                           | Secret                                                                                                                         |
| ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| [Webhook-eindpoint](/nl/webhooks/endpoints) (`/v1/developer/webhook-endpoints`)                | `secret` per eindpoint (48 hexadecimale tekens), eenmalig geretourneerd bij aanmaken                                           |
| [Verouderde webhook met één URL](/api-reference/organizations#legacy-single-url-webhook)       | `secret` per organisatie, geretourneerd via `GET /v1/webhook`                                                                  |
| [Aanroep van tool-eindpoint](/nl/tools/overview) (rechtstreekse aanroep van je `endpoint.url`) | Het **webhook-secret op organisatieniveau** (hetzelfde als voor de verouderde webhook met één URL) — geen secret per eindpoint |

Sla het secret op in je secretmanager of omgevingsvariabele — commit het nooit.

## Referentie-implementaties

Alle vier verifiëren de onbewerkte requestbody:

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

## Frameworkspecifieke integratie

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

## Toolaanroepen verifiëren

Wanneer de spraakagent rechtstreeks een van je
[functietools](/nl/tools/overview) aanroept (de tool heeft een
`endpoint`), bevat het verzoek naast je geconfigureerde
`endpoint.headers` twee ThunderPhone-headers:

* `X-ThunderPhone-Call-ID` — de numerieke id van het actieve gesprek.
* `X-ThunderPhone-Signature` — HMAC-SHA256, met je
  **webhookgeheim op organisatieniveau** als sleutel, over de exacte
  bytes van de aanvraagbody.

Dezelfde `verify()`-helper werkt ongewijzigd, met twee nuances:

1. **`GET`- / `DELETE`-tools hebben geen body.** Argumenten worden als
   queryparameters doorgegeven en de handtekening wordt berekend over
   de **lege bytestring** — dus `verify(b"", sig, secret)` (Python) of
   `verify(Buffer.alloc(0), sig, secret)` (Node). Hash de querystring
   **niet**.
2. **Organisaties zonder geconfigureerde verouderde webhook hebben geen
   organisatiegeheim.** In dat geval bevatten toolaanroepen alleen
   `X-ThunderPhone-Call-ID` en geen handtekeningheader. Configureer de
   verouderde webhook (`PUT /v1/webhook`) om een ondertekeningsgeheim te
   krijgen, of verifieer toolaanroepen met je eigen 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)
    ...
```

Tooldispatch in webhook-**modus** (tools zonder een `endpoint`, geleverd
aan je organisatiewebhook als `telephony.tool` / `web.tool`) is een
gewone ondertekende webhook — het standaardrecept hierboven is van
toepassing. Zie [Functietools](/nl/tools/overview) voor beide
aanvraagvormen.

## Veelvoorkomende valkuilen

<AccordionGroup>
  <Accordion title="Opnieuw serialiseren met standaardopmaak">
    De body parsen en opnieuw dumpen met de standaardinstellingen van je
    JSON-bibliotheek (spaties na `,` / `:`, sleutels in invoegvolgorde) produceert
    andere bytes en breekt de HMAC. Verifieer de onbewerkte body — of als
    je opnieuw moet serialiseren, volg dan exact onze canonieke vorm: gesorteerde
    sleutels, compacte scheidingstekens, UTF-8.
  </Accordion>

  <Accordion title="Framework parseert JSON automatisch">
    De `express.json()`-middleware van Express verbruikt de bodystream
    en je verliest de onbewerkte bytes. Gebruik specifiek `express.raw()` op de
    webhookroute, of buffer de onbewerkte body in een pre-middleware.
    Hetzelfde geldt voor NestJS / Koa — bekijk hun documentatie over de "raw body".
  </Accordion>

  <Accordion title="Niet timing-safe vergelijken">
    `expected === signature` in JS of `expected == signature` in
    Python zijn vergelijkingen met variabele timing. Gebruik respectievelijk `crypto.timingSafeEqual`
    of `hmac.compare_digest`. Het prestatieverschil
    is nihil.
  </Accordion>

  <Accordion title="Verkeerd secret voor tool-endpoints">
    Rechtstreekse aanroepen van tool-endpoints worden ondertekend met het **webhooksecret
    op organisatieniveau** (`GET /v1/webhook`) — niet met een secret per eindpunt
    uit `/v1/developer/webhook-endpoints`. Hergebruik dezelfde `verify()`
    functie, maar zorg dat je deze op toolroutes het organisatiesecret geeft.
  </Accordion>

  <Accordion title="De querystring hashen bij GET/DELETE-tools">
    Voor toolmethoden zonder body omvat de handtekening de lege bytestring,
    waardoor je één universele werkwijze behoudt: HMAC de onbewerkte requestbody,
    wat die ook is. De URL of querystring hashen komt nooit overeen.
  </Accordion>

  <Accordion title="Geen 401 retourneren bij mismatch">
    Een 200 retourneren bij mislukte verificatie maakt de handler een doelwit
    voor replayaanvallen. Geef altijd een niet-2xx-status terug als verificatie mislukt.
  </Accordion>
</AccordionGroup>

***

## Volgende stappen

<CardGroup cols={2}>
  <Card title="Webhookoverzicht" icon="bolt" href="/nl/webhooks/overview">
    Leveringssemantiek, retries, bron-IP's.
  </Card>

  <Card title="Webhook-eindpunten" icon="plug" href="/nl/webhooks/endpoints">
    Beheer meerdere URL's, roteer secrets.
  </Card>

  <Card title="Functietools" icon="screwdriver-wrench" href="/nl/tools/overview">
    De twee paden voor toolaanroepen en hun requeststructuren.
  </Card>

  <Card title="Toolintegraties" icon="wrench" href="/nl/guides/build-tool-integration">
    Bouw een complete integratie met tools van begin tot eind.
  </Card>
</CardGroup>
