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

# Verificer webhook-signaturer

> Alle webhook- og værktøjsanmodninger fra ThunderPhone er signeret. Verificer én gang, og genbrug overalt.

Hver anmodning, vi sender til din server — webhook-leveringer og
kald af værktøjsendepunkter — indeholder en HMAC-SHA256-signatur i
headeren `X-ThunderPhone-Signature`. Få verificeringen korrekt én gang,
og brug den samme hjælpefunktion i alle handlere.

## Algoritmen

1. Læs den **rå** anmodningsbody — de præcise bytes, vi POSTede til dig.
2. Beregn `hmac_sha256(secret, body).hexdigest()`.
3. Sammenlign i **konstant tid** med `X-ThunderPhone-Signature`.
   (En naiv strengsammenligning lækker tidsoplysninger.)

Vi signerer præcis de bytes, vi sender, så verificering af den rå body
virker altid. Disse bytes er også payloadets **kanoniske JSON-serialisering**
— nøgler sorteret alfabetisk, kompakte separatorer
(`,` og `:` uden mellemrum), UTF-8. Det giver dig en anden, fuldt
ækvivalent metode, når dit framework kun eksponerer parset JSON:
serialiser kanonisk igen, 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")
```

Foretræk den rå body — det er ét trin mindre og undgår særheder ved
JSON-tals round-tripping i nogle sprog.

## Hvilken hemmelighed?

| Kilde                                                                                  | Hemmelighed                                                                                                                               |
| -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| [Webhook-endepunkt](/da/webhooks/endpoints) (`/v1/developer/webhook-endpoints`)        | `secret` pr. endepunkt (48 hex-tegn), som returneres én gang ved oprettelse                                                               |
| [Ældre webhook med enkelt URL](/api-reference/organizations#legacy-single-url-webhook) | `secret` pr. organisation, returneret ved `GET /v1/webhook`                                                                               |
| [Kald af værktøjsendepunkt](/da/tools/overview) (direkte kald til din `endpoint.url`)  | **Webhook-hemmeligheden på organisationsniveau** (den samme som for den ældre webhook med enkelt URL) — ikke en hemmelighed pr. endepunkt |

Gem hemmeligheden i din secret manager eller miljøvariabel — commit den aldrig.

## Referenceimplementeringer

Alle fire verificerer den rå anmodningsbody:

<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-specifik opsætning

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

## Verificering af værktøjskald

Når agenten kalder et af dine
[funktionsværktøjer](/da/tools/overview) direkte (værktøjet har et
`endpoint`), indeholder anmodningen to ThunderPhone-headere sammen med
dine konfigurerede `endpoint.headers`:

* `X-ThunderPhone-Call-ID` — det numeriske id for det aktive opkald.
* `X-ThunderPhone-Signature` — HMAC-SHA256 med din
  **webhookhemmelighed på organisationsniveau** som nøgle, baseret på de nøjagtige
  bytes i anmodningens body.

Den samme `verify()`-hjælpefunktion fungerer uændret, med to detaljer:

1. **`GET` / `DELETE`-værktøjer har ingen body.** Argumenter sendes som
   forespørgselsparametre, og signaturen beregnes over den **tomme
   bytestreng** — altså `verify(b"", sig, secret)` (Python) eller
   `verify(Buffer.alloc(0), sig, secret)` (Node). Hash **ikke**
   forespørgselsstrengen.
2. **Organisationer uden en konfigureret ældre webhook har ingen organisationshemmelighed.**
   I så fald indeholder værktøjskald kun `X-ThunderPhone-Call-ID` og ingen
   signaturheader. Konfigurer den ældre webhook
   (`PUT /v1/webhook`) for at få en signeringshemmelighed, eller godkend
   værktøjskald 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)
    ...
```

Værktøjsafsendelse i webhook-**tilstand** (værktøjer uden et `endpoint`, leveret
til din organisationswebhook som `telephony.tool` / `web.tool`) er en almindelig
signeret webhook — standardopsætningen ovenfor gælder. Se
[Funktionsværktøjer](/da/tools/overview) for begge anmodningsformater.

## Almindelige faldgruber

<AccordionGroup>
  <Accordion title="Gendannelse af serialisering med standardformatering">
    At parse brødteksten og dumpe den igen med dit JSON-biblioteks
    standardindstillinger (mellemrum efter `,` / `:`, nøgler i indsættelsesrækkefølge) giver
    forskellige bytes og ødelægger HMAC'en. Verificer den rå brødtekst — eller hvis
    du skal serialisere igen, så match vores kanoniske format nøjagtigt: sorterede
    nøgler, kompakte separatorer, UTF-8.
  </Accordion>

  <Accordion title="Framework parser JSON automatisk">
    Express' `express.json()`-middleware bruger brødtekststrømmen,
    og du mister de rå bytes. Brug specifikt `express.raw()` på webhook-ruten,
    eller buffer den rå brødtekst i en pre-middleware.
    Det samme gælder NestJS / Koa — se deres dokumentation om "raw body".
  </Accordion>

  <Accordion title="Timing-usikker sammenligning">
    `expected === signature` i JS eller `expected == signature` i
    Python er sammenligninger med variabel timing. Brug henholdsvis `crypto.timingSafeEqual`
    eller `hmac.compare_digest`. Ydelsesforskellen
    er nul.
  </Accordion>

  <Accordion title="Forkert secret til værktøjsendepunkter">
    Direkte kald til værktøjsendepunkter signeres med **webhook-secreten
    på organisationsniveau** (`GET /v1/webhook`) — ikke med en secret pr. endepunkt
    fra `/v1/developer/webhook-endpoints`. Genbrug den samme `verify()`-
    funktion, men sørg for at give den organisations-secreten på værktøjsruter.
  </Accordion>

  <Accordion title="Hashing af querystrengen på GET/DELETE-værktøjer">
    For værktøjsmetoder uden brødtekst dækker signaturen den tomme bytestreng,
    så der bevares én universel opskrift: HMAC den rå request body,
    uanset hvad den er. Hashing af URL'en eller querystrengen vil aldrig matche.
  </Accordion>

  <Accordion title="Returnerer ikke 401 ved uoverensstemmelse">
    At returnere 200 ved mislykket verificering gør handleren til et mål for replay-angreb.
    Svar altid med en ikke-2xx-status, hvis verificeringen mislykkes.
  </Accordion>
</AccordionGroup>

***

## Næste trin

<CardGroup cols={2}>
  <Card title="Webhooks-oversigt" icon="bolt" href="/da/webhooks/overview">
    Leveringssemantik, genforsøg, kilde-IP'er.
  </Card>

  <Card title="Webhook-endepunkter" icon="plug" href="/da/webhooks/endpoints">
    Administrer flere URL'er, rotér secrets.
  </Card>

  <Card title="Funktionsværktøjer" icon="screwdriver-wrench" href="/da/tools/overview">
    De to veje til værktøjskald og deres request-formater.
  </Card>

  <Card title="Værktøjsintegrationer" icon="wrench" href="/da/guides/build-tool-integration">
    Byg en komplet værktøjsunderstøttet integration fra start til slut.
  </Card>
</CardGroup>
