---
title: "Preverite podpise webhookov"
description: "Vsak webhook in zahteva za orodje, ki ju pošlje ThunderPhone, sta podpisana. Podpis enkrat preverite s tukajšnjim postopkom, nato pa isto preverjanje uporabite na vsaki končni točki, ki jo izvajate."
---

Vsaka zahteva, ki jo pošljemo vašemu strežniku — dostave webhookov in
klici končnih točk orodij — vsebuje podpis HMAC-SHA256 v glavi
`X-ThunderPhone-Signature`. Preverjanje pravilno nastavite enkrat in
isti pomožni program vključite v vsak obdelovalnik.

## Algoritem

1. Preberite **surovo** telo zahteve — natančne bajte, ki smo vam jih poslali z zahtevo POST.
2. Izračunajte `hmac_sha256(secret, body).hexdigest()`.
3. V **konstantnem času** primerjajte z `X-ThunderPhone-Signature`.
   (Preprosta primerjava nizov razkriva informacije o času izvajanja.)

Podpišemo natanko bajte, ki jih prenesemo, zato preverjanje surovega telesa
vedno deluje. Ti bajti so tudi **kanonična serializacija JSON**
koristnega tovora — ključi so razvrščeni po abecedi, ločila so strnjena
(`,` in `:` brez presledkov), kodiranje pa je UTF-8. To vam ponuja drugi,
popolnoma enakovreden postopek, kadar vaše ogrodje omogoča le razčlenjeni JSON:
znova ga serializirajte kanonično in nad njim izračunajte HMAC.

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

Prednost dajte surovemu telesu — to je en korak manj in je odporno na posebnosti
ponovnega pretvarjanja števil JSON v nekaterih jezikih.

## Katera skrivnost?

| Vir | Skrivnost |
|--------|--------|
| [Končna točka webhooka](/sl/webhooks/endpoints) (`/v1/developer/webhook-endpoints`) | `secret` za posamezno končno točko (48 šestnajstiških znakov), vrnjen enkrat ob ustvarjanju |
| [Podedovani webhook z enim URL-jem](/api-reference/organizations#legacy-single-url-webhook) | `secret` za posamezno organizacijo, vrnjen pri `GET /v1/webhook` |
| [Klic končne točke orodja](/sl/tools/overview) (neposredni klic vaše `endpoint.url`) | **Skrivnost webhooka na ravni organizacije** (ista kot pri podedovanem webhooku z enim URL-jem) — ne skrivnost za posamezno končno točko |

Skrivnost shranite v upravitelju skrivnosti ali spremenljivki okolja — nikoli je ne potrdite v repozitorij.

## Referenčne implementacije

Vse štiri preverjajo surovo telo zahteve:

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

## Povezovanje, specifično za ogrodje

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

## Preverjanje klicev orodij

Ko agent neposredno prikliče eno od vaših
[funkcijskih orodij](/sl/tools/overview) (orodje ima
`endpoint`), zahteva poleg vaših konfiguriranih
`endpoint.headers` vsebuje dve glavi ThunderPhone:

- `X-ThunderPhone-Call-ID` — številčni ID aktivnega klica.
- `X-ThunderPhone-Signature` — HMAC-SHA256 z vašim
  **skrivnim ključem webhooka na ravni organizacije** kot ključem, izračunan nad natančnimi bajti telesa zahteve.

Isti pomožnik `verify()` deluje nespremenjeno, z dvema posebnostma:

1. **Orodja `GET` / `DELETE` nimajo telesa.** Argumenti se posredujejo kot parametri poizvedbe, podpis pa se izračuna nad **praznim bajtnim nizom** — torej `verify(b"", sig, secret)` (Python) ali
   `verify(Buffer.alloc(0), sig, secret)` (Node). Ne zgoščujte niza poizvedbe.
2. **Organizacije brez konfiguriranega podedovanega webhooka nimajo skrivnega ključa organizacije.** V tem primeru klici orodij vsebujejo samo `X-ThunderPhone-Call-ID` in nobene glave s podpisom. Konfigurirajte podedovani webhook
   (`PUT /v1/webhook`), da pridobite skrivni ključ za podpisovanje, ali avtenticirajte klice orodij z lastno glavo prek `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)
    ...
```

Usmerjanje orodij v **načinu webhooka** (orodja brez `endpoint`, dostavljena
na webhook vaše organizacije kot `telephony.tool` / `web.tool`) je običajen
podpisan webhook — uporabite zgornji standardni postopek. Za oba formata zahtevkov glejte
[Funkcijska orodja](/sl/tools/overview).

## Pogoste pasti

<AccordionGroup>
  <Accordion title="Ponovno serializiranje s privzetim oblikovanjem">
    Razčlenjevanje telesa in njegovo ponovno izpisovanje s privzetimi
    nastavitvami knjižnice JSON (presledki za `,` / `:`, ključi v vrstnem redu vstavljanja) ustvari
    drugačne bajte in pokvari HMAC. Preverite neobdelano telo — ali pa, če ga
    morate ponovno serializirati, natančno uporabite našo kanonično obliko: razvrščene
    ključe, strnjena ločila, UTF-8.
  </Accordion>

  <Accordion title="Ogrodje samodejno razčleni JSON">
    Vmesna programska oprema `express.json()` v Expressu porabi tok telesa
    in izgubite neobdelane bajte. Posebej za pot webhooka uporabite `express.raw()`
    ali shranite neobdelano telo v predhodni vmesni programski opremi.
    Enako velja za NestJS / Koa — preverite njihovo dokumentacijo za »raw body«.
  </Accordion>

  <Accordion title="Primerjava, nevarna glede časa izvajanja">
    `expected === signature` v JS ali `expected == signature` v
    Pythonu sta primerjavi s spremenljivim časom izvajanja. Uporabite `crypto.timingSafeEqual`
    oziroma `hmac.compare_digest`. Razlike v zmogljivosti ni.
  </Accordion>

  <Accordion title="Napačna skrivnost za končne točke orodij">
    Neposredni klici končnih točk orodij so podpisani s **skrivnostjo webhooka
    na ravni organizacije** (`GET /v1/webhook`) — ne s skrivnostjo posamezne končne točke
    iz `/v1/developer/webhook-endpoints`. Znova uporabite isto funkcijo `verify()`,
    vendar poskrbite, da ji na poteh orodij posredujete skrivnost organizacije.
  </Accordion>

  <Accordion title="Zgoščevanje poizvedbenega niza pri orodjih GET/DELETE">
    Pri metodah orodij brez telesa podpis zajema prazen bajtni
    niz, kar ohranja en univerzalen postopek: izračunajte HMAC neobdelanega telesa zahteve,
    ne glede na njegovo vsebino. Zgoščevanje URL-ja ali poizvedbenega niza se nikoli ne bo ujemalo.
  </Accordion>

  <Accordion title="Nevračanje odgovora 401 ob neujemanju">
    Vračanje 200 ob neuspelem preverjanju spremeni upravljalnik v cilj
    za ponovitvene napade. Če preverjanje ne uspe, vedno odgovorite s kodo, ki ni 2xx.
  </Accordion>
</AccordionGroup>

---

## Naslednji koraki

<CardGroup cols={2}>
  <Card title="Pregled webhookov" icon="bolt" href="/sl/webhooks/overview">
    Semantika dostave, ponovni poskusi, izvorni IP-naslovi.
  </Card>
  <Card title="Končne točke webhookov" icon="plug" href="/sl/webhooks/endpoints">
    Upravljajte več URL-jev, zamenjajte skrivnosti.
  </Card>
  <Card title="Funkcijska orodja" icon="screwdriver-wrench" href="/sl/tools/overview">
    Dve poti za priklic orodij in njuni obliki zahtev.
  </Card>
  <Card title="Integracije orodij" icon="wrench" href="/sl/guides/build-tool-integration">
    Izdelajte celovito integracijo, podprto z orodji, od začetka do konca.
  </Card>
</CardGroup>
