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

# Verifiera webhook-signaturer

> Varje webhook- och verktygsbegäran från ThunderPhone är signerad. Verifiera en gång och återanvänd överallt.

Varje begäran vi skickar till din server — webhook-leveranser och
anrop till verktygsslutpunkter — innehåller en HMAC-SHA256-signatur i
rubriken `X-ThunderPhone-Signature`. Verifiera den korrekt en gång och
använd samma hjälpfunktion i varje hanterare.

## Algoritmen

1. Läs den **råa** begärandetexten — de exakta byte vi POSTade till dig.
2. Beräkna `hmac_sha256(secret, body).hexdigest()`.
3. Jämför i **konstant tid** med `X-ThunderPhone-Signature`.
   (En naiv strängjämförelse läcker tidsinformation.)

Vi signerar exakt de byte vi överför, så verifiering av den råa texten
fungerar alltid. Dessa byte är också payloadens **kanoniska JSON-serialisering**
— nycklar sorterade alfabetiskt, kompakta avgränsare
(`,` och `:` utan mellanslag), UTF-8. Det ger dig ett andra, helt
likvärdigt tillvägagångssätt när ditt ramverk bara exponerar parsad JSON:
serialisera om kanoniskt och beräkna HMAC för det.

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

Föredra den råa texten — det är ett steg mindre och immunt mot
egenheter vid JSON-talens tur-och-retur-konvertering i vissa språk.

## Vilken hemlighet?

| Källa                                                                                    | Hemlighet                                                                                                                               |
| ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| [Webhook-slutpunkt](/sv/webhooks/endpoints) (`/v1/developer/webhook-endpoints`)          | Slutpunktsspecifik `secret` (48 hextecken) som returneras en gång vid skapande                                                          |
| [Äldre webhook med en enda URL](/api-reference/organizations#legacy-single-url-webhook)  | Organisationsspecifik `secret` som returneras vid `GET /v1/webhook`                                                                     |
| [Anrop till verktygsslutpunkt](/sv/tools/overview) (direktanrop till din `endpoint.url`) | **Webhook-hemligheten på organisationsnivå** (samma som för den äldre webhooken med en enda URL) — inte en slutpunktsspecifik hemlighet |

Lagra hemligheten i din hemlighetshanterare eller miljövariabel — committa den aldrig.

## Referensimplementationer

Alla fyra verifierar den råa begärandetexten:

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

## Frameworkspecifik koppling

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

## Verifiera verktygsanrop

När agenten anropar ett av dina
[funktionsverktyg](/sv/tools/overview) direkt (verktyget har en
`endpoint`) innehåller begäran två ThunderPhone-rubriker utöver dina
konfigurerade `endpoint.headers`:

* `X-ThunderPhone-Call-ID` — det numeriska ID:t för det aktiva samtalet.
* `X-ThunderPhone-Signature` — HMAC-SHA256, med din
  **webhook-hemlighet på organisationsnivå** som nyckel, över de exakta
  bytevärdena i begärans brödtext.

Samma `verify()`-hjälpfunktion fungerar utan ändringar, med två detaljer:

1. **`GET`- / `DELETE`-verktyg har ingen brödtext.** Argument skickas som
   frågeparametrar, och signaturen beräknas över den **tomma
   bytesträngen** — alltså `verify(b"", sig, secret)` (Python) eller
   `verify(Buffer.alloc(0), sig, secret)` (Node). Hasha **inte**
   frågesträngen.
2. **Organisationer utan en konfigurerad äldre webhook har ingen
   organisationshemlighet.** I det fallet innehåller verktygsanrop endast
   `X-ThunderPhone-Call-ID` och ingen signaturrubrik. Konfigurera den äldre
   webhooken (`PUT /v1/webhook`) för att få en signeringshemlighet, eller
   autentisera verktygsanrop med din egen rubrik 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)
    ...
```

Verktygsdirigering i webhook-**läge** (verktyg utan en `endpoint`, som
levereras till din organisationswebhook som `telephony.tool` / `web.tool`)
är en vanlig signerad webhook — standardreceptet ovan gäller. Se
[Funktionsverktyg](/sv/tools/overview) för båda begärandeformaten.

## Vanliga fallgropar

<AccordionGroup>
  <Accordion title="Serialisera om med standardformatering">
    Att parsa brödtexten och dumpa den igen med JSON-bibliotekets
    standardinställningar (mellanslag efter `,` / `:`, nycklar i insättningsordning) ger
    andra byte och gör att HMAC:en inte fungerar. Verifiera den råa brödtexten — eller, om
    du måste serialisera om, matcha vår kanoniska form exakt: sorterade
    nycklar, kompakta avgränsare, UTF-8.
  </Accordion>

  <Accordion title="Ramverket parsar JSON automatiskt">
    Express-mellanprogrammet `express.json()` förbrukar brödtextströmmen
    och du förlorar de råa byten. Använd `express.raw()` specifikt på webhook-routen,
    eller buffra den råa brödtexten i ett mellanprogram före detta.
    Samma sak gäller NestJS / Koa — läs deras dokumentation om "raw body".
  </Accordion>

  <Accordion title="Tidsosäker jämförelse">
    `expected === signature` i JS eller `expected == signature` i
    Python är tidsvariabla jämförelser. Använd `crypto.timingSafeEqual`
    respektive `hmac.compare_digest`. Prestandaskillnaden
    är obefintlig.
  </Accordion>

  <Accordion title="Fel hemlighet för verktygsslutpunkter">
    Direkta anrop till verktygsslutpunkter signeras med **webhook-hemligheten
    på organisationsnivå** (`GET /v1/webhook`) — inte med någon hemlighet per slutpunkt
    från `/v1/developer/webhook-endpoints`. Återanvänd samma `verify()`
    funktion, men se till att du skickar in organisationshemligheten på verktygsrutter.
  </Accordion>

  <Accordion title="Hasha frågesträngen för GET/DELETE-verktyg">
    För verktygsmetoder utan brödtext omfattar signaturen den tomma bytesträngen,
    vilket ger ett universellt recept: HMAC:a den råa begärandebrödtexten,
    oavsett vad den innehåller. Att hasha URL:en eller frågesträngen kommer aldrig att matcha.
  </Accordion>

  <Accordion title="Returnerar inte 401 vid felmatchning">
    Att returnera 200 när verifieringen misslyckas gör hanteraren till ett mål för replay-attacker.
    Svara alltid med annat än 2xx om verifieringen misslyckas.
  </Accordion>
</AccordionGroup>

***

## Nästa steg

<CardGroup cols={2}>
  <Card title="Översikt över webhooks" icon="bolt" href="/sv/webhooks/overview">
    Leveranssemantik, återförsök, käll-IP-adresser.
  </Card>

  <Card title="Webhook-slutpunkter" icon="plug" href="/sv/webhooks/endpoints">
    Hantera flera URL:er, rotera hemligheter.
  </Card>

  <Card title="Funktionsverktyg" icon="screwdriver-wrench" href="/sv/tools/overview">
    De två sökvägarna för verktygsanrop och deras begärandeformat.
  </Card>

  <Card title="Verktygsintegrationer" icon="wrench" href="/sv/guides/build-tool-integration">
    Bygg en komplett verktygsstödd integration från början till slut.
  </Card>
</CardGroup>
