---
title: "Provjerite potpise webhookova"
description: "Svaki webhook i svaki zahtjev alata koji ThunderPhone šalje potpisan je. Jednom provjerite potpis prema ovdje navedenom postupku, a zatim istu provjeru ponovno upotrijebite na svakom endpointu koji pokrećete."
---

Svaki zahtjev koji šaljemo vašem poslužitelju — isporuke web-dojavnika i
pozivi krajnjih točaka alata — sadrži HMAC-SHA256 potpis u
zaglavlju `X-ThunderPhone-Signature`. Jednom ispravno implementirajte provjeru i
uključite isti pomoćnik u svaki rukovatelj.

## Algoritam

1. Pročitajte **neobrađeno** tijelo zahtjeva — točne bajtove koje smo vam poslali metodom POST.
2. Izračunajte `hmac_sha256(secret, body).hexdigest()`.
3. Usporedite u **konstantnom vremenu** sa zaglavljem `X-ThunderPhone-Signature`.
   (Naivna usporedba nizova otkriva informacije o vremenu.)

Potpisujemo točno one bajtove koje prenosimo, stoga provjera neobrađenog tijela
uvijek funkcionira. Ti bajtovi ujedno su **kanonska JSON serijalizacija**
korisnog sadržaja — ključevi poredani abecedno, sažeti razdjelnici
(`,` i `:` bez razmaka), UTF-8. To vam pruža drugi, potpuno
ekvivalentan postupak kada vaš okvir izlaže samo parsirani JSON:
ponovno ga serijalizirajte kanonski i 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")
```

Dajte prednost neobrađenom tijelu — to je jedan korak manje i otporno je na
specifičnosti ponovnog pretvaranja JSON brojeva u nekim jezicima.

## Koja tajna?

| Izvor | Tajna |
|--------|--------|
| [Krajnja točka web-dojavnika](/hr/webhooks/endpoints) (`/v1/developer/webhook-endpoints`) | `secret` za pojedinu krajnju točku (48 heksadekadskih znakova) vraćen jednokratno pri stvaranju |
| [Naslijeđeni web-dojavnik s jednim URL-om](/api-reference/organizations#legacy-single-url-webhook) | `secret` za organizaciju vraćen pri `GET /v1/webhook` |
| [Poziv krajnje točke alata](/hr/tools/overview) (izravni poziv vašeg `endpoint.url`) | **Tajna web-dojavnika na razini organizacije** (ista kao za naslijeđeni web-dojavnik s jednim URL-om) — ne tajna pojedine krajnje točke |

Pohranite tajnu u upravitelj tajni ili varijablu okruženja — nikada je nemojte predati u repozitorij.

## Referentne implementacije

Sve četiri provjeravaju neobrađeno tijelo zahtjeva:

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

## Povezivanje specifično za okvir

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

## Provjera poziva alata

Kada agent izravno pozove jedan od vaših
[funkcijskih alata](/hr/tools/overview) (alat ima
`endpoint`), zahtjev uz vaša konfigurirana zaglavlja `endpoint.headers`
sadrži dva zaglavlja ThunderPhonea:

- `X-ThunderPhone-Call-ID` — numerički ID aktivnog poziva.
- `X-ThunderPhone-Signature` — HMAC-SHA256 s ključem
  **tajne webhooka na razini organizacije**, izračunat nad točnim
  bajtovima tijela zahtjeva.

Isti pomoćnik `verify()` radi bez izmjena, uz dvije pojedinosti:

1. **Alati `GET` / `DELETE` nemaju tijelo.** Argumenti se prenose kao
   parametri upita, a potpis se izračunava nad **praznim nizom
   bajtova** — stoga `verify(b"", sig, secret)` (Python) ili
   `verify(Buffer.alloc(0), sig, secret)` (Node). **Nemojte** raspršivati
   niz upita.
2. **Organizacije bez konfiguriranog naslijeđenog webhooka nemaju tajnu organizacije.** U
   tom slučaju pozivi alata sadrže samo `X-ThunderPhone-Call-ID`, bez
   zaglavlja potpisa. Konfigurirajte naslijeđeni webhook
   (`PUT /v1/webhook`) da biste dobili tajnu za potpisivanje ili
   autentificirajte pozive alata vlastitim zaglavljem putem `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)
    ...
```

Slanje alata u webhook-**načinu** rada (alati bez `endpoint`, isporučeni
na webhook vaše organizacije kao `telephony.tool` / `web.tool`) običan je
potpisani webhook — primjenjuje se standardni postupak iznad. Pogledajte
[Funkcijski alati](/hr/tools/overview) za oba oblika zahtjeva.

## Uobičajene zamke

<AccordionGroup>
  <Accordion title="Ponovna serijalizacija sa zadanim formatiranjem">
    Raščlanjivanje tijela i njegovo ponovno zapisivanje sa zadanim
    postavkama vaše JSON biblioteke (razmaci nakon `,` / `:`, ključevi prema redoslijedu umetanja) stvara
    različite bajtove i kvari HMAC. Provjerite neobrađeno tijelo — ili, ako ga
    morate ponovno serijalizirati, točno uskladite naš kanonski oblik: sortirani
    ključevi, sažeti razdjelnici, UTF-8.
  </Accordion>

  <Accordion title="Okvir automatski raščlanjuje JSON">
    Expressov međuprogram `express.json()` troši tok tijela
    i gubite neobrađene bajtove. Upotrijebite `express.raw()` posebno na webhook
    ruti ili spremite neobrađeno tijelo u međuprogramu koji se izvršava ranije.
    Isto vrijedi za NestJS / Koa — provjerite njihovu dokumentaciju za „neobrađeno tijelo”.
  </Accordion>

  <Accordion title="Usporedba nesigurna na vremenske napade">
    `expected === signature` u JS-u ili `expected == signature` u
    Pythonu usporedbe su s promjenjivim vremenom izvršavanja. Upotrijebite `crypto.timingSafeEqual`
    odnosno `hmac.compare_digest`. Razlika u performansama
    je zanemariva.
  </Accordion>

  <Accordion title="Pogrešna tajna za krajnje točke alata">
    Izravni pozivi krajnjih točaka alata potpisuju se pomoću **tajne webhooka
    na razini organizacije** (`GET /v1/webhook`) — a ne pomoću tajne za pojedinačnu krajnju točku
    iz `/v1/developer/webhook-endpoints`. Ponovno upotrijebite istu funkciju `verify()`,
    ali provjerite prosljeđujete li joj tajnu organizacije na rutama alata.
  </Accordion>

  <Accordion title="Sažimanje niza upita za GET/DELETE alate">
    Za metode alata bez tijela potpis obuhvaća prazan niz
    bajtova, čime se zadržava jedan univerzalni postupak: izračunajte HMAC za neobrađeno tijelo zahtjeva,
    kakvo god ono bilo. Sažimanje URL-a ili niza upita nikada se neće podudarati.
  </Accordion>

  <Accordion title="Nevraćanje odgovora 401 pri nepodudaranju">
    Vraćanje odgovora 200 nakon neuspjele provjere čini rukovatelj metom
    ponavljanja zahtjeva. Uvijek odgovorite statusom koji nije 2xx ako provjera ne uspije.
  </Accordion>
</AccordionGroup>

---

## Sljedeći koraci

<CardGroup cols={2}>
  <Card title="Pregled webhookova" icon="bolt" href="/hr/webhooks/overview">
    Semantika isporuke, ponovni pokušaji, izvorne IP adrese.
  </Card>
  <Card title="Krajnje točke webhookova" icon="plug" href="/hr/webhooks/endpoints">
    Upravljajte višestrukim URL-ovima, rotirajte tajne.
  </Card>
  <Card title="Funkcijski alati" icon="screwdriver-wrench" href="/hr/tools/overview">
    Dva puta pozivanja alata i njihovi oblici zahtjeva.
  </Card>
  <Card title="Integracije alata" icon="wrench" href="/hr/guides/build-tool-integration">
    Izradite potpunu integraciju podržanu alatom od početka do kraja.
  </Card>
</CardGroup>
