---
title: "Проверите потписе веб-хукова"
description: "Сваки захтев веб-хука и алатке који ThunderPhone шаље је потписан. Једном проверите потпис помоћу овде наведеног поступка, а затим исту проверу поново користите на свакој крајњој тачки коју покрећете."
---

Сваки захтев који шаљемо вашем серверу — webhook испоруке и
позиви крајњих тачака алата — садржи HMAC-SHA256 потпис у
заглављу `X-ThunderPhone-Signature`. Једном правилно подесите верификацију и
укључите исти помоћни програм у сваки обрађивач.

## Алгоритам

1. Прочитајте **сирово** тело захтева — тачне бајтове које смо вам послали методом POST.
2. Израчунајте `hmac_sha256(secret, body).hexdigest()`.
3. Упоредите у **константном времену** са `X-ThunderPhone-Signature`.
   (Наивно поређење стрингова открива информације о времену.)

Потписујемо тачно бајтове које преносимо, тако да верификација сировог тела
увек функционише. Ти бајтови су такође **канонска JSON серијализација**
садржаја — кључеви су сортирани по абецеди, раздвајачи су компактни
(`,` и `:` без размака), а кодирање је UTF-8. То вам даје други, потпуно
еквивалентан поступак када ваш оквир излаже само рашчлањени JSON:
поново га серијализујте канонски и израчунајте HMAC над њим.

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

Дајте предност сировом телу — има један корак мање и отпорно је на
необичности поновног претварања JSON бројева у неким језицима.

## Која тајна?

| Извор | Тајна |
|--------|--------|
| [Webhook крајња тачка](/sr/webhooks/endpoints) (`/v1/developer/webhook-endpoints`) | `secret` за сваку крајњу тачку (48 хексадецималних знакова) који се враћа једном при креирању |
| [Застарели webhook са једним URL-ом](/api-reference/organizations#legacy-single-url-webhook) | `secret` за сваку организацију који се враћа при `GET /v1/webhook` |
| [Позив крајње тачке алата](/sr/tools/overview) (директан позив вашег `endpoint.url`) | **Webhook тајна на нивоу организације** (иста као за застарели webhook са једним URL-ом) — не тајна за појединачну крајњу тачку |

Чувајте тајну у менаџеру тајни или променљивој окружења — никада је не предајте у репозиторијум.

## Референтне имплементације

Све четири верификују сирово тело захтева:

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

## Повезивање специфично за оквир

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

## Верификација позива алата

Када агент директно позове један од ваших
[алата функција](/sr/tools/overview) (алат има
`endpoint`), захтев садржи два ThunderPhone заглавља уз
ваша конфигурисана `endpoint.headers`:

- `X-ThunderPhone-Call-ID` — нумерички ИД активног позива.
- `X-ThunderPhone-Signature` — HMAC-SHA256, са кључем који је ваша
  **тајна вредност веб-хука на нивоу организације**, над тачним
  бајтовима тела захтева.

Иста помоћна функција `verify()` ради без измена, уз две напомене:

1. **Алати `GET` / `DELETE` немају тело.** Аргументи се преносе као
   параметри упита, а потпис се израчунава над **празним низом
   бајтова** — дакле `verify(b"", sig, secret)` (Python) или
   `verify(Buffer.alloc(0), sig, secret)` (Node). Немојте хеширати
   ниску упита.
2. **Организације без конфигурисаног застарелог веб-хука немају тајну
   вредност организације.** У том случају позиви алата садрже само
   `X-ThunderPhone-Call-ID` и немају заглавље потписа. Конфигуришите
   застарели веб-хук (`PUT /v1/webhook`) да бисте добили тајну вредност
   за потписивање или аутентификујте позиве алата сопственим заглављем
   преко `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)
    ...
```

Отпрема алата у **режиму** веб-хука (алати без `endpoint`, испоручени
на веб-хук ваше организације као `telephony.tool` / `web.tool`) је
обичан потписани веб-хук — важи стандардни поступак изнад. Погледајте
[Алати функција](/sr/tools/overview) за оба облика захтева.

## Уобичајене замке

<AccordionGroup>
  <Accordion title="Поновна серијализација са подразумеваним форматирањем">
    Парсирање тела и његово поновно исписивање помоћу подразумеваних
    подешавања Ваше JSON библиотеке (размаци после `,` / `:`, кључеви по редоследу уметања) даје
    различите бајтове и нарушава HMAC. Проверите сирово тело — или, ако
    морате поново да га серијализујете, тачно ускладите наш канонски облик:
    сортирани кључеви, компактни раздвајачи, UTF-8.
  </Accordion>

  <Accordion title="Оквир аутоматски парсира JSON">
    Express-ов `express.json()` посреднички софтвер троши ток тела
    и губите сирове бајтове. Користите `express.raw()` посебно на webhook
    рути или баферујте сирово тело у претходном посредничком софтверу.
    Исто важи за NestJS / Koa — погледајте њихову документацију о „сировом телу“.
  </Accordion>

  <Accordion title="Поређење небезбедно у погледу времена">
    `expected === signature` у JS или `expected == signature` у
    Python-у су поређења променљивог трајања. Користите `crypto.timingSafeEqual`
    односно `hmac.compare_digest`. Разлика у перформансама
    је занемарљива.
  </Accordion>

  <Accordion title="Погрешна тајна за крајње тачке алата">
    Позиви директно ка крајњим тачкама алата потписују се помоћу **webhook
    тајне на нивоу организације** (`GET /v1/webhook`) — не помоћу било које тајне по крајњој тачки
    из `/v1/developer/webhook-endpoints`. Поново употребите исту функцију `verify()`,
    али проверите да ли јој прослеђујете тајну организације на рутама алата.
  </Accordion>

  <Accordion title="Хеширање упитног низа код GET/DELETE алата">
    За методе алата без тела, потпис обухвата празан низ бајтова,
    чиме се задржава један универзални поступак: примените HMAC на сирово тело захтева,
    какво год да је. Хеширање URL-а или упитног низа никада се неће подударати.
  </Accordion>

  <Accordion title="Невраћање 401 при неподударности">
    Враћање 200 при неуспешној верификацији чини обрађивач метом за поновљене захтеве.
    Увек одговорите статусом који није 2xx ако верификација не успе.
  </Accordion>
</AccordionGroup>

---

## Следећи кораци

<CardGroup cols={2}>
  <Card title="Преглед webhook-ова" icon="bolt" href="/sr/webhooks/overview">
    Семантика испоруке, поновни покушаји, изворне IP адресе.
  </Card>
  <Card title="Webhook крајње тачке" icon="plug" href="/sr/webhooks/endpoints">
    Управљајте са више URL адреса, ротирајте тајне.
  </Card>
  <Card title="Функцијски алати" icon="screwdriver-wrench" href="/sr/tools/overview">
    Два пута позивања алата и њихови облици захтева.
  </Card>
  <Card title="Интеграције алата" icon="wrench" href="/sr/guides/build-tool-integration">
    Изградите комплетну интеграцију подржану алатима од почетка до краја.
  </Card>
</CardGroup>
