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

# Verificați semnăturile webhook

> Fiecare solicitare webhook și de instrument de la ThunderPhone este semnată. Verificați o dată; reutilizați peste tot.

Fiecare solicitare pe care o trimitem către serverul dumneavoastră — livrările de webhook și invocările endpointurilor de instrumente — include o semnătură HMAC-SHA256 în antetul
`X-ThunderPhone-Signature`. Configurați verificarea corect o singură dată și utilizați același ajutor în fiecare handler.

## Algoritmul

1. Citiți corpul **brut** al solicitării — octeții exacți pe care vi i-am trimis prin POST.
2. Calculați `hmac_sha256(secret, body).hexdigest()`.
3. Comparați în **timp constant** cu `X-ThunderPhone-Signature`.
   (Compararea naivă a șirurilor dezvăluie informații de temporizare.)

Semnăm exact octeții pe care îi transmitem, astfel încât verificarea corpului brut
funcționează întotdeauna. Acei octeți reprezintă și **serializarea JSON canonică**
a payloadului — chei sortate alfabetic, separatori compacți
(`,` și `:` fără spații), UTF-8. Astfel aveți o a doua metodă, complet
echivalentă, atunci când frameworkul dumneavoastră expune doar JSON analizat:
reserializați canonic și calculați HMAC pentru acesta.

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

Preferați corpul brut — este cu un pas mai puțin și este imun la
particularitățile conversiei dus-întors a numerelor JSON în unele limbaje.

## Care secret?

| Sursă                                                                                                   | Secret                                                                                                                           |
| ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| [Endpoint webhook](/ro/webhooks/endpoints) (`/v1/developer/webhook-endpoints`)                          | `secret` per endpoint (48 de caractere hexazecimale), returnat o singură dată la creare                                          |
| [Webhook moștenit cu un singur URL](/api-reference/organizations#legacy-single-url-webhook)             | `secret` per organizație, returnat la `GET /v1/webhook`                                                                          |
| [Invocare endpoint de instrumente](/ro/tools/overview) (apel direct la `endpoint.url` al dumneavoastră) | **Secretul webhook la nivel de organizație** (același ca pentru webhookul moștenit cu un singur URL) — nu un secret per endpoint |

Stocați secretul în managerul dumneavoastră de secrete sau într-o variabilă de mediu — nu îl comiteți niciodată.

## Implementări de referință

Toate cele patru verifică corpul brut al solicitării:

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

## Configurare specifică frameworkului

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

## Verificarea apelurilor de instrumente

Când agentul invocă direct unul dintre
[instrumentele de funcții](/ro/tools/overview) (instrumentul are un
`endpoint`), solicitarea include două antete ThunderPhone pe lângă
`endpoint.headers` configurat:

* `X-ThunderPhone-Call-ID` — ID-ul numeric al apelului în desfășurare.
* `X-ThunderPhone-Signature` — HMAC-SHA256, cu cheia reprezentată de
  **secretul webhook la nivel de organizație**, calculat peste octeții exacți
  ai corpului solicitării.

Același ajutor `verify()` funcționează fără modificări, cu două particularități:

1. **Instrumentele `GET` / `DELETE` nu au corp.** Argumentele sunt transmise ca
   parametri de interogare, iar semnătura este calculată peste **șirul de octeți
   gol** — deci `verify(b"", sig, secret)` (Python) sau
   `verify(Buffer.alloc(0), sig, secret)` (Node). Nu calculați hash-ul
   șirului de interogare.
2. **Organizațiile fără un webhook moștenit configurat nu au un secret de organizație.**
   În acest caz, apelurile de instrumente includ doar `X-ThunderPhone-Call-ID`
   și niciun antet de semnătură. Configurați webhookul moștenit
   (`PUT /v1/webhook`) pentru a obține un secret de semnare sau autentificați
   apelurile de instrumente cu propriul antet prin `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)
    ...
```

Dispecerizarea instrumentelor în modul webhook (instrumente fără un `endpoint`,
livrate către webhookul organizației dumneavoastră ca `telephony.tool` / `web.tool`)
este un webhook semnat obișnuit — se aplică procedura standard de mai sus. Consultați
[Instrumente de funcții](/ro/tools/overview) pentru ambele forme de solicitare.

## Capcane frecvente

<AccordionGroup>
  <Accordion title="Reserializarea cu formatarea implicită">
    Analizarea corpului și serializarea lui din nou cu setările implicite
    ale bibliotecii JSON (spații după `,` / `:`, chei în ordinea inserării) produce
    octeți diferiți și invalidează HMAC-ul. Verificați corpul brut — sau, dacă
    trebuie să îl reserializați, respectați exact forma noastră canonică: chei
    sortate, separatori compacți, UTF-8.
  </Accordion>

  <Accordion title="Frameworkul analizează automat JSON-ul">
    Middleware-ul `express.json()` din Express consumă fluxul corpului
    și pierdeți octeții bruti. Utilizați `express.raw()` specific pe ruta
    webhookului sau stocați corpul brut într-un pre-middleware.
    La fel și pentru NestJS / Koa — consultați documentația lor despre „raw body”.
  </Accordion>

  <Accordion title="Comparație nesigură din perspectiva timpului">
    `expected === signature` în JS sau `expected == signature` în
    Python sunt comparații cu durată variabilă. Utilizați `crypto.timingSafeEqual`
    sau, respectiv, `hmac.compare_digest`. Diferența de performanță
    este nulă.
  </Accordion>

  <Accordion title="Secret greșit pentru endpointurile de instrumente">
    Apelurile directe către endpointurile de instrumente sunt semnate cu **secretul
    webhook la nivel de organizație** (`GET /v1/webhook`) — nu cu un secret per-endpoint
    din `/v1/developer/webhook-endpoints`. Reutilizați aceeași funcție `verify()`,
    dar asigurați-vă că îi transmiteți secretul organizației pe rutele instrumentelor.
  </Accordion>

  <Accordion title="Hash-uirea șirului de interogare pentru instrumentele GET/DELETE">
    Pentru metodele de instrumente fără corp, semnătura acoperă șirul gol de
    octeți, păstrând o singură rețetă universală: calculați HMAC-ul pentru corpul brut
    al solicitării, indiferent care este acesta. Hash-uirea URL-ului sau a șirului de interogare nu se va potrivi niciodată.
  </Accordion>

  <Accordion title="Nereturnarea codului 401 la nepotrivire">
    Returnarea codului 200 când verificarea eșuează transformă handlerul într-o
    țintă pentru reluarea solicitărilor. Răspundeți întotdeauna cu un cod diferit de 2xx dacă verificarea eșuează.
  </Accordion>
</AccordionGroup>

***

## Pașii următori

<CardGroup cols={2}>
  <Card title="Prezentare generală a webhookurilor" icon="bolt" href="/ro/webhooks/overview">
    Semantica livrării, reîncercări, IP-uri sursă.
  </Card>

  <Card title="Endpointuri webhook" icon="plug" href="/ro/webhooks/endpoints">
    Gestionați mai multe URL-uri, rotiți secretele.
  </Card>

  <Card title="Instrumente pentru funcții" icon="screwdriver-wrench" href="/ro/tools/overview">
    Cele două căi de invocare a instrumentelor și formatele solicitărilor lor.
  </Card>

  <Card title="Integrări de instrumente" icon="wrench" href="/ro/guides/build-tool-integration">
    Creați o integrare completă bazată pe instrumente, de la un capăt la altul.
  </Card>
</CardGroup>
