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

# Verifiser webhook-signaturer

> Hver webhook- og verktøyforespørsel fra ThunderPhone er signert. Verifiser én gang, og bruk det på nytt overalt.

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 hver handler.

## Algoritmen

1. Les den **rå** 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`.
   (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.
Dette gir deg en annen, helt tilsvarende metode når rammeverket ditt
bare eksponerer parsede JSON-data: serialiser kanonisk på nytt og
beregn HMAC over det.

```python theme={null}
# 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 steg mindre og
upåvirket av særegenheter ved rundturkonvertering av JSON-tall i noen språk.

## Hvilken hemmelighet?

| Kilde                                                                                 | Hemmelighet                                                                                                                           |
| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| [Webhook-endepunkt](/nb/webhooks/endpoints) (`/v1/developer/webhook-endpoints`)       | `secret` per endepunkt (48 heksadesimale tegn) returnert én gang ved opprettelse                                                      |
| [Eldre webhook med én URL](/api-reference/organizations#legacy-single-url-webhook)    | `secret` per organisasjon returnert ved `GET /v1/webhook`                                                                             |
| [Kall til verktøyendepunkt](/nb/tools/overview) (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 en miljøvariabel — aldri commit den.

## Referanseimplementasjoner

Alle fire verifiserer den rå forespørselsteksten:

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

## Rammeverksspesifikk oppsett

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

## Verifisere verktøykall

Når agenten kaller et av
[funksjonsverktøyene](/nb/tools/overview) dine direkte (verktøyet har et
`endpoint`), inneholder forespørselen to ThunderPhone-headere i tillegg
til de 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ørselskroppen.

Den samme `verify()`-hjelperen fungerer uendret, med to særtilfeller:

1. **`GET`- / `DELETE`-verktøy har ingen kropp.** Argumenter sendes som
   spørringsparametere, og signaturen beregnes over den **tomme byte-
   strengen** — altså `verify(b"", sig, secret)` (Python) eller
   `verify(Buffer.alloc(0), sig, secret)` (Node). Ikke hash
   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`.

```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)
    ...
```

Utsending av verktøy 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](/nb/tools/overview) for begge forespørselsformatene.

## Vanlige fallgruver

<AccordionGroup>
  <Accordion title="Re-serialisering med standardformatering">
    Å parse kroppen og dumpe den på nytt med JSON-bibliotekets
    standardinnstillinger (mellomrom etter `,` / `:`, innsettingsordnede nøkler) gir
    andre byte og ødelegger HMAC-en. Verifiser den rå kroppen — eller hvis
    du må re-serialisere, må du samsvare nøyaktig med vår kanoniske form: sorterte
    nøkler, kompakte skilletegn, UTF-8.
  </Accordion>

  <Accordion title="Rammeverket parser JSON automatisk">
    Express-mellomvaren `express.json()` leser 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 — se dokumentasjonen deres for «raw body».
  </Accordion>

  <Accordion title="Sammenligning som ikke er timingsikker">
    `expected === signature` i JS eller `expected == signature` i
    Python er sammenligninger med variabel timing. Bruk `crypto.timingSafeEqual`
    eller henholdsvis `hmac.compare_digest`. Ytelsesforskjellen
    er ubetydelig.
  </Accordion>

  <Accordion title="Feil hemmelighet for verktøyendepunkter">
    Direkte kall til verktøyendepunkter signeres med **webhook-
    hemmeligheten på organisasjonsnivå** (`GET /v1/webhook`) — ikke med noen hemmelighet per endepunkt
    fra `/v1/developer/webhook-endpoints`. Gjenbruk den samme `verify()`-
    funksjonen, men sørg for at du sender inn organisasjonshemmeligheten på verktøyruter.
  </Accordion>

  <Accordion title="Hasjing av 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. Hasjing av URL-en eller spørringsstrengen vil aldri samsvare.
  </Accordion>

  <Accordion title="Ikke returnere 401 ved avvik">
    Å returnere 200 ved mislykket verifisering gjør behandleren til et mål for replay-
    angrep. Svar alltid med en ikke-2xx-status hvis verifiseringen mislykkes.
  </Accordion>
</AccordionGroup>

***

## Neste trinn

<CardGroup cols={2}>
  <Card title="Oversikt over webhooks" icon="bolt" href="/nb/webhooks/overview">
    Leveringssemantikk, nye forsøk, kilde-IP-er.
  </Card>

  <Card title="Webhook-endepunkter" icon="plug" href="/nb/webhooks/endpoints">
    Administrer flere URL-er, roter hemmeligheter.
  </Card>

  <Card title="Funksjonsverktøy" icon="screwdriver-wrench" href="/nb/tools/overview">
    De to banene for verktøykall og forespørselsformatene deres.
  </Card>

  <Card title="Verktøyintegrasjoner" icon="wrench" href="/nb/guides/build-tool-integration">
    Bygg en komplett verktøystøttet integrasjon fra start til slutt.
  </Card>
</CardGroup>
