ThunderPhone 2.0 आता लाइव्ह आहे.स्वतःच सुरू करा—2¢/मिनिटपासून.घोषणा वाचा

Operations

वेबहुक स्वाक्षऱ्या सत्यापित करा

ThunderPhone पाठवणारी प्रत्येक वेबहुक आणि टूल विनंती टाइमस्टॅम्पसह स्वाक्षरीत असते. येथे दिलेल्या रेसिपी किंवा SDK हेल्परसह ती सत्यापित करा आणि पाच मिनिटांपेक्षा जुन्या पुनर्प्रक्षेपित विनंत्या नाकारा.

तुमच्या सर्व्हरला आम्ही पाठवणाऱ्या प्रत्येक विनंतीमध्ये (webhook वितरणे, लेगसी single-URL webhook आणि tool-endpoint कॉल) दोन स्वाक्षरी हेडर असतात. दोन्ही एकाच गुपितासह HMAC-SHA256 आहेत.

हेडरस्वाक्षरी करतेरिप्ले संरक्षण
X-ThunderPhone-Signature-V2 (शिफारस केलेले)टाइमस्टॅम्प, पद्धत, पथ, क्वेरी आणि बॉडी5 मिनिटांपेक्षा जुन्या विनंत्या नाकारा
X-ThunderPhone-Signatureफक्त बॉडीनाही: कॅप्चर केलेली विनंती कायम सत्यापित होते

नवीन इंटिग्रेशनमध्ये X-ThunderPhone-Signature-V2 सत्यापित करा आणि विद्यमान इंटिग्रेशन त्यावर स्थलांतरित करा. X-ThunderPhone-Signature अजूनही प्रत्येक विनंतीवर अपरिवर्तित पाठवले जाते, त्यामुळे त्यावर तयार केलेले व्हेरिफायर कार्यरत राहतात. आम्ही ते पाठवणे थांबवण्यापूर्वी तारीख जाहीर करू.

स्वाक्षरी V2

X-ThunderPhone-Signature-V2: t=1767225600,v1=03a89bec521f78d55727237ca4713bb0b8dcdf2e09e450865b5b5637b80c1c4e
  • t हा आम्ही विनंती पाठवली तेव्हाचा सेकंदांमधील Unix वेळ आहे. पुन्हा प्रयत्न केलेल्या डिलिव्हरीवर ती पाठवताना तिच्या स्वतःच्या t सह पुन्हा स्वाक्षरी केली जाते.
  • v1 हे तुमच्या secret ने की केलेल्या खालील स्वाक्षरी केलेल्या स्ट्रिंगचे hex HMAC-SHA256 आहे.

मूल्य ही स्वल्पविरामाने विभाजित केलेली key=value जोड्यांची सूची आहे. तुम्हाला ओळखीच्या नसलेल्या की दुर्लक्षित करा आणि कोणतेही v1 जुळल्यास विनंती स्वीकारा: भविष्यातील बदलादरम्यान आम्ही दुसरी स्वाक्षरी जोडू शकतो.

स्वाक्षरी केलेली स्ट्रिंग

चार फील्ड, शेवटचे वगळता प्रत्येकानंतर एक newline (\n, बाइट 0x0A):

<t>\n<METHOD>\n<path and query>\n<body>
फील्डमूल्य
tहेडरमधील t मूल्य
METHODमोठ्या अक्षरांमधील HTTP पद्धत: POST, GET, PUT, PATCH किंवा DELETE
path and queryHTTP विनंती ओळीतील विनंती लक्ष्य: प्रथम path, नंतर ? आणि असल्यास query स्ट्रिंग. scheme, host किंवा fragment नाही. डीकोड न करता किंवा पॅरामीटर्सचा क्रम न बदलता, ते जसे प्राप्त झाले तसेच वापरा
bodyकच्चे विनंती body. GET आणि DELETE tool call ला body नसते, त्यामुळे त्यांची स्वाक्षरी केलेली स्ट्रिंग path नंतरच्या newline ने समाप्त होते

सत्यापित करण्यासाठी:

  1. कोणत्याही parsing पूर्वी हेडर, पद्धत, कच्चे path आणि query, तसेच कच्चे body वाचा.
  2. हेडरमधून t आणि प्रत्येक v1 घ्या.
  3. t तुमच्या घड्याळाच्या 300 सेकंद आधी किंवा नंतर असल्यास विनंती नाकारा.
  4. तुमच्या secret सह स्वाक्षरी केलेल्या स्ट्रिंगचे hex HMAC-SHA256 गणना करा.
  5. त्याची v1 शी constant time मध्ये तुलना करा. काहीही जुळत नसल्यास 401 प्रतिसाद द्या.

कार्य केलेले उदाहरण

Secret whsec_example_4c1f2a9b. आम्ही ही विनंती t = 1767225600 (2026-01-01 00:00:00 UTC) येथे पाठवतो:

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

स्वाक्षरी केलेली स्ट्रिंग body नंतर newline नसलेल्या या चार ओळी आहेत:

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

त्याचे HMAC-SHA256 आहे 03a89bec521f78d55727237ca4713bb0b8dcdf2e09e450865b5b5637b80c1c4e. त्याच्या विरुद्ध तुमची अंमलबजावणी तपासा किंवा openssl वापरून ते पुनर्निर्मित करा:

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

त्याच सेकंदाला /tools/lookup?customer_id=cust_123 साठीच्या GET tool call चे body रिकामे असते. त्याची स्वाक्षरी केलेली स्ट्रिंग 1767225600\nGET\n/tools/lookup?customer_id=cust_123\n आहे आणि त्याची स्वाक्षरी 3ce214cb0b8f2bf39b1d19d4e7265df9dbaff5a5f838d3cc6c753a8f7a4ad280 आहे.

SDK सह

Python आणि TypeScript SDK मध्ये एक helper समाविष्ट आहे (thunderphone 0.2.0 किंवा नंतरचे, @thunderphone/sdk 0.2.0 किंवा नंतरचे). ते 5-मिनिटांची वेळ मर्यादा तपासते, constant time मध्ये तुलना करते आणि secrets ची सूची स्वीकारते, त्यामुळे तुम्ही डाउनटाइमशिवाय rotation करू शकता.

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
)
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
});

SDK शिवाय

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)
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)
    );
  });
}

फ्रेमवर्क एकीकरण

प्रत्येक उदाहरण रॉ पथ आणि क्वेरी तसेच रॉ बॉडी वाचते, आणि नंतर SDK हेल्परला कॉल करते.

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}
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);
  },
);
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})

req.originalUrl (Express) आणि ASGI मधील raw_path व query_string (FastAPI) हे प्राप्त झाल्याप्रमाणेच लक्ष्य आहेत. Django मधील get_full_path() आणि Flask मधील request.path प्रथम पथ डीकोड करतात, ज्यामुळे सामान्य पथांसाठी समान स्ट्रिंग मिळते. तुमच्या एंडपॉइंट पथामध्ये %2F, ; किंवा = यांसारखी टक्केवारी-एन्कोड केलेली किंवा राखीव अक्षरे असल्यास, त्याऐवजी तुमचा सर्व्हर पुरवत असलेले रॉ लक्ष्य वाचा (उदाहरणार्थ, Gunicorn वर request.META["RAW_URI"]).

कोणते गुप्त की?

दोन्ही हेडर्सवर स्वाक्षरी करण्यासाठी एकच secret वापरला जातो.

स्रोतगुप्त की
Webhook एंडपॉइंट (/v1/developer/webhook-endpoints)तयार करताना एकदाच परत मिळणारे प्रत्येक एंडपॉइंटसाठीचे secret (48 हेक्स अक्षरे)
लेगसी सिंगल-URL webhookGET /v1/webhook वर परत मिळणारे प्रत्येक ऑर्गनायझेशनसाठीचे secret
टूल-एंडपॉइंट इनव्होकेशन (तुमच्या endpoint.url ला थेट कॉल)ऑर्गनायझेशन-स्तरीय webhook गुप्त की (लेगसी सिंगल-URL webhook प्रमाणेच) — प्रत्येक एंडपॉइंटची गुप्त की नाही

गुप्त की तुमच्या सीक्रेट मॅनेजरमध्ये किंवा env var मध्ये साठवा — ती कधीही कमिट करू नका.

टूल कॉल सत्यापित करणे

जेव्हा एजंट तुमच्या फंक्शन टूल्स पैकी एक थेट कॉल करतो (त्या टूलकडे endpoint असते), तेव्हा विनंतीमध्ये तुमच्या कॉन्फिगर केलेल्या endpoint.headers सोबत खालील ThunderPhone हेडर असतात:

  • X-ThunderPhone-Call-ID — सुरू असलेल्या कॉलचा संख्यात्मक ID.
  • X-ThunderPhone-Signature-V2 — तुमच्या संस्थास्तरीय वेबहुक गुपिताने की केलेली वरील टाइमस्टॅम्पयुक्त स्वाक्षरी.
  • X-ThunderPhone-Signature — तोच की वापरणारी लेगसी केवळ-बॉडी स्वाक्षरी.

वेबहुकपेक्षा दोन गोष्टी वेगळ्या आहेत:

  1. GET / DELETE टूल्सना बॉडी नसते. आर्ग्युमेंट्स क्वेरी पॅरामीटर्सद्वारे पाठवले जातात. V2 स्वाक्षरी पाथ आणि क्वेरीद्वारे त्यांना कव्हर करते, आणि बॉडी फील्ड रिकामे असते. मूळ क्वेरी स्ट्रिंगसह पडताळणी करा: पॅरामीटर्स पार्स करून नवीन क्वेरी स्ट्रिंग तयार केल्यास त्यांचा क्रम किंवा एन्कोडिंग बदलू शकते. लेगसी हेडर रिकाम्या बाइट स्ट्रिंगची स्वाक्षरी करते.
  2. ज्या संस्थांमध्ये लेगसी वेबहुक कॉन्फिगर केलेला नाही, त्यांच्याकडे संस्था गुपित नसते. त्या प्रकरणात टूल कॉल्समध्ये फक्त X-ThunderPhone-Call-ID असते आणि कोणतेही स्वाक्षरी हेडर नसतात. स्वाक्षरी गुपित मिळवण्यासाठी लेगसी वेबहुक (PUT /v1/webhook) कॉन्फिगर करा किंवा endpoint.headers द्वारे तुमच्या स्वतःच्या हेडरने टूल कॉल्सचे प्रमाणीकरण करा.
@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)
    ...

Express मध्ये, बॉडी पार्सर GET वर req.body रिकामा ऑब्जेक्ट म्हणून ठेवतो, म्हणून Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0) पाठवा.

Webhook-मोड टूल डिस्पॅच (endpoint नसलेली आणि तुमच्या संस्था webhook वर telephony.tool / web.tool म्हणून वितरित होणारी टूल्स) हा सामान्य स्वाक्षरीत webhook आहे — वरील मानक पद्धत लागू होते. दोन्ही विनंती स्वरूपांसाठी फंक्शन टूल्स पहा.

जुनी स्वाक्षरी: X-ThunderPhone-Signature

मूळ हेडर हा त्याच secret सह फक्त raw body चा हेक्स HMAC-SHA256 असतो. त्यात timestamp नसतो, त्यामुळे स्वाक्षरी केलेली विनंती कॅप्चर करणारा कोणीही ती पुन्हा पाठवू शकतो. इंटिग्रेशन V2 कडे स्थलांतरित होत असतानाही तो प्रत्येक विनंतीसोबत पाठवला जातो.

  1. कच्चा विनंती body वाचा — आम्ही तुम्हाला पाठवलेले अचूक bytes.
  2. hmac_sha256(secret, body).hexdigest() गणना करा.
  3. X-ThunderPhone-Signature शी स्थिर वेळेत तुलना करा.

आम्ही प्रसारित करतो तेच अचूक bytes स्वाक्षरीत करतो. ते bytes payload चे कॅनॉनिकल JSON क्रमिकीकरण देखील असतात — की alphabetically क्रमबद्ध केलेल्या, compact separators (रिकाम्या जागांशिवाय , आणि :), UTF-8. तुमचे framework फक्त parsed JSON उपलब्ध करून देत असल्यास, ते कॅनॉनिकली पुन्हा क्रमीकृत करा आणि त्याचा HMAC करा:

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

कच्चा body प्राधान्याने वापरा — त्यात एक पायरी कमी असते आणि काही भाषांमधील JSON number round-tripping च्या वैशिष्ट्यांपासून तो सुरक्षित असतो. V2 च्या body field साठीही हेच लागू होते.

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 "")
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
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
require "openssl"
 
def verify(body, signature, secret)
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
  Rack::Utils.secure_compare(expected, signature.to_s)
end

सामान्य अडचणी

घड्याळातील तफावत

5-मिनिटांची मर्यादा तुमच्या सर्व्हरच्या घड्याळानुसार मोजली जाते. ते NTP सह समक्रमित ठेवा; 5 मिनिटांपेक्षा जास्त फरक असलेले घड्याळ प्रत्येक विनंती नाकारते.

प्रॉक्सी मार्ग पुन्हा लिहिते

स्वाक्षरी केलेला मार्ग आम्ही विनंती केलेलाच असतो: तुम्ही ThunderPhone मध्ये कॉन्फिगर केलेल्या URL चा मार्ग. तुमच्या ॲप्लिकेशनला विनंती दिसण्यापूर्वी रिव्हर्स प्रॉक्सी किंवा API गेटवे उपसर्ग काढून टाकत असेल किंवा जोडत असेल, तर पडताळणी करण्यापूर्वी आम्ही पाठवलेला मार्ग पुन्हा तयार करा (उदाहरणार्थ, उपसर्ग परत जोडा).

डीफॉल्ट स्वरूपनासह पुन्हा अनुक्रमीकरण

बॉडी पार्स करून ती तुमच्या JSON लायब्ररीच्या डीफॉल्ट सेटिंग्जसह पुन्हा डंप केल्यास (, / : नंतरच्या रिकाम्या जागा, समाविष्ट करण्याच्या क्रमाने असलेल्या की) वेगळे बाइट्स तयार होतात आणि HMAC खंडित होते. रॉ बॉडी पडताळा — किंवा तुम्हाला पुन्हा अनुक्रमीकरण करणे आवश्यक असल्यास, आमच्या कॅनॉनिकल स्वरूपाशी अचूक जुळवा: क्रमबद्ध की, संक्षिप्त विभाजक, UTF-8.

फ्रेमवर्क JSON आपोआप पार्स करते

Express चे express.json() मिडलवेअर बॉडी स्ट्रीम वापरून टाकते आणि रॉ बाइट्स गमावले जातात. वेबहुक रूटवर विशेषतः express.raw() वापरा, किंवा प्री-मिडलवेअरमध्ये रॉ बॉडी बफर करा. NestJS / Koa साठीही तेच लागू होते — त्यांचे "रॉ बॉडी" दस्तऐवज तपासा.

टायमिंगसाठी असुरक्षित तुलना

JS मधील expected === signature किंवा Python मधील expected == signature या टायमिंग-व्हेरिएबल तुलना आहेत. अनुक्रमे crypto.timingSafeEqual किंवा hmac.compare_digest वापरा. कार्यक्षमतेतील फरक नगण्य आहे.

टूल एंडपॉइंट्ससाठी चुकीचे सीक्रेट

थेट टूल-एंडपॉइंट कॉल्सवर संस्था-स्तरीय वेबहुक सीक्रेट (GET /v1/webhook) सह स्वाक्षरी केली जाते — /v1/developer/webhook-endpoints मधील कोणत्याही प्रति-एंडपॉइंट सीक्रेटसह नाही. तोच हेल्पर पुन्हा वापरा, परंतु टूल मार्गांवर तुम्ही त्याला संस्थेचे सीक्रेट देत आहात याची खात्री करा.

GET/DELETE टूल्सवरील क्वेरी स्ट्रिंग्स

V2 क्वेरी स्ट्रिंग पाठवली आहे तशीच स्वाक्षरी करते. लेगसी हेडर रिकाम्या बॉडीवर स्वाक्षरी करते आणि क्वेरी स्ट्रिंगवर कधीही नाही, त्यामुळे लेगसी हेडरसाठी URL हॅश केल्यास ते कधीही जुळणार नाही.

मर्यादेतील डुप्लिकेट्स

टाइमस्टॅम्प 5 मिनिटांपेक्षा जुन्या रिप्ले थांबवतो, त्या मर्यादेतील दुसरी प्रत नाही. एंडपॉइंट डिलिव्हरी किमान-एकदा होते, त्यामुळे त्याच इव्हेंटचा पुनःप्रयत्न देखील येऊ शकतो; एन्व्हलपच्या event_id वर डुप्लिकेट काढून टाका.

न जुळल्यास 401 परत न करणे

सत्यापन अयशस्वी झाल्यावर 200 परत केल्यास हँडलर रिप्ले टार्गेट बनतो. सत्यापन अयशस्वी झाल्यास नेहमी non-2xx प्रतिसाद द्या.


पुढील पायऱ्या