Open in
वेबहुक स्वाक्षऱ्या सत्यापित करा
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=03a89bec521f78d55727237ca4713bb0b8dcdf2e09e450865b5b5637b80c1c4etहा आम्ही विनंती पाठवली तेव्हाचा सेकंदांमधील 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 query | HTTP विनंती ओळीतील विनंती लक्ष्य: प्रथम path, नंतर ? आणि असल्यास query स्ट्रिंग. scheme, host किंवा fragment नाही. डीकोड न करता किंवा पॅरामीटर्सचा क्रम न बदलता, ते जसे प्राप्त झाले तसेच वापरा |
body | कच्चे विनंती body. GET आणि DELETE tool call ला body नसते, त्यामुळे त्यांची स्वाक्षरी केलेली स्ट्रिंग path नंतरच्या newline ने समाप्त होते |
सत्यापित करण्यासाठी:
- कोणत्याही parsing पूर्वी हेडर, पद्धत, कच्चे path आणि query, तसेच कच्चे body वाचा.
- हेडरमधून
tआणि प्रत्येकv1घ्या. tतुमच्या घड्याळाच्या 300 सेकंद आधी किंवा नंतर असल्यास विनंती नाकारा.- तुमच्या secret सह स्वाक्षरी केलेल्या स्ट्रिंगचे hex HMAC-SHA256 गणना करा.
- त्याची
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 करू शकता.
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
)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 शिवाय
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)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 हेल्परला कॉल करते.
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}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);
},
);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 webhook | GET /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— तोच की वापरणारी लेगसी केवळ-बॉडी स्वाक्षरी.
वेबहुकपेक्षा दोन गोष्टी वेगळ्या आहेत:
GET/DELETEटूल्सना बॉडी नसते. आर्ग्युमेंट्स क्वेरी पॅरामीटर्सद्वारे पाठवले जातात. V2 स्वाक्षरी पाथ आणि क्वेरीद्वारे त्यांना कव्हर करते, आणि बॉडी फील्ड रिकामे असते. मूळ क्वेरी स्ट्रिंगसह पडताळणी करा: पॅरामीटर्स पार्स करून नवीन क्वेरी स्ट्रिंग तयार केल्यास त्यांचा क्रम किंवा एन्कोडिंग बदलू शकते. लेगसी हेडर रिकाम्या बाइट स्ट्रिंगची स्वाक्षरी करते.- ज्या संस्थांमध्ये लेगसी वेबहुक कॉन्फिगर केलेला नाही, त्यांच्याकडे संस्था गुपित नसते. त्या
प्रकरणात टूल कॉल्समध्ये फक्त
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 कडे स्थलांतरित होत असतानाही तो प्रत्येक विनंतीसोबत पाठवला जातो.
- कच्चा विनंती body वाचा — आम्ही तुम्हाला पाठवलेले अचूक bytes.
hmac_sha256(secret, body).hexdigest()गणना करा.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 साठीही हेच लागू होते.
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 "")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),
);
}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))
}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 प्रतिसाद द्या.