---
title: "Verificați semnăturile webhook"
description: "Fiecare cerere webhook și de instrument trimisă de ThunderPhone este semnată cu un marcaj temporal. Verificați-o cu rețeta sau cu ajutorul SDK de aici și respingeți cererile retransmise mai vechi de cinci minute."
---

Fiecare cerere pe care o trimitem către serverul dumneavoastră (livrări webhook, webhook-ul vechi cu un singur URL și apeluri către endpoint-uri de instrumente) include două
antete de semnătură. Ambele folosesc HMAC-SHA256 cu același secret.

| Antet | Semnează | Protecție împotriva reluării |
|--------|-------|-------------------|
| `X-ThunderPhone-Signature-V2` (recomandat) | Marcaj temporal, metodă, cale, interogare și corp | Respingeți cererile mai vechi de 5 minute |
| `X-ThunderPhone-Signature` | Doar corpul | Niciuna: o cerere capturată se verifică pentru totdeauna |

Verificați `X-ThunderPhone-Signature-V2` în integrările noi și migrați
integrările existente către acesta. `X-ThunderPhone-Signature` este încă trimis la fiecare
cerere, neschimbat, astfel încât verificatorii bazați pe acesta continuă să funcționeze. Vom
anunța o dată înainte de a înceta să îl mai trimitem.

## Semnătura V2

```http
X-ThunderPhone-Signature-V2: t=1767225600,v1=03a89bec521f78d55727237ca4713bb0b8dcdf2e09e450865b5b5637b80c1c4e
```

- `t` este timpul Unix, în secunde, la care am trimis cererea. O livrare
  reîncercată este semnată din nou când este trimisă, cu propriul `t`.
- `v1` este HMAC-SHA256 hexazecimal al șirului semnat de mai jos, calculat cu
  secretul dvs.

Valoarea este o listă de perechi `key=value` separate prin virgule. Ignorați cheile pe care
nu le recunoașteți și acceptați cererea dacă se potrivește orice `v1`: este posibil să adăugăm
o a doua semnătură într-o modificare viitoare.

### Șirul semnat

Patru câmpuri, fiecare urmat de un caracter de linie nouă (`\n`, octetul `0x0A`), cu excepția
ultimului:

```text
<t>\n<METHOD>\n<path and query>\n<body>
```

| Câmp | Valoare |
|-------|-------|
| `t` | Valoarea `t` din antet |
| `METHOD` | Metoda HTTP cu majuscule: `POST`, `GET`, `PUT`, `PATCH` sau `DELETE` |
| path and query | Ținta cererii din linia cererii HTTP: calea, apoi `?` și șirul de interogare, dacă există. Fără schemă, gazdă sau fragment. Folosiți-o exact cum a fost primită, fără a o decoda sau a reordona parametrii |
| `body` | Corpul brut al cererii. Apelurile de instrumente `GET` și `DELETE` nu au corp, astfel încât șirul lor semnat se termină cu caracterul de linie nouă de după cale |

Pentru verificare:

1. Înainte de orice analiză, citiți antetul, metoda, calea brută și
   interogarea, precum și corpul brut.
2. Preluați `t` și fiecare `v1` din antet.
3. Respingeți cererea dacă `t` este cu mai mult de **300 de secunde** înainte sau după
   ceasul dvs.
4. Calculați HMAC-SHA256 hexazecimal al șirului semnat cu secretul dvs.
5. Comparați-l cu `v1` în timp constant. Răspundeți cu `401` dacă nu se potrivește nimic.

### Exemplu complet

Secretul este `whsec_example_4c1f2a9b`. Trimitem această cerere la
`t = 1767225600` (2026-01-01 00:00:00 UTC):

```http
POST /tools/lookup?region=us HTTP/1.1
Host: api.example.com
Content-Type: application/json
X-ThunderPhone-Signature-V2: t=1767225600,v1=03a89bec521f78d55727237ca4713bb0b8dcdf2e09e450865b5b5637b80c1c4e

{"customer_id":"cust_123"}
```

Șirul semnat este format din aceste patru linii, fără caracter de linie nouă după corp:

```text
1767225600
POST
/tools/lookup?region=us
{"customer_id":"cust_123"}
```

HMAC-SHA256 al acestuia este
`03a89bec521f78d55727237ca4713bb0b8dcdf2e09e450865b5b5637b80c1c4e`.
Verificați implementarea dvs. comparând-o cu acesta sau reproduceți-l cu `openssl`:

```bash
printf '1767225600\nPOST\n/tools/lookup?region=us\n{"customer_id":"cust_123"}' \
  | openssl dgst -sha256 -hmac 'whsec_example_4c1f2a9b'
```

Un apel de instrument `GET` către `/tools/lookup?customer_id=cust_123` în aceeași
secundă are corpul gol. Șirul său semnat este
`1767225600\nGET\n/tools/lookup?customer_id=cust_123\n`, iar semnătura sa
este `3ce214cb0b8f2bf39b1d19d4e7265df9dbaff5a5f838d3cc6c753a8f7a4ad280`.

### Cu SDK-urile

[SDK-urile Python și TypeScript](/api-reference/sdks) includ un ajutor
(`thunderphone` 0.2.0 sau o versiune ulterioară, `@thunderphone/sdk` 0.2.0 sau o versiune ulterioară). Acesta
verifică fereastra de 5 minute, compară în timp constant și acceptă o listă
de secrete, astfel încât să le puteți roti fără întreruperi.

<CodeGroup>
```python Python
from thunderphone import verify_signature

ok = verify_signature(
    header,            # the X-ThunderPhone-Signature-V2 value
    secret=SECRET,     # or [NEW_SECRET, OLD_SECRET] while rotating
    method=method,     # "POST"
    path=path,         # "/tools/lookup?region=us", exactly as received
    body=raw_body,     # bytes, before any JSON parsing
    tolerance=300,     # optional; seconds
)
```

```javascript Node.js
import { verifySignature } from "@thunderphone/sdk";

const ok = verifySignature({
  header,               // the X-ThunderPhone-Signature-V2 value
  secret: SECRET,       // or [NEW_SECRET, OLD_SECRET] while rotating
  method,               // "POST"
  path,                 // "/tools/lookup?region=us", exactly as received
  body: rawBody,        // Buffer or string, before any JSON parsing
  toleranceSeconds: 300, // optional
});
```
</CodeGroup>

### Fără un SDK

<CodeGroup>
```python Python
import hashlib
import hmac
import time

TOLERANCE_SECONDS = 300


def verify_v2(header: str, secret: str, method: str, path: str, body: bytes) -> bool:
    """path is the path plus ?query exactly as received; body is the raw body."""
    timestamp, signatures = None, []
    for item in (header or "").split(","):
        key, _, value = item.strip().partition("=")
        if key == "t" and value.isdigit() and len(value) <= 15:
            timestamp = int(value)
        elif key == "v1":
            signatures.append(value)
    if timestamp is None or abs(time.time() - timestamp) > TOLERANCE_SECONDS:
        return False
    signed = f"{timestamp}\n{method.upper()}\n{path}\n".encode() + body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected.encode(), s.encode()) for s in signatures)
```

```javascript Node.js
import crypto from "node:crypto";

const TOLERANCE_SECONDS = 300;

// path is the path plus ?query exactly as received; body is the raw Buffer.
export function verifyV2(header, secret, method, path, body) {
  let timestamp;
  const signatures = [];
  for (const item of (header || "").split(",")) {
    const [key, value = ""] = item.trim().split("=", 2);
    if (key === "t" && /^\d+$/.test(value)) timestamp = Number(value);
    else if (key === "v1") signatures.push(value);
  }
  if (timestamp === undefined) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;
  const expected = Buffer.from(
    crypto
      .createHmac("sha256", secret)
      .update(`${timestamp}\n${method.toUpperCase()}\n${path}\n`)
      .update(body)
      .digest("hex"),
  );
  return signatures.some((sig) => {
    const candidate = Buffer.from(sig);
    return (
      candidate.length === expected.length &&
      crypto.timingSafeEqual(candidate, expected)
    );
  });
}
```
</CodeGroup>

### Conectarea frameworkului

Fiecare exemplu citește calea și interogarea brute, precum și corpul brut, apoi apelează
funcția de ajutor din SDK.

<CodeGroup>
```python FastAPI
import json

from fastapi import FastAPI, HTTPException, Request
from thunderphone import verify_signature

app = FastAPI()


def raw_path_and_query(request: Request) -> str:
    path = (request.scope.get("raw_path") or request.url.path.encode()).decode("latin-1")
    query = request.scope.get("query_string", b"").decode("latin-1")
    return f"{path}?{query}" if query else path


@app.post("/thunderphone-webhook")
async def hook(request: Request):
    body = await request.body()  # raw bytes, NOT request.json()
    if not verify_signature(
        request.headers.get("X-ThunderPhone-Signature-V2"),
        secret=SECRET,
        method=request.method,
        path=raw_path_and_query(request),
        body=body,
    ):
        raise HTTPException(status_code=401)
    event = json.loads(body)
    # … dispatch on event["type"] …
    return {"ok": True}
```

```javascript Express
import express from "express";
import { verifySignature } from "@thunderphone/sdk";

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 ok = verifySignature({
      header: req.header("X-ThunderPhone-Signature-V2"),
      secret: process.env.WEBHOOK_SECRET,
      method: req.method,
      path: req.originalUrl, // path and query as received
      body: req.body,
    });
    if (!ok) 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 HttpResponseForbidden, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from thunderphone import verify_signature


@csrf_exempt
@require_POST
def hook(request):
    if not verify_signature(
        request.headers.get("X-ThunderPhone-Signature-V2"),
        secret=SECRET,
        method=request.method,
        path=request.get_full_path(),
        body=request.body,  # raw bytes
    ):
        return HttpResponseForbidden("invalid signature")
    event = json.loads(request.body)
    # … dispatch on event["type"] …
    return JsonResponse({"ok": True})
```
</CodeGroup>

`req.originalUrl` (Express), precum și `raw_path` și `query_string` din ASGI
(FastAPI) reprezintă ținta exact așa cum a fost primită. `get_full_path()` din Django și
`request.path` din Flask decodifică mai întâi calea, ceea ce
produce același șir pentru căile obișnuite. Dacă calea endpointului dumneavoastră conține
caractere codificate procentual sau rezervate, precum `%2F`, `;` sau `=`, citiți în schimb
ținta brută furnizată de serverul dumneavoastră (de exemplu `request.META["RAW_URI"]` pe
Gunicorn).

## Ce secret?

Același secret semnează ambele anteturi.

| 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` |
| [Apelare endpoint de instrumente](/ro/tools/overview) (apel direct către `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ă.

## Verificarea apelurilor de instrumente

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

- `X-ThunderPhone-Call-ID` — ID-ul numeric al apelului activ.
- `X-ThunderPhone-Signature-V2` — semnătura cu marcaj temporal de mai sus, asociată
  cu **secretul webhookului la nivel de organizație**.
- `X-ThunderPhone-Signature` — semnătura veche doar pentru corp, cu aceeași cheie.

Două aspecte diferă față de webhookuri:

1. **Instrumentele `GET` / `DELETE` nu au corp.** Argumentele sunt transmise ca
   parametri de interogare. Semnătura V2 le acoperă prin cale și interogare,
   iar câmpul corpului este gol. Verificați folosind șirul de interogare brut: analizarea
   parametrilor și construirea unui nou șir de interogare le poate modifica ordinea
   sau codificarea. Antetul vechi semnează șirul de octeți gol.
2. **Organizațiile fără un webhook vechi configurat nu au un secret de organizație.** În
   acest caz, apelurile instrumentelor includ doar `X-ThunderPhone-Call-ID` și niciun
   antet de semnătură. Configurați webhookul vechi
   (`PUT /v1/webhook`) pentru a obține un secret de semnare sau autentificați apelurile
   instrumentelor cu propriul antet prin `endpoint.headers`.

```python
@app.api_route("/tools/search-appointments", methods=["GET", "POST"])
async def tool(request: Request):
    body = await request.body()  # b"" for GET/DELETE tools
    if not verify_signature(
        request.headers.get("X-ThunderPhone-Signature-V2"),
        secret=ORG_WEBHOOK_SECRET,
        method=request.method,
        path=raw_path_and_query(request),
        body=body,
    ):
        raise HTTPException(status_code=401)
    call_id = request.headers.get("X-ThunderPhone-Call-ID", "")
    args = json.loads(body) if body else dict(request.query_params)
    ...
```

În Express, un parser de corp lasă `req.body` ca obiect gol pentru un `GET`,
așadar transmiteți `Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0)`.

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

## Semnătură moștenită: `X-ThunderPhone-Signature`

Antetul original este HMAC-SHA256 în format hex al corpului brut, folosind
același secret. Nu conține niciun marcaj temporal, astfel încât oricine capturează o
cerere semnată o poate retransmite. Este trimis în continuare la fiecare cerere cât timp
integrările migrează la V2.

1. Citiți corpul de cerere **brut** — octeții exacți pe care vi i-am trimis.
2. Calculați `hmac_sha256(secret, body).hexdigest()`.
3. Comparați în **timp constant** cu `X-ThunderPhone-Signature`.

Semnăm exact octeții pe care îi transmitem. Acești octeți reprezintă și
**serializarea JSON canonică** a încărcăturii utile — chei sortate
alfabetic, separatori compacți (`,` și `:` fără spații), UTF-8.
Dacă frameworkul dumneavoastră expune doar JSON-ul analizat, reserializați canonic și
calculați HMAC pentru acesta:

```python
# 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 de conversie
dus-întors ale numerelor JSON în unele limbaje. Același lucru este valabil pentru câmpul
de corp din V2.

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

## Capcane frecvente

<AccordionGroup>
  <Accordion title="Decalajul ceasului">
    Fereastra de 5 minute este măsurată în raport cu ceasul serverului dumneavoastră. Mențineți-l
    sincronizat cu NTP; un ceas decalibrat cu mai mult de 5 minute respinge fiecare
    cerere.
  </Accordion>

  <Accordion title="Un proxy rescrie calea">
    Calea semnată este cea pe care am solicitat-o: calea URL-ului pe care l-ați
    configurat în ThunderPhone. Dacă un proxy invers sau un gateway API elimină
    sau adaugă un prefix înainte ca aplicația dumneavoastră să vadă cererea, reconstruiți
    calea pe care am trimis-o (de exemplu, adăugați prefixul înapoi) înainte de verificare.
  </Accordion>

  <Accordion title="Reserializarea cu formatarea implicită">
    Analizarea corpului și serializarea lui din nou folosind 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, reproduceț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 bruți. Utilizați `express.raw()` în mod specific pe ruta
    webhookului sau stocați în buffer corpul brut într-un pre-middleware.
    La fel și pentru NestJS / Koa — consultați documentația lor despre „corpul brut”.
  </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 `hmac.compare_digest`, respectiv. Diferența de performanță
    este nulă.
  </Accordion>

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

  <Accordion title="Șiruri de interogare pentru instrumentele GET/DELETE">
    V2 semnează șirul de interogare exact așa cum a fost trimis. Antetul vechi semnează
    corpul gol și niciodată șirul de interogare, astfel încât hash-ul URL-ului pentru
    antetul vechi nu va corespunde niciodată.
  </Accordion>

  <Accordion title="Duplicate în interiorul ferestrei">
    Marca temporală oprește retransmiterile mai vechi de 5 minute, nu o a doua copie
    din interiorul acelei ferestre. Livrările către endpointuri sunt efectuate cel puțin o dată, astfel încât
    poate ajunge și o reîncercare a aceluiași eveniment; eliminați duplicatele folosind
    `event_id` din plic.
  </Accordion>

  <Accordion title="Nereturnarea codului 401 la nepotrivire">
    Returnarea codului 200 când verificarea eșuează transformă handlerul într-o
    țintă pentru reluări. Răspundeți întotdeauna cu un cod care nu este 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 de funcții" icon="screwdriver-wrench" href="/ro/tools/overview">
    Cele două căi de invocare a instrumentelor și formatele cererilor acestora.
  </Card>
  <Card title="Integrări de instrumente" icon="wrench" href="/ro/guides/build-tool-integration">
    Construiți cap-coadă o integrare completă bazată pe instrumente.
  </Card>
</CardGroup>
