---
title: "Overenie podpisov webhookov"
description: "Každý webhook a každá požiadavka nástroja, ktorú ThunderPhone odosiela, je podpísaná. Podpis raz overte podľa postupu uvedeného tu a potom rovnakú kontrolu použite na každom spustenom koncovom bode."
---

Každá požiadavka, ktorú odosielame na váš server — doručenia webhookov a
volania koncových bodov nástrojov — obsahuje podpis HMAC-SHA256 v hlavičke
`X-ThunderPhone-Signature`. Overenie nastavte raz správne a
rovnakého pomocníka použite v každom obslužnom programe.

## Algoritmus

1. Prečítajte **surové** telo požiadavky — presné bajty, ktoré sme vám odoslali prostredníctvom POST.
2. Vypočítajte `hmac_sha256(secret, body).hexdigest()`.
3. Porovnajte v **konštantnom čase** s hodnotou `X-ThunderPhone-Signature`.
   (Naivné porovnanie reťazcov prezrádza časové informácie.)

Podpisujeme presne tie bajty, ktoré prenášame, takže overenie surového tela
funguje vždy. Tieto bajty sú zároveň **kanonickou serializáciou JSON**
obsahu — kľúče zoradené abecedne, kompaktné oddeľovače
(`,` a `:` bez medzier), UTF-8. To vám poskytuje druhý, úplne
ekvivalentný postup, keď váš framework sprístupňuje iba spracovaný JSON:
znova ho kanonicky serializujte a vypočítajte HMAC z neho.

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

Uprednostnite surové telo — je to o jeden krok menej a je odolné voči
zvláštnostiam opätovného prevodu čísel JSON v niektorých jazykoch.

## Ktorý tajný kľúč?

| Zdroj | Tajný kľúč |
|--------|--------|
| [Koncový bod webhooku](/sk/webhooks/endpoints) (`/v1/developer/webhook-endpoints`) | `secret` pre konkrétny koncový bod (48 hexadecimálnych znakov), vrátený iba pri vytvorení |
| [Starší webhook s jednou URL](/api-reference/organizations#legacy-single-url-webhook) | `secret` pre organizáciu vrátený pri `GET /v1/webhook` |
| [Volanie koncového bodu nástroja](/sk/tools/overview) (priame volanie vášho `endpoint.url`) | **Tajný kľúč webhooku na úrovni organizácie** (rovnaký ako pre starší webhook s jednou URL) — nie tajný kľúč pre konkrétny koncový bod |

Tajný kľúč uložte do správcu tajných údajov alebo premennej prostredia — nikdy ho neukladajte do repozitára.

## Referenčné implementácie

Všetky štyri overujú surové telo požiadavky:

<CodeGroup>
```python Python
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
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
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
require "openssl"

def verify(body, signature, secret)
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
  Rack::Utils.secure_compare(expected, signature.to_s)
end
```
</CodeGroup>

## Zapojenie špecifické pre framework

<CodeGroup>
```python FastAPI
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
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
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>

## Overovanie volaní nástrojov

Keď agent priamo vyvolá jeden z vašich
[funkčných nástrojov](/sk/tools/overview) (nástroj má
`endpoint`), požiadavka obsahuje dve hlavičky ThunderPhone spolu
s nakonfigurovanými hlavičkami `endpoint.headers`:

- `X-ThunderPhone-Call-ID` — číselné ID prebiehajúceho hovoru.
- `X-ThunderPhone-Signature` — HMAC-SHA256 s kľúčom vo forme vášho
  **tajného kľúča webhooku na úrovni organizácie** nad presnými bajtmi tela požiadavky.

Rovnaký pomocník `verify()` funguje bez zmien, s dvoma odlišnosťami:

1. **Nástroje `GET` / `DELETE` nemajú telo.** Argumenty sa prenášajú ako parametre dopytu
   a podpis sa vypočíta nad **prázdnym bajtovým
   reťazcom** — teda `verify(b"", sig, secret)` (Python) alebo
   `verify(Buffer.alloc(0), sig, secret)` (Node). Reťazec dopytu
   **nehášujte**.
2. **Organizácie bez nakonfigurovaného staršieho webhooku nemajú tajný kľúč organizácie.** V
   takom prípade volania nástrojov obsahujú iba `X-ThunderPhone-Call-ID` a žiadnu
   hlavičku podpisu. Nakonfigurujte starší webhook
   (`PUT /v1/webhook`), aby ste získali tajný kľúč na podpisovanie, alebo overujte volania
   nástrojov vlastnou hlavičkou prostredníctvom `endpoint.headers`.

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

Dispečovanie nástrojov v **režime** webhooku (nástroje bez `endpoint`, doručené
do webhooku vašej organizácie ako `telephony.tool` / `web.tool`) je bežný
podpísaný webhook — platí štandardný postup uvedený vyššie. Pre oba
tvary požiadaviek si pozrite [Funkčné nástroje](/sk/tools/overview).

## Bežné úskalia

<AccordionGroup>
  <Accordion title="Opätovná serializácia s predvoleným formátovaním">
    Spracovanie tela a jeho opätovný výpis s predvolenými nastaveniami
    vašej knižnice JSON (medzery po `,` / `:`, kľúče v poradí vloženia) vytvorí
    odlišné bajty a naruší HMAC. Overujte nespracované telo — alebo ak ho
    musíte znovu serializovať, presne dodržte náš kanonický formát: zoradené
    kľúče, kompaktné oddeľovače, UTF-8.
  </Accordion>

  <Accordion title="Framework automaticky spracúva JSON">
    Middleware `express.json()` v Express spotrebuje stream tela
    a stratíte nespracované bajty. Použite `express.raw()` konkrétne na
    trase webhooku alebo uložte nespracované telo do vyrovnávacej pamäte v
    predbežnom middleware. To isté platí pre NestJS / Koa — pozrite si ich
    dokumentáciu k „raw body“.
  </Accordion>

  <Accordion title="Porovnanie nebezpečné z hľadiska časovania">
    `expected === signature` v JS alebo `expected == signature` v
    Pythone sú porovnania s premenlivým časovaním. Použite príslušne `crypto.timingSafeEqual`
    alebo `hmac.compare_digest`. Rozdiel vo výkone je zanedbateľný.
  </Accordion>

  <Accordion title="Nesprávny secret pre endpointy nástrojov">
    Priame volania endpointov nástrojov sú podpisované pomocou **secretu webhooku
    na úrovni organizácie** (`GET /v1/webhook`) — nie secretom konkrétneho endpointu
    z `/v1/developer/webhook-endpoints`. Znovu použite rovnakú funkciu `verify()`,
    ale uistite sa, že na trasách nástrojov jej odovzdávate secret organizácie.
  </Accordion>

  <Accordion title="Hašovanie query stringu pri nástrojoch GET/DELETE">
    Pri metódach nástrojov bez tela podpis pokrýva prázdny bajtový
    reťazec, takže stačí jeden univerzálny postup: vypočítajte HMAC z nespracovaného
    tela požiadavky, nech je akékoľvek. Hašovanie URL alebo query stringu sa nikdy nebude zhodovať.
  </Accordion>

  <Accordion title="Nevrátenie 401 pri nezhode">
    Vrátenie 200 pri neúspešnom overení robí z obsluhy cieľ opakovaných
    útokov. Ak overenie zlyhá, vždy odpovedzte stavom iným než 2xx.
  </Accordion>
</AccordionGroup>

---

## Ďalšie kroky

<CardGroup cols={2}>
  <Card title="Prehľad webhookov" icon="bolt" href="/sk/webhooks/overview">
    Sémantika doručovania, opakovania, zdrojové IP adresy.
  </Card>
  <Card title="Endpointy webhookov" icon="plug" href="/sk/webhooks/endpoints">
    Spravujte viacero URL adries, rotujte secrety.
  </Card>
  <Card title="Funkčné nástroje" icon="screwdriver-wrench" href="/sk/tools/overview">
    Dve cesty vyvolania nástrojov a tvary ich požiadaviek.
  </Card>
  <Card title="Integrácie nástrojov" icon="wrench" href="/sk/guides/build-tool-integration">
    Vytvorte kompletnú integráciu založenú na nástrojoch od začiatku do konca.
  </Card>
</CardGroup>
