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

# Ověření podpisů webhooků

> Každý webhook a každý požadavek nástroje z ThunderPhone je podepsán. Ověřte jednou, používejte všude.

Každý požadavek, který odesíláme na váš server — doručení webhooku i
volání koncového bodu nástroje — obsahuje podpis HMAC-SHA256 v hlavičce
`X-ThunderPhone-Signature`. Ověření nastavte jednou správně a stejnou pomocnou funkci
použijte ve všech handlerech.

## Algoritmus

1. Přečtěte **nezpracované** tělo požadavku — přesné bajty, které jsme vám odeslali metodou POST.
2. Vypočítejte `hmac_sha256(secret, body).hexdigest()`.
3. Porovnejte jej v **konstantním čase** s `X-ThunderPhone-Signature`.
   (Naivní porovnání řetězců odhaluje informace o časování.)

Podepisujeme přesně ty bajty, které přenášíme, takže ověření nezpracovaného těla
vždy funguje. Tyto bajty jsou také **kanonickou serializací JSON**
datové části — klíče jsou řazeny abecedně, oddělovače jsou kompaktní
(`,` a `:` bez mezer), kódování je UTF-8. To vám poskytuje druhý, zcela
ekvivalentní postup, když váš framework zpřístupňuje pouze parsovaný JSON:
proveďte kanonickou reserializaci a nad ní vypočítejte HMAC.

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

Upřednostněte nezpracované tělo — je to o jeden krok méně a vyhnete se tím zvláštnostem
při opakovaném převodu čísel JSON v některých jazycích.

## Který secret?

| Zdroj                                                                                     | Secret                                                                                                                           |
| ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| [Koncový bod webhooku](/cs/webhooks/endpoints) (`/v1/developer/webhook-endpoints`)        | `secret` pro konkrétní koncový bod (48 hexadecimálních znaků), vrácený jednou při vytvoření                                      |
| [Starší webhook s jednou URL](/api-reference/organizations#legacy-single-url-webhook)     | `secret` pro organizaci vrácený při `GET /v1/webhook`                                                                            |
| [Volání koncového bodu nástroje](/cs/tools/overview) (přímé volání vašeho `endpoint.url`) | **Secret webhooku na úrovni organizace** (stejný jako pro starší webhook s jednou URL) — nikoli secret pro konkrétní koncový bod |

Uložte secret do správce tajemství nebo proměnné prostředí — nikdy jej necommitujte.

## Referenční implementace

Všechny čtyři ověřují nezpracované tělo požadavku:

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

## Zapojení specifické pro framework

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

## Ověřování volání nástrojů

Když agent přímo vyvolá některý z vašich
[funkčních nástrojů](/cs/tools/overview) (nástroj má
`endpoint`), požadavek obsahuje dvě hlavičky ThunderPhone spolu
s nakonfigurovanými `endpoint.headers`:

* `X-ThunderPhone-Call-ID` — číselné ID probíhajícího hovoru.
* `X-ThunderPhone-Signature` — HMAC-SHA256 s klíčem ve formě vašeho
  **tajného klíče webhooku na úrovni organizace** nad přesnými bajty
  těla požadavku.

Stejný pomocník `verify()` funguje beze změny, se dvěma rozdíly:

1. **Nástroje `GET` / `DELETE` nemají tělo.** Argumenty se předávají jako
   parametry dotazu a podpis se vypočítá nad **prázdným bajtovým
   řetězcem** — tedy `verify(b"", sig, secret)` (Python) nebo
   `verify(Buffer.alloc(0), sig, secret)` (Node). Řetězec dotazu
   **nehashujte**.
2. **Organizace bez nakonfigurovaného staršího webhooku nemají tajný klíč organizace.** V
   takovém případě volání nástrojů obsahují pouze `X-ThunderPhone-Call-ID` a žádnou
   hlavičku podpisu. Nakonfigurujte starší webhook
   (`PUT /v1/webhook`) pro získání podpisového tajného klíče, nebo ověřujte volání
   nástrojů vlastní hlavičkou prostřednictvím `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)
    ...
```

Odesílání nástrojů v **režimu webhooku** (nástroje bez `endpoint`, doručované
na webhook vaší organizace jako `telephony.tool` / `web.tool`) je běžný
podepsaný webhook — platí pro něj výše uvedený standardní postup. Oba tvary
požadavků najdete v dokumentaci [Funkční nástroje](/cs/tools/overview).

## Běžné chyby

<AccordionGroup>
  <Accordion title="Opětovná serializace s výchozím formátováním">
    Parsování těla a jeho opětovný výpis s výchozím nastavením vaší knihovny JSON
    (mezery po `,` / `:`, klíče v pořadí vložení) vytvoří
    jiné bajty a poruší HMAC. Ověřujte nezpracované tělo — nebo pokud jej
    musíte znovu serializovat, přesně dodržte náš kanonický formát: seřazené
    klíče, kompaktní oddělovače, UTF-8.
  </Accordion>

  <Accordion title="Framework automaticky parsuje JSON">
    Middleware `express.json()` v Expressu spotřebuje stream těla
    a přijdete o nezpracované bajty. Použijte `express.raw()` přímo pro cestu webhooku,
    nebo nezpracované tělo uložte do bufferu v předběžném middleware.
    Totéž platí pro NestJS / Koa — projděte si jejich dokumentaci k „raw body“.
  </Accordion>

  <Accordion title="Porovnání nebezpečné z hlediska časování">
    `expected === signature` v JS nebo `expected == signature` v
    Pythonu jsou porovnání závislá na časování. Použijte
    `crypto.timingSafeEqual`, respektive `hmac.compare_digest`.
    Rozdíl ve výkonu je nulový.
  </Accordion>

  <Accordion title="Nesprávný secret pro endpointy nástrojů">
    Přímá volání endpointů nástrojů jsou podepsána pomocí **webhook secretu
    na úrovni organizace** (`GET /v1/webhook`) — nikoli pomocí secretu
    konkrétního endpointu z `/v1/developer/webhook-endpoints`. Znovu použijte stejnou funkci
    `verify()`, ale ujistěte se, že pro cesty nástrojů předáváte secret organizace.
  </Accordion>

  <Accordion title="Hashování query stringu u nástrojů GET/DELETE">
    U metod nástrojů bez těla podpis pokrývá prázdný řetězec bajtů,
    což zachovává jeden univerzální postup: vypočítejte HMAC z nezpracovaného těla požadavku,
    ať je jakékoli. Hashování adresy URL nebo query stringu nikdy nebude odpovídat.
  </Accordion>

  <Accordion title="Nevracení 401 při neshodě">
    Vrácení 200 při neúspěšném ověření z handleru vytváří cíl pro replay útoky.
    Pokud ověření selže, vždy odpovězte jiným stavem než 2xx.
  </Accordion>
</AccordionGroup>

***

## Další kroky

<CardGroup cols={2}>
  <Card title="Přehled webhooků" icon="bolt" href="/cs/webhooks/overview">
    Sémantika doručování, opakování, zdrojové IP adresy.
  </Card>

  <Card title="Endpointy webhooků" icon="plug" href="/cs/webhooks/endpoints">
    Spravujte více adres URL, rotujte secrety.
  </Card>

  <Card title="Function Tools" icon="screwdriver-wrench" href="/cs/tools/overview">
    Dvě cesty volání nástrojů a tvary jejich požadavků.
  </Card>

  <Card title="Integrace nástrojů" icon="wrench" href="/cs/guides/build-tool-integration">
    Vytvořte kompletní integraci s podporou nástrojů od začátku do konce.
  </Card>
</CardGroup>
