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

# Vahvista webhook-allekirjoitukset

> Jokainen ThunderPhonesta tuleva webhook- ja työkalupyyntö on allekirjoitettu. Vahvista kerran; käytä uudelleen kaikkialla.

Jokaisessa palvelimellesi lähettämässämme pyynnössä — webhook-toimituksissa ja
työkalupäätepisteiden kutsuissa — on HMAC-SHA256-allekirjoitus
`X-ThunderPhone-Signature`-otsakkeessa. Toteuta varmennus oikein kerran ja
käytä samaa apufunktiota jokaisessa käsittelijässä.

## Algoritmi

1. Lue pyynnön **raaka** runko — täsmälleen ne tavut, jotka POSTasimme sinulle.
2. Laske `hmac_sha256(secret, body).hexdigest()`.
3. Vertaa sitä **vakioajassa** arvoon `X-ThunderPhone-Signature`.
   (Tavallinen merkkijonovertailu vuotaa ajoitustietoa.)

Allekirjoitamme täsmälleen lähettämämme tavut, joten raakarungon
varmentaminen toimii aina. Nämä tavut ovat myös hyötykuorman **kanoninen JSON-sarjallistus**
— avaimet aakkosjärjestyksessä, tiiviit erotinmerkit
(`,` ja `:` ilman välilyöntejä), UTF-8. Tämä antaa sinulle toisen, täysin
vastaavan tavan, kun kehyksesi tarjoaa vain jäsennetyn JSONin:
sarjallista kanonisesti uudelleen ja laske sille HMAC.

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

Suosi raakaa runkoa — siinä on yksi vaihe vähemmän, eikä se ole altis joidenkin kielten
JSON-lukujen edestakaisen muunnoksen erityispiirteille.

## Mikä salaisuus?

| Lähde                                                                                       | Salaisuus                                                                                                                       |
| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| [Webhook-päätepiste](/fi/webhooks/endpoints) (`/v1/developer/webhook-endpoints`)            | Päätepistekohtainen `secret` (48 heksadesimaalimerkkiä), joka palautetaan kerran luotaessa                                      |
| [Vanha yhden URL-osoitteen webhook](/api-reference/organizations#legacy-single-url-webhook) | Organisaatiokohtainen `secret`, joka palautetaan pyynnöllä `GET /v1/webhook`                                                    |
| [Työkalupäätepisteen kutsu](/fi/tools/overview) (suora kutsu osoitteeseen `endpoint.url`)   | **Organisaatiotason webhook-salaisuus** (sama kuin vanhassa yhden URL-osoitteen webhookissa) — ei päätepistekohtainen salaisuus |

Tallenna salaisuus salaisuuksien hallintaan tai ympäristömuuttujaan — älä koskaan commitoi sitä.

## Viitetoteutukset

Kaikki neljä varmentavat pyynnön raakarungon:

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

## Kehyskohtainen kytkentä

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

## Työkalukutsujen varmentaminen

Kun agentti kutsuu jotakin
[funktiotyökaluistasi](/fi/tools/overview) suoraan (työkalulla on
`endpoint`), pyyntö sisältää kaksi ThunderPhone-otsaketta määritettyjen
`endpoint.headers`-otsakkeidesi lisäksi:

* `X-ThunderPhone-Call-ID` — käynnissä olevan puhelun numeerinen tunniste.
* `X-ThunderPhone-Signature` — HMAC-SHA256, joka on avattu
  **organisaatiotason webhook-salaisuudellasi**, täsmälleen pyyntörungon tavujen perusteella.

Sama `verify()`-apuohjelma toimii sellaisenaan, mutta huomioi kaksi asiaa:

1. **`GET`- / `DELETE`-työkaluilla ei ole runkoa.** Argumentit välitetään kyselyparametreina, ja allekirjoitus lasketaan **tyhjälle tavumerkkijonolle** — siis `verify(b"", sig, secret)` (Python) tai `verify(Buffer.alloc(0), sig, secret)` (Node). Älä tiivistä kyselymerkkijonoa.
2. **Organisaatioilla, joille ei ole määritetty vanhaa webhookia, ei ole organisaatiosalaisuutta.** Tällöin työkalukutsut sisältävät vain `X-ThunderPhone-Call-ID`-otsakkeen eivätkä allekirjoitusotsaketta. Määritä vanha webhook (`PUT /v1/webhook`) saadaksesi allekirjoitussalaisuuden tai todenna työkalukutsut omalla otsakkeellasi `endpoint.headers`-kentän kautta.

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

Webhook-**tilan** työkalujen välitys (työkalut, joilla ei ole `endpoint`-määritystä ja jotka toimitetaan organisaatiosi webhookiin muodossa `telephony.tool` / `web.tool`) on tavallinen allekirjoitettu webhook — yllä oleva vakiomenettely pätee. Katso molemmat pyyntömuodot kohdasta [Funktiotyökalut](/fi/tools/overview).

## Yleiset sudenkuopat

<AccordionGroup>
  <Accordion title="Uudelleensarjoittaminen oletusmuotoilulla">
    Rungon jäsentäminen ja uudelleenkirjoittaminen JSON-kirjastosi
    oletusasetuksilla (välilyönnit merkkien `,` / `:` jälkeen, lisäysjärjestyksessä olevat avaimet) tuottaa
    eri tavut ja rikkoo HMACin. Vahvista raaka runko — tai jos sinun on sarjoitettava se uudelleen, vastaa täsmälleen kanonista muotoamme: lajitellut
    avaimet, tiiviit erotinmerkit, UTF-8.
  </Accordion>

  <Accordion title="Kehys jäsentää JSONin automaattisesti">
    Expressin `express.json()`-väliohjelmisto kuluttaa rungon virran
    ja menetät raa'at tavut. Käytä `express.raw()`-toimintoa erityisesti webhook-reitillä
    tai puskuroi raaka runko esiväliohjelmistossa.
    Sama koskee NestJS:ää / Koaa — tarkista niiden "raw body" -dokumentaatio.
  </Accordion>

  <Accordion title="Ajoitukselle altis vertailu">
    `expected === signature` JS:ssä tai `expected == signature` Pythonissa ovat
    ajoitukseltaan vaihtelevia vertailuja. Käytä vastaavasti `crypto.timingSafeEqual`
    tai `hmac.compare_digest`-toimintoa. Suorituskykyeroa
    ei käytännössä ole.
  </Accordion>

  <Accordion title="Väärä salaisuus työkalupäätepisteille">
    Suorat työkalupäätepistekutsut allekirjoitetaan **organisaatiotason webhook-salaisuudella**
    (`GET /v1/webhook`) — ei millään päätepistekohtaisella salaisuudella,
    joka tulee polusta `/v1/developer/webhook-endpoints`. Käytä samaa `verify()`
    funktiota uudelleen, mutta varmista, että syötät sille organisaation salaisuuden työkalureiteillä.
  </Accordion>

  <Accordion title="Kyselymerkkijonon hajauttaminen GET/DELETE-työkaluissa">
    Rungottomissa työkalumenetelmissä allekirjoitus kattaa tyhjän tavumerkkijonon,
    jolloin käytössä säilyy yksi yleispätevä toimintatapa: HMAC raakaan pyyntörunkoon,
    olipa se mikä tahansa. URL-osoitteen tai kyselymerkkijonon hajautus ei koskaan täsmää.
  </Accordion>

  <Accordion title="401-tilakoodin palauttamatta jättäminen ristiriitatilanteessa">
    Tilakoodin 200 palauttaminen epäonnistuneessa vahvistuksessa tekee käsittelijästä
    toistohyökkäyksen kohteen. Vastaa aina muulla kuin 2xx-tilakoodilla, jos vahvistus epäonnistuu.
  </Accordion>
</AccordionGroup>

***

## Seuraavat vaiheet

<CardGroup cols={2}>
  <Card title="Webhookien yleiskatsaus" icon="bolt" href="/fi/webhooks/overview">
    Toimitussemantiikka, uudelleenyritykset, lähde-IP-osoitteet.
  </Card>

  <Card title="Webhook-päätepisteet" icon="plug" href="/fi/webhooks/endpoints">
    Hallitse useita URL-osoitteita, kierrätä salaisuuksia.
  </Card>

  <Card title="Funktiotyökalut" icon="screwdriver-wrench" href="/fi/tools/overview">
    Kaksi työkalukutsupolkua ja niiden pyyntömuodot.
  </Card>

  <Card title="Työkalintegraatiot" icon="wrench" href="/fi/guides/build-tool-integration">
    Rakenna täydellinen työkaluihin perustuva integraatio alusta loppuun.
  </Card>
</CardGroup>
