---
title: "Pārbaudiet tīmekļa aizķeru parakstus"
description: "Katrs ThunderPhone nosūtītais tīmekļa aizķeres un rīka pieprasījums tiek parakstīts ar laikspiedolu. Pārbaudiet to, izmantojot šeit sniegto recepti vai SDK palīgfunkciju, un noraidiet atkārtoti nosūtītus pieprasījumus, kas ir vecāki par piecām minūtēm."
---

Katram pieprasījumam, ko nosūtām uz jūsu serveri (tīmekļa aizķeru piegādēm, mantotā
viena URL tīmekļa aizķera un rīku galapunktu izsaukumiem), ir divas paraksta
galvenes. Abas izmanto HMAC-SHA256 ar vienu un to pašu noslēpumu.

| Galvene | Paraksta | Aizsardzība pret atkārtojumu |
|--------|-------|-------------------|
| `X-ThunderPhone-Signature-V2` (ieteicams) | Laikspiedolu, metodi, ceļu, vaicājumu un pamattekstu | Noraidiet pieprasījumus, kas ir vecāki par 5 minūtēm |
| `X-ThunderPhone-Signature` | Tikai pamattekstu | Nav: pārtverts pieprasījums tiek verificēts bez termiņa |

Jaunās integrācijās verificējiet `X-ThunderPhone-Signature-V2` un pārvietojiet
uz to esošās integrācijas. `X-ThunderPhone-Signature` joprojām tiek nosūtīts ar
katru pieprasījumu bez izmaiņām, tāpēc uz tā balstītie verificētāji turpina
darboties. Pirms pārtrauksim to sūtīt, paziņosim datumu.

## Paraksts V2

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

- `t` ir Unix laiks sekundēs, kad nosūtījām pieprasījumu. Atkārtoti
  nosūtīta piegāde tiek parakstīta vēlreiz tās nosūtīšanas brīdī ar savu `t`.
- `v1` ir tālāk norādītās parakstītās virknes heksadecimālais HMAC-SHA256,
  izmantojot jūsu slepeno atslēgu.

Vērtība ir ar komatiem atdalīts `key=value` pāru saraksts. Ignorējiet atslēgas,
kuras neatpazīstat, un pieņemiet pieprasījumu, ja atbilst jebkurš `v1`: turpmāku
izmaiņu laikā varam pievienot otru parakstu.

### Parakstītā virkne

Četri lauki, katram seko viena jauna rinda (`\n`, baits `0x0A`), izņemot
pēdējo:

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

| Lauks | Vērtība |
|-------|-------|
| `t` | `t` vērtība no galvenes |
| `METHOD` | HTTP metode ar lielajiem burtiem: `POST`, `GET`, `PUT`, `PATCH` vai `DELETE` |
| path and query | Pieprasījuma mērķis no HTTP pieprasījuma rindas: ceļš, pēc tam `?` un vaicājuma virkne, ja tāda ir. Bez shēmas, resursdatora vai fragmenta. Izmantojiet to tieši tā, kā tas tika saņemts, to neatkodējot un nemainot parametru secību |
| `body` | Neapstrādāts pieprasījuma pamatteksts. `GET` un `DELETE` rīka izsaukumiem nav pamatteksta, tādēļ to parakstītā virkne beidzas ar jauno rindu pēc ceļa |

Lai verificētu:

1. Pirms jebkādas parsēšanas nolasiet galveni, metodi, neapstrādāto ceļu un
   vaicājumu, kā arī neapstrādāto pamattekstu.
2. No galvenes iegūstiet `t` un katru `v1`.
3. Noraidiet pieprasījumu, ja `t` ir vairāk nekā **300 sekundes** pirms vai pēc
   jūsu pulksteņa laika.
4. Aprēķiniet parakstītās virknes heksadecimālo HMAC-SHA256, izmantojot savu slepeno atslēgu.
5. Salīdziniet to ar `v1` konstantā laikā. Atbildiet ar `401`, ja nekas neatbilst.

### Piemērs

Slepenā atslēga `whsec_example_4c1f2a9b`. Mēs nosūtām šo pieprasījumu
laikā `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"}
```

Parakstītā virkne ir šīs četras rindas bez jaunas rindas pēc pamatteksta:

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

Tās HMAC-SHA256 ir
`03a89bec521f78d55727237ca4713bb0b8dcdf2e09e450865b5b5637b80c1c4e`.
Pārbaudiet savu implementāciju ar to vai atveidojiet to, izmantojot `openssl`:

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

`GET` rīka izsaukumam uz `/tools/lookup?customer_id=cust_123` tajā pašā
sekundē ir tukšs pamatteksts. Tā parakstītā virkne ir
`1767225600\nGET\n/tools/lookup?customer_id=cust_123\n`, un tās paraksts
ir `3ce214cb0b8f2bf39b1d19d4e7265df9dbaff5a5f838d3cc6c753a8f7a4ad280`.

### Ar SDK

[Python un TypeScript SDK](/api-reference/sdks) ietver palīgfunkciju
(`thunderphone` 0.2.0 vai jaunāka versija, `@thunderphone/sdk` 0.2.0 vai
jaunāka versija). Tā pārbauda 5 minūšu logu, salīdzina konstantā laikā un
pieņem slepeno atslēgu sarakstu, lai jūs varētu tās nomainīt bez dīkstāves.

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

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

### Sistēmas pieslēgšana

Katrs piemērs nolasa neapstrādāto ceļu un vaicājumu, kā arī neapstrādāto pamattekstu, pēc tam izsauc
SDK palīgfunkciju.

<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) un ASGI `raw_path` un `query_string`
(FastAPI) ir mērķis tieši tāds, kā saņemts. Django
`get_full_path()` un Flask `request.path` vispirms atkodē ceļu, kas
parastiem ceļiem dod to pašu virkni. Ja jūsu galapunkta ceļā ir procentu kodētas vai rezervētas rakstzīmes, piemēram, `%2F`, `;` vai `=`, tā vietā nolasiet
servera nodrošināto neapstrādāto mērķi (piemēram, `request.META["RAW_URI"]` sistēmā
Gunicorn).

## Kurš noslēpums?

Tas pats noslēpums paraksta abas galvenes.

| Avots | Noslēpums |
|--------|--------|
| [Tīmekļa aizķeres galapunkts](/lv/webhooks/endpoints) (`/v1/developer/webhook-endpoints`) | Katram galapunktam paredzēts `secret` (48 heksadecimālās rakstzīmes), kas tiek atgriezts vienreiz izveides laikā |
| [Mantotā tīmekļa aizķere ar vienu URL](/api-reference/organizations#legacy-single-url-webhook) | Organizācijai paredzēts `secret`, kas tiek atgriezts ar `GET /v1/webhook` |
| [Rīka galapunkta izsaukums](/lv/tools/overview) (tiešs izsaukums uz jūsu `endpoint.url`) | **Organizācijas līmeņa tīmekļa aizķeres noslēpums** (tas pats, kas mantotajai tīmekļa aizķerei ar vienu URL) — nevis katram galapunktam paredzēts noslēpums |

Glabājiet noslēpumu savu noslēpumu pārvaldniekā vai vides mainīgajā — nekad to neiekļaujiet repozitorijā.

## Rīku izsaukumu verificēšana

Kad balss aģents tieši izsauc kādu no jūsu
[funkciju rīkiem](/lv/tools/overview) (rīkam ir
`endpoint`), pieprasījumā kopā ar jūsu konfigurētajiem `endpoint.headers`
tiek iekļautas šādas ThunderPhone galvenes:

- `X-ThunderPhone-Call-ID` — aktīvā zvana skaitliskais ID.
- `X-ThunderPhone-Signature-V2` — iepriekš norādītais paraksts ar laika zīmogu,
  kas veidots, izmantojot jūsu **organizācijas līmeņa webhook noslēpumu**.
- `X-ThunderPhone-Signature` — mantotais paraksts, kas attiecas tikai uz pamattekstu, ar to pašu atslēgu.

No webhook atšķiras divas lietas:

1. **`GET` / `DELETE` rīkiem nav pamatteksta.** Argumenti tiek nodoti kā vaicājuma
   parametri. V2 paraksts tos aptver, izmantojot ceļu un vaicājumu,
   bet pamatteksta lauks ir tukšs. Verificējiet, izmantojot neapstrādāto vaicājuma virkni:
   parametru parsēšana un jaunas vaicājuma virknes izveide var mainīt to secību
   vai kodējumu. Mantotā galvene paraksta tukšu baitu virkni.
2. **Organizācijām bez konfigurēta mantotā webhook nav organizācijas noslēpuma.** Šādā
   gadījumā rīku izsaukumos ir tikai `X-ThunderPhone-Call-ID` un nav
   paraksta galveņu. Konfigurējiet mantoto webhook
   (`PUT /v1/webhook`), lai iegūtu parakstīšanas noslēpumu, vai autentificējiet rīku
   izsaukumus ar savu galveni, izmantojot `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)
    ...
```

Programmā Express pamatteksta parsētājs `GET` pieprasījumā atstāj `req.body` kā tukšu objektu,
tāpēc nododiet `Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0)`.

Tīmekļa aizķeres **režīma** rīku nosūtīšana (rīki bez `endpoint`, kas tiek
piegādāti uz jūsu organizācijas tīmekļa aizķeri kā `telephony.tool` / `web.tool`)
ir parasta parakstīta tīmekļa aizķere — piemērojama iepriekš norādītā standarta
procedūra. Abus pieprasījumu formātus skatiet sadaļā
[Funkciju rīki](/lv/tools/overview).

## Mantotais paraksts: `X-ThunderPhone-Signature`

Sākotnējā galvene ir tikai neapstrādātā pamatteksta heksadecimālais HMAC-SHA256, izmantojot to pašu noslēpumu. Tā nesatur laikspiedolu, tāpēc ikviens, kurš pārtver vienu parakstītu pieprasījumu, var to atkārtoti nosūtīt. Tā joprojām tiek sūtīta ar katru pieprasījumu, kamēr integrācijas pāriet uz V2.

1. Nolasiet **neapstrādāto** pieprasījuma pamattekstu — precīzos baitus, ko jums nosūtījām.
2. Aprēķiniet `hmac_sha256(secret, body).hexdigest()`.
3. Salīdziniet **konstantā laikā** ar `X-ThunderPhone-Signature`.

Mēs parakstām tieši tos baitus, kurus pārsūtām. Šie baiti ir arī
slodzes **kanoniskā JSON serializācija** — atslēgas sakārtotas
alfabētiski, kompakti atdalītāji (`,` un `:` bez atstarpēm), UTF-8.
Ja jūsu ietvars nodrošina tikai parsētu JSON, serializējiet to atkārtoti kanoniski un
aprēķiniet tam HMAC:

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

Dodiet priekšroku neapstrādātajam pamattekstam — tas ir par vienu soli mazāk un nav pakļauts JSON skaitļu atkārtotas pārveidošanas īpatnībām dažās valodās. Tas pats attiecas uz V2 pamatteksta lauku.

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

## Biežākās kļūdas

<AccordionGroup>
  <Accordion title="Pulksteņa laika nobīde">
    5 minūšu periods tiek mērīts pēc jūsu servera pulksteņa. Sinhronizējiet to
    ar NTP; pulkstenis, kas atpaliek vai steidzas vairāk nekā par 5 minūtēm, noraida ikvienu
    pieprasījumu.
  </Accordion>

  <Accordion title="Starpniekserveris pārraksta ceļu">
    Parakstītais ceļš ir tas, kuru pieprasījām: URL ceļš, ko
    konfigurējāt ThunderPhone. Ja reversais starpniekserveris vai API vārteja noņem
    vai pievieno prefiksu, pirms jūsu lietojumprogramma saņem pieprasījumu, pirms pārbaudes atjaunojiet
    mūsu nosūtīto ceļu (piemēram, pievienojiet prefiksu atpakaļ).
  </Accordion>

  <Accordion title="Atkārtota serializēšana ar noklusējuma formatējumu">
    Pamatteksta parsēšana un atkārtota serializēšana ar jūsu JSON bibliotēkas
    noklusējuma iestatījumiem (atstarpes pēc `,` / `:`, ievietošanas secībā sakārtotas atslēgas) rada
    atšķirīgus baitus un sabojā HMAC. Pārbaudiet neapstrādāto pamattekstu — vai, ja
    tas ir atkārtoti jāserializē, precīzi ievērojiet mūsu kanonisko formu: sakārtotas
    atslēgas, kompakti atdalītāji, UTF-8.
  </Accordion>

  <Accordion title="Ietvars automātiski parsē JSON">
    Express `express.json()` starpprogrammatūra patērē pamatteksta plūsmu,
    un jūs zaudējat neapstrādātos baitus. Konkrēti tīmekļa aizķeres maršrutā izmantojiet `express.raw()`
    vai saglabājiet neapstrādāto pamattekstu buferī pirms starpprogrammatūras.
    Tas pats attiecas uz NestJS / Koa — skatiet to dokumentāciju par “neapstrādātu pamattekstu”.
  </Accordion>

  <Accordion title="Salīdzināšana, kas nav droša pret laika uzbrukumiem">
    `expected === signature` JS vai `expected == signature` valodā
    Python ir salīdzinājumi ar mainīgu izpildes laiku. Attiecīgi izmantojiet `crypto.timingSafeEqual`
    vai `hmac.compare_digest`. Veiktspējas atšķirība
    ir niecīga.
  </Accordion>

  <Accordion title="Nepareizs noslēpums rīku galapunktiem">
    Tiešie rīku galapunktu izsaukumi tiek parakstīti ar **organizācijas līmeņa tīmekļa aizķeres
    noslēpumu** (`GET /v1/webhook`) — nevis ar katra galapunkta noslēpumu
    no `/v1/developer/webhook-endpoints`. Izmantojiet to pašu palīgfunkciju,
    bet pārliecinieties, ka rīku maršrutos tai nododat organizācijas noslēpumu.
  </Accordion>

  <Accordion title="Vaicājuma virknes GET/DELETE rīkos">
    V2 paraksta vaicājuma virkni tieši tādu, kāda tā ir nosūtīta. Mantotā galvene paraksta
    tukšo pamattekstu, bet nekad vaicājuma virkni, tāpēc URL jaucējvērtības aprēķināšana
    mantotajai galvenei nekad nesakritīs.
  </Accordion>

  <Accordion title="Dublikāti periodā">
    Laikspiedols aptur atkārtotus pieprasījumus, kas ir vecāki par 5 minūtēm, nevis otru kopiju
    šajā periodā. Galapunktu piegādes notiek vismaz vienu reizi, tāpēc var tikt saņemts arī
    tā paša notikuma atkārtots mēģinājums; novērsiet dublikātus pēc
    aploksnes `event_id`.
  </Accordion>

  <Accordion title="401 neatgriešana neatbilstības gadījumā">
    200 atgriešana neveiksmīgas verifikācijas gadījumā padara apstrādātāju par atkārtotas atskaņošanas
    uzbrukuma mērķi. Ja verifikācija neizdodas, vienmēr atbildiet ar statusu, kas nav 2xx.
  </Accordion>
</AccordionGroup>

---

## Nākamās darbības

<CardGroup cols={2}>
  <Card title="Tīmekļa aizķeru pārskats" icon="bolt" href="/lv/webhooks/overview">
    Piegādes semantika, atkārtoti mēģinājumi, avota IP adreses.
  </Card>
  <Card title="Tīmekļa aizķeru galapunkti" icon="plug" href="/lv/webhooks/endpoints">
    Pārvaldiet vairākus URL, mainiet noslēpumus.
  </Card>
  <Card title="Funkciju rīki" icon="screwdriver-wrench" href="/lv/tools/overview">
    Abi rīku izsaukšanas ceļi un to pieprasījumu formas.
  </Card>
  <Card title="Rīku integrācijas" icon="wrench" href="/lv/guides/build-tool-integration">
    Izveidojiet pilnīgu, ar rīkiem nodrošinātu integrāciju no sākuma līdz beigām.
  </Card>
</CardGroup>
