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

# Weryfikowanie podpisów webhooków

> Każde żądanie webhooka i narzędzia z ThunderPhone jest podpisane. Zweryfikuj raz; używaj wszędzie.

Każde żądanie, które wysyłamy na Twój serwer — dostarczenie webhooka i
wywołanie endpointu narzędzia — zawiera podpis HMAC-SHA256 w nagłówku
`X-ThunderPhone-Signature`. Poprawnie zaimplementuj weryfikację raz, a następnie
użyj tego samego pomocnika w każdym handlerze.

## Algorytm

1. Odczytaj **surowe** ciało żądania — dokładne bajty, które wysłaliśmy.
2. Oblicz `hmac_sha256(secret, body).hexdigest()`.
3. Porównaj w **stałym czasie** z `X-ThunderPhone-Signature`.
   (Naiwne porównanie ciągów ujawnia informacje o czasie.)

Podpisujemy dokładnie te bajty, które przesyłamy, więc weryfikacja surowego ciała
zawsze działa. Te bajty są również **kanoniczną serializacją JSON**
payloadu — klucze posortowane alfabetycznie, zwarte separatory
(`,` i `:` bez spacji), UTF-8. Daje to drugi, w pełni
równoważny sposób, gdy framework udostępnia wyłącznie sparsowany JSON:
ponownie serializuj kanonicznie i oblicz HMAC dla wyniku.

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

Preferuj surowe ciało — to o jeden krok mniej i nie jest podatne na problemy
z ponownym przetwarzaniem liczb JSON w niektórych językach.

## Który sekret?

| Źródło                                                                                              | Sekret                                                                                                                                      |
| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| [Endpoint webhooka](/pl/webhooks/endpoints) (`/v1/developer/webhook-endpoints`)                     | `secret` dla danego endpointu (48 znaków szesnastkowych), zwracany jednorazowo podczas tworzenia                                            |
| [Starszy webhook z pojedynczym adresem URL](/api-reference/organizations#legacy-single-url-webhook) | `secret` dla organizacji zwracany przez `GET /v1/webhook`                                                                                   |
| [Wywołanie endpointu narzędzia](/pl/tools/overview) (bezpośrednie wywołanie Twojego `endpoint.url`) | **Sekret webhooka na poziomie organizacji** (ten sam co dla starszego webhooka z pojedynczym adresem URL) — nie sekret dla danego endpointu |

Przechowuj sekret w menedżerze sekretów lub zmiennej środowiskowej — nigdy nie commituj go.

## Implementacje referencyjne

Wszystkie cztery weryfikują surowe ciało żądania:

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

## Integracja specyficzna dla frameworka

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

## Weryfikowanie wywołań narzędzi

Gdy agent wywołuje bezpośrednio jedno z Twoich
[narzędzi funkcyjnych](/pl/tools/overview) (narzędzie ma
`endpoint`), żądanie zawiera dwa nagłówki ThunderPhone wraz z
skonfigurowanymi przez Ciebie nagłówkami `endpoint.headers`:

* `X-ThunderPhone-Call-ID` — numeryczny identyfikator trwającego połączenia.
* `X-ThunderPhone-Signature` — HMAC-SHA256 z kluczem w postaci
  **sekretu webhooka na poziomie organizacji**, obliczony na podstawie dokładnych bajtów treści żądania.

Ten sam pomocnik `verify()` działa bez zmian, z dwoma niuansami:

1. **Narzędzia `GET` / `DELETE` nie mają treści.** Argumenty są przekazywane jako parametry zapytania, a podpis jest obliczany na podstawie **pustego ciągu bajtów** — więc użyj `verify(b"", sig, secret)` (Python) lub `verify(Buffer.alloc(0), sig, secret)` (Node). **Nie** haszuj ciągu zapytania.
2. **Organizacje bez skonfigurowanego starszego webhooka nie mają sekretu organizacji.** W takim przypadku wywołania narzędzi zawierają tylko `X-ThunderPhone-Call-ID` i nie zawierają nagłówka podpisu. Skonfiguruj starszy webhook (`PUT /v1/webhook`), aby uzyskać sekret podpisywania, lub uwierzytelniaj wywołania narzędzi własnym nagłówkiem za pomocą `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)
    ...
```

Wysyłanie narzędzi w trybie webhooka (narzędzia bez `endpoint`, dostarczane
do webhooka organizacji jako `telephony.tool` / `web.tool`) jest zwykłym
podpisanym webhookiem — obowiązuje standardowa procedura opisana powyżej. Zobacz
[Narzędzia funkcyjne](/pl/tools/overview), aby poznać oba formaty żądań.

## Typowe pułapki

<AccordionGroup>
  <Accordion title="Ponowna serializacja z domyślnym formatowaniem">
    Przetworzenie treści i ponowne zapisanie jej za pomocą biblioteki JSON z
    ustawieniami domyślnymi (spacje po `,` / `:`, klucze w kolejności wstawiania) powoduje
    powstanie innych bajtów i unieważnia HMAC. Zweryfikuj surową treść — lub jeśli
    musisz ją ponownie serializować, dokładnie dopasuj naszą formę kanoniczną: posortowane
    klucze, zwarte separatory, UTF-8.
  </Accordion>

  <Accordion title="Framework automatycznie przetwarza JSON">
    Middleware `express.json()` w Express zużywa strumień treści
    i tracisz surowe bajty. Użyj `express.raw()` konkretnie dla trasy webhooka
    albo buforuj surową treść w pre-middleware.
    Tak samo jest w NestJS / Koa — sprawdź ich dokumentację dotyczącą „raw body”.
  </Accordion>

  <Accordion title="Porównanie nieodporne na ataki czasowe">
    `expected === signature` w JS lub `expected == signature` w
    Pythonie to porównania o zmiennym czasie wykonania. Użyj odpowiednio `crypto.timingSafeEqual`
    lub `hmac.compare_digest`. Różnica w wydajności
    jest pomijalna.
  </Accordion>

  <Accordion title="Nieprawidłowy sekret dla endpointów narzędzi">
    Bezpośrednie wywołania endpointów narzędzi są podpisywane za pomocą **sekretu
    webhooka na poziomie organizacji** (`GET /v1/webhook`) — a nie za pomocą sekretu przypisanego do endpointu
    z `/v1/developer/webhook-endpoints`. Użyj ponownie tej samej funkcji `verify()`,
    ale upewnij się, że dla tras narzędzi przekazujesz do niej sekret organizacji.
  </Accordion>

  <Accordion title="Haszowanie ciągu zapytania w narzędziach GET/DELETE">
    W przypadku metod narzędzi bez treści podpis obejmuje pusty ciąg
    bajtów, co pozwala zachować jedną uniwersalną metodę: wykonaj HMAC na surowej treści żądania,
    niezależnie od tego, jaka ona jest. Haszowanie adresu URL lub ciągu zapytania nigdy nie będzie zgodne.
  </Accordion>

  <Accordion title="Brak zwracania 401 przy niezgodności">
    Zwracanie 200 po nieudanej weryfikacji sprawia, że handler staje się celem
    ataków typu replay. Zawsze zwracaj kod inny niż 2xx, jeśli weryfikacja się nie powiedzie.
  </Accordion>
</AccordionGroup>

***

## Kolejne kroki

<CardGroup cols={2}>
  <Card title="Przegląd webhooków" icon="bolt" href="/pl/webhooks/overview">
    Semantyka dostarczania, ponowienia, źródłowe adresy IP.
  </Card>

  <Card title="Endpointy webhooków" icon="plug" href="/pl/webhooks/endpoints">
    Zarządzaj wieloma adresami URL, rotuj sekrety.
  </Card>

  <Card title="Narzędzia funkcji" icon="screwdriver-wrench" href="/pl/tools/overview">
    Dwie ścieżki wywoływania narzędzi i formaty ich żądań.
  </Card>

  <Card title="Integracje narzędzi" icon="wrench" href="/pl/guides/build-tool-integration">
    Utwórz kompletną integrację opartą na narzędziach od początku do końca.
  </Card>
</CardGroup>
