---
title: "ওয়েবহুক স্বাক্ষর যাচাই করুন"
description: "ThunderPhone পাঠানো প্রতিটি ওয়েবহুক ও টুল অনুরোধ স্বাক্ষরিত থাকে। এখানকার পদ্ধতি দিয়ে একবার স্বাক্ষর যাচাই করুন, তারপর আপনার চালানো প্রতিটি এন্ডপয়েন্টে একই যাচাই পুনরায় ব্যবহার করুন।"
---

আপনার সার্ভারে আমরা পাঠানো প্রতিটি অনুরোধ — webhook ডেলিভারি এবং
টুল-এন্ডপয়েন্ট ইনভোকেশন — `X-ThunderPhone-Signature` হেডারে একটি HMAC-SHA256 স্বাক্ষর বহন করে। একবার যাচাইকরণ সঠিকভাবে করুন এবং
প্রতিটি হ্যান্ডলারে একই সহায়ক ব্যবহার করুন।

## অ্যালগরিদম

1. **অপরিবর্তিত** রিকোয়েস্ট বডি পড়ুন — আমরা আপনাকে POST করা সঠিক বাইটগুলো।
2. `hmac_sha256(secret, body).hexdigest()` গণনা করুন।
3. `X-ThunderPhone-Signature`-এর সঙ্গে **স্থির সময়ে** তুলনা করুন।
   (সাধারণ স্ট্রিং তুলনা টাইমিং তথ্য ফাঁস করে।)

আমরা ঠিক যে বাইটগুলো পাঠাই, সেগুলোতেই স্বাক্ষর করি, তাই অপরিবর্তিত বডি
যাচাই সবসময় কাজ করে। এই বাইটগুলো পে-লোডের **ক্যানোনিক্যাল JSON সিরয়ালাইজেশনও**
— কীগুলো বর্ণানুক্রমে সাজানো, কমপ্যাক্ট সেপারেটর
(কোনো স্পেস ছাড়া `,` এবং `:`), UTF-8। আপনার ফ্রেমওয়ার্ক যদি শুধু পার্স করা JSON প্রকাশ করে, তাহলে এটি আপনাকে দ্বিতীয়, সম্পূর্ণ
সমতুল্য পদ্ধতি দেয়: ক্যানোনিক্যালভাবে পুনরায় সিরয়ালাইজ করুন এবং তাতে HMAC প্রয়োগ করুন।

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

অপরিবর্তিত বডিই অগ্রাধিকার দিন — এতে একটি ধাপ কম এবং কিছু ভাষায় JSON সংখ্যার
রাউন্ড-ট্রিপিংজনিত অসঙ্গতি থেকে সুরক্ষিত।

## কোন সিক্রেট?

| উৎস | সিক্রেট |
|--------|--------|
| [Webhook এন্ডপয়েন্ট](/bn/webhooks/endpoints) (`/v1/developer/webhook-endpoints`) | তৈরির সময় একবার ফেরত দেওয়া প্রতি-এন্ডপয়েন্ট `secret` (48 হেক্স অক্ষর) |
| [লিগ্যাসি একক-URL webhook](/api-reference/organizations#legacy-single-url-webhook) | `GET /v1/webhook`-এ ফেরত দেওয়া প্রতি-অর্গ `secret` |
| [টুল-এন্ডপয়েন্ট ইনভোকেশন](/bn/tools/overview) (আপনার `endpoint.url`-এ সরাসরি কল) | **অর্গ-স্তরের webhook সিক্রেট** (লিগ্যাসি একক-URL webhook-এর একইটি) — প্রতি-এন্ডপয়েন্ট সিক্রেট নয় |

সিক্রেটটি আপনার সিক্রেট ম্যানেজার বা env var-এ সংরক্ষণ করুন — কখনো কমিট করবেন না।

## রেফারেন্স ইমপ্লিমেন্টেশন

চারটিই অপরিবর্তিত রিকোয়েস্ট বডি যাচাই করে:

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

## ফ্রেমওয়ার্ক-নির্দিষ্ট সংযোগ

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

## টুল কল যাচাই করা

এজেন্ট যখন আপনার কোনো
[ফাংশন টুল](/bn/tools/overview) সরাসরি আহ্বান করে (টুলটির একটি
`endpoint` থাকে), তখন অনুরোধটি আপনার কনফিগার করা `endpoint.headers`-এর
পাশাপাশি দুটি ThunderPhone হেডার বহন করে:

- `X-ThunderPhone-Call-ID` — চলমান কলের সংখ্যাসূচক id।
- `X-ThunderPhone-Signature` — সঠিক অনুরোধ-বডির বাইটের ওপর আপনার
  **সংস্থা-স্তরের webhook secret** দিয়ে কী করা HMAC-SHA256।

একই `verify()` হেল্পার অপরিবর্তিতভাবে কাজ করে, তবে দুটি বিষয় মনে রাখুন:

1. **`GET` / `DELETE` টুলের কোনো বডি থাকে না।** আর্গুমেন্টগুলো কোয়েরি
   প্যারামিটার হিসেবে যায়, এবং স্বাক্ষরটি **খালি বাইট স্ট্রিং**-এর ওপর
   গণনা করা হয় — তাই `verify(b"", sig, secret)` (Python) অথবা
   `verify(Buffer.alloc(0), sig, secret)` (Node) ব্যবহার করুন। কোয়েরি
   স্ট্রিং হ্যাশ করবেন **না**।
2. **লিগ্যাসি webhook কনফিগার করা নেই এমন সংস্থার কোনো সংস্থা secret নেই।**
   সে ক্ষেত্রে টুল কলগুলো শুধু `X-ThunderPhone-Call-ID` বহন করে এবং কোনো
   স্বাক্ষর হেডার থাকে না। সাইনিং secret পেতে লিগ্যাসি 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 — উপরের মানক পদ্ধতিটি প্রযোজ্য। উভয় অনুরোধের ধরন দেখতে
[ফাংশন টুল](/bn/tools/overview) দেখুন।

## সাধারণ সমস্যাসমূহ

<AccordionGroup>
  <Accordion title="ডিফল্ট ফরম্যাটিং দিয়ে পুনরায় সিরিয়ালাইজ করা">
    বডি পার্স করে আপনার JSON লাইব্রেরির ডিফল্ট সেটিংস দিয়ে আবার ডাম্প করলে
    (`,` / `:`-এর পরে স্পেস, ইনসার্শন-অর্ডার করা কী) ভিন্ন
    বাইট তৈরি হয় এবং HMAC ব্যর্থ হয়। কাঁচা বডি যাচাই করুন — অথবা পুনরায় সিরিয়ালাইজ করতেই হলে,
    আমাদের ক্যানোনিক্যাল ফর্মটি হুবহু মিলিয়ে নিন: সাজানো
    কী, কমপ্যাক্ট সেপারেটর, UTF-8।
  </Accordion>

  <Accordion title="ফ্রেমওয়ার্ক স্বয়ংক্রিয়ভাবে JSON পার্স করে">
    Express-এর `express.json()` মিডলওয়্যার বডি স্ট্রিম
    ব্যবহার করে ফেলে এবং আপনি কাঁচা বাইট হারান। ওয়েবহুক
    রুটে নির্দিষ্টভাবে `express.raw()` ব্যবহার করুন, অথবা প্রি-মিডলওয়্যারে কাঁচা বডি বাফার করুন।
    NestJS / Koa-এর ক্ষেত্রেও একই বিষয় — তাদের "কাঁচা বডি" ডকুমেন্টেশন দেখুন।
  </Accordion>

  <Accordion title="টাইমিং-অনিরাপদ তুলনা">
    JS-এ `expected === signature` অথবা
    Python-এ `expected == signature` টাইমিং-ভেরিয়েবল তুলনা। যথাক্রমে `crypto.timingSafeEqual`
    অথবা `hmac.compare_digest` ব্যবহার করুন। পারফরম্যান্সের পার্থক্য
    নেই।
  </Accordion>

  <Accordion title="টুল এন্ডপয়েন্টের জন্য ভুল সিক্রেট">
    সরাসরি টুল-এন্ডপয়েন্ট কলগুলো **অর্গ-স্তরের ওয়েবহুক
    সিক্রেট** (`GET /v1/webhook`) দিয়ে সাইন করা হয় — 
    `/v1/developer/webhook-endpoints`-এর কোনো প্রতি-এন্ডপয়েন্ট সিক্রেট দিয়ে নয়। একই `verify()`
    ফাংশন পুনঃব্যবহার করুন, তবে নিশ্চিত করুন যে টুল রুটে এতে অর্গ সিক্রেটই দিচ্ছেন।
  </Accordion>

  <Accordion title="GET/DELETE টুলে কোয়েরি স্ট্রিং হ্যাশ করা">
    বডিবিহীন টুল মেথডের জন্য সিগনেচারটি খালি বাইট
    স্ট্রিং কভার করে, ফলে একটি সার্বজনীন পদ্ধতি বজায় থাকে: কাঁচা রিকোয়েস্ট বডিতে HMAC করুন,
    তা যা-ই হোক। URL বা কোয়েরি স্ট্রিং হ্যাশ করলে কখনোই মিলবে না।
  </Accordion>

  <Accordion title="অমিল হলে 401 ফেরত না দেওয়া">
    যাচাইকরণ ব্যর্থ হলে 200 ফেরত দিলে হ্যান্ডলারটি রিপ্লে
    টার্গেটে পরিণত হয়। যাচাইকরণ ব্যর্থ হলে সর্বদা নন-2xx রেসপন্স দিন।
  </Accordion>
</AccordionGroup>

---

## পরবর্তী ধাপ

<CardGroup cols={2}>
  <Card title="ওয়েবহুকের সংক্ষিপ্ত বিবরণ" icon="bolt" href="/bn/webhooks/overview">
    ডেলিভারি সেমান্টিকস, পুনরায় চেষ্টা, উৎস IP।
  </Card>
  <Card title="ওয়েবহুক এন্ডপয়েন্ট" icon="plug" href="/bn/webhooks/endpoints">
    একাধিক URL পরিচালনা করুন, সিক্রেট রোটেট করুন।
  </Card>
  <Card title="ফাংশন টুল" icon="screwdriver-wrench" href="/bn/tools/overview">
    দুটি টুল-ইনভোকেশন পাথ এবং তাদের রিকোয়েস্টের গঠন।
  </Card>
  <Card title="টুল ইন্টিগ্রেশন" icon="wrench" href="/bn/guides/build-tool-integration">
    শুরু থেকে শেষ পর্যন্ত একটি সম্পূর্ণ টুল-সমর্থিত ইন্টিগ্রেশন তৈরি করুন।
  </Card>
</CardGroup>
