---
title: "Επαληθεύστε τις υπογραφές webhook"
description: "Κάθε αίτημα webhook και εργαλείου που στέλνει το ThunderPhone είναι υπογεγραμμένο. Επαληθεύστε την υπογραφή μία φορά με την παρακάτω διαδικασία και, στη συνέχεια, χρησιμοποιήστε τον ίδιο έλεγχο σε κάθε τελικό σημείο που εκτελείτε."
---

Κάθε αίτημα που στέλνουμε στον διακομιστή σας — παραδόσεις webhook και
επικλήσεις τελικών σημείων εργαλείων — περιλαμβάνει μια υπογραφή HMAC-SHA256 στην
κεφαλίδα `X-ThunderPhone-Signature`. Ρυθμίστε σωστά την επαλήθευση μία φορά και
χρησιμοποιήστε τον ίδιο βοηθό σε κάθε χειριστή.

## Ο αλγόριθμος

1. Διαβάστε το **ακατέργαστο** σώμα του αιτήματος — τα ακριβή byte που σας στείλαμε με POST.
2. Υπολογίστε `hmac_sha256(secret, body).hexdigest()`.
3. Συγκρίνετε σε **σταθερό χρόνο** με το `X-ThunderPhone-Signature`.
   (Η αφελής σύγκριση συμβολοσειρών διαρρέει πληροφορίες χρονισμού.)

Υπογράφουμε ακριβώς τα byte που μεταδίδουμε, επομένως η επαλήθευση του ακατέργαστου σώματος
λειτουργεί πάντα. Αυτά τα byte είναι επίσης η **κανονική σειριοποίηση JSON**
του ωφέλιμου φορτίου — κλειδιά ταξινομημένα αλφαβητικά, συμπαγείς διαχωριστές
(`,` και `:` χωρίς κενά), UTF-8. Αυτό σας παρέχει μια δεύτερη, πλήρως
ισοδύναμη μέθοδο όταν το framework σας εκθέτει μόνο αναλυμένο JSON:
σειριοποιήστε ξανά κανονικά και υπολογίστε HMAC σε αυτό.

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

Προτιμήστε το ακατέργαστο σώμα — είναι ένα βήμα λιγότερο και δεν επηρεάζεται από
ιδιαιτερότητες επαναμετατροπής αριθμών JSON σε ορισμένες γλώσσες.

## Ποιο μυστικό;

| Πηγή | Μυστικό |
|--------|--------|
| [Τελικό σημείο webhook](/el/webhooks/endpoints) (`/v1/developer/webhook-endpoints`) | `secret` ανά τελικό σημείο (48 δεκαεξαδικοί χαρακτήρες), που επιστρέφεται μία φορά κατά τη δημιουργία |
| [Παλαιού τύπου webhook με ένα URL](/api-reference/organizations#legacy-single-url-webhook) | `secret` ανά οργανισμό που επιστρέφεται στο `GET /v1/webhook` |
| [Επίκληση τελικού σημείου εργαλείου](/el/tools/overview) (άμεση κλήση στο `endpoint.url` σας) | Το **μυστικό webhook σε επίπεδο οργανισμού** (το ίδιο με το παλαιού τύπου webhook με ένα URL) — όχι μυστικό ανά τελικό σημείο |

Αποθηκεύστε το μυστικό στον διαχειριστή μυστικών ή σε μεταβλητή περιβάλλοντος — μην το υποβάλετε ποτέ σε commit.

## Υλοποιήσεις αναφοράς

Και οι τέσσερις επαληθεύουν το ακατέργαστο σώμα του αιτήματος:

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

## Σύνδεση ανά framework

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

## Επαλήθευση κλήσεων εργαλείων

Όταν ο πράκτορας καλεί απευθείας ένα από τα
[εργαλεία συναρτήσεων](/el/tools/overview) σας (το εργαλείο διαθέτει
`endpoint`), το αίτημα περιλαμβάνει δύο κεφαλίδες ThunderPhone μαζί με
τις ρυθμισμένες `endpoint.headers` σας:

- `X-ThunderPhone-Call-ID` — το αριθμητικό αναγνωριστικό της ενεργής κλήσης.
- `X-ThunderPhone-Signature` — HMAC-SHA256, με κλειδί το
  **μυστικό webhook σε επίπεδο οργανισμού**, πάνω στα ακριβή byte του σώματος του αιτήματος.

Ο ίδιος βοηθός `verify()` λειτουργεί χωρίς αλλαγές, με δύο ιδιαιτερότητες:

1. **Τα εργαλεία `GET` / `DELETE` δεν έχουν σώμα.** Τα ορίσματα μεταφέρονται ως παράμετροι ερωτήματος
   και η υπογραφή υπολογίζεται πάνω στην **κενή συμβολοσειρά byte**
   — επομένως `verify(b"", sig, secret)` (Python) ή
   `verify(Buffer.alloc(0), sig, secret)` (Node). Μην κάνετε hash τη
   συμβολοσειρά ερωτήματος.
2. **Οι οργανισμοί χωρίς ρυθμισμένο παλαιού τύπου webhook δεν έχουν μυστικό οργανισμού.** Σε
   αυτήν την περίπτωση, οι κλήσεις εργαλείων περιλαμβάνουν μόνο `X-ThunderPhone-Call-ID` και καμία
   κεφαλίδα υπογραφής. Ρυθμίστε το παλαιού τύπου webhook
   (`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)
    ...
```

Η δρομολόγηση εργαλείων σε **λειτουργία** webhook (εργαλεία χωρίς `endpoint`, που παραδίδονται
στο webhook του οργανισμού σας ως `telephony.tool` / `web.tool`) είναι ένα συνηθισμένο
υπογεγραμμένο webhook — εφαρμόζεται η παραπάνω τυπική διαδικασία. Δείτε τα
[Εργαλεία συναρτήσεων](/el/tools/overview) για τις δύο μορφές αιτημάτων.

## Συνήθη προβλήματα

<AccordionGroup>
  <Accordion title="Επανασειριοποίηση με προεπιλεγμένη μορφοποίηση">
    Η ανάλυση του σώματος και η εκ νέου εξαγωγή του με τις
    προεπιλογές της βιβλιοθήκης JSON σας (κενά μετά τα `,` / `:`, κλειδιά με σειρά εισαγωγής) παράγει
    διαφορετικά byte και καταστρέφει το HMAC. Επαληθεύστε το ακατέργαστο σώμα — ή, αν
    πρέπει να το επανασειριοποιήσετε, αντιστοιχίστε ακριβώς την κανονική μας μορφή: ταξινομημένα
    κλειδιά, συμπαγείς διαχωριστές, UTF-8.
  </Accordion>

  <Accordion title="Το framework αναλύει αυτόματα το JSON">
    Το middleware `express.json()` του Express καταναλώνει τη ροή του σώματος
    και χάνετε τα ακατέργαστα byte. Χρησιμοποιήστε `express.raw()` ειδικά στη διαδρομή
    webhook ή αποθηκεύστε προσωρινά το ακατέργαστο σώμα σε ένα προ-middleware.
    Το ίδιο ισχύει για NestJS / Koa — ελέγξτε την τεκμηρίωσή τους για το «ακατέργαστο σώμα».
  </Accordion>

  <Accordion title="Σύγκριση μη ασφαλής ως προς τον χρόνο">
    Τα `expected === signature` σε JS ή `expected == signature` σε
    Python είναι συγκρίσεις μεταβλητού χρόνου. Χρησιμοποιήστε `crypto.timingSafeEqual`
    ή `hmac.compare_digest` αντίστοιχα. Η διαφορά στην απόδοση
    είναι μηδενική.
  </Accordion>

  <Accordion title="Λανθασμένο μυστικό για endpoints εργαλείων">
    Οι άμεσες κλήσεις σε endpoint εργαλείων υπογράφονται με το **μυστικό webhook
    σε επίπεδο οργανισμού** (`GET /v1/webhook`) — όχι με κάποιο μυστικό ανά endpoint
    από το `/v1/developer/webhook-endpoints`. Επαναχρησιμοποιήστε την ίδια συνάρτηση `verify()`,
    αλλά βεβαιωθείτε ότι τροφοδοτείτε το μυστικό του οργανισμού στις διαδρομές εργαλείων.
  </Accordion>

  <Accordion title="Κατακερματισμός του query string σε εργαλεία GET/DELETE">
    Για μεθόδους εργαλείων χωρίς σώμα, η υπογραφή καλύπτει την κενή ακολουθία
    byte, διατηρώντας μία καθολική συνταγή: εφαρμόστε HMAC στο ακατέργαστο σώμα του αιτήματος,
    όποιο κι αν είναι. Ο κατακερματισμός του URL ή του query string δεν θα ταιριάξει ποτέ.
  </Accordion>

  <Accordion title="Μη επιστροφή 401 σε ασυμφωνία">
    Η επιστροφή 200 όταν η επαλήθευση αποτυγχάνει καθιστά τον χειριστή στόχο επανάληψης.
    Να απαντάτε πάντα με κωδικό εκτός 2xx αν η επαλήθευση αποτύχει.
  </Accordion>
</AccordionGroup>

---

## Επόμενα βήματα

<CardGroup cols={2}>
  <Card title="Επισκόπηση webhooks" icon="bolt" href="/el/webhooks/overview">
    Σημασιολογία παράδοσης, επαναλήψεις, IP πηγής.
  </Card>
  <Card title="Endpoints webhook" icon="plug" href="/el/webhooks/endpoints">
    Διαχειριστείτε πολλαπλά URL, εναλλάξτε μυστικά.
  </Card>
  <Card title="Εργαλεία συναρτήσεων" icon="screwdriver-wrench" href="/el/tools/overview">
    Οι δύο διαδρομές επίκλησης εργαλείων και οι μορφές αιτημάτων τους.
  </Card>
  <Card title="Ενσωματώσεις εργαλείων" icon="wrench" href="/el/guides/build-tool-integration">
    Δημιουργήστε μια πλήρη ενσωμάτωση με υποστήριξη εργαλείων από άκρο σε άκρο.
  </Card>
</CardGroup>
