وبهوکها
رویدادهای حساب و محصولها را با درخواست امضاشده دریافت کنید؛ قالب دقیق، تایید امضا در چهار زبان و سیاست تلاش دوباره.
کارکرد
وبهوک نشانی https سرور شماست که نمایک هنگام رخ دادن رویداد (کمبود موجودی، توقف سرویس، ابطال کلید و…) یک درخواست POST امضاشده به آن میفرستد. وبهوک را در پنل پروژه یا با API عمومی بسازید؛ راز امضا (whsec_…) فقط همان یک بار نمایش داده میشود.
curl -sS -X POST "https://api.namayek.com/v1/webhooks" \ -H "X-Namayek-Key: $NAMAYEK_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://example.com/hooks/namayek","events":["billing.low_balance","api_key.revoked"]}'curl -sS -X POST "https://api.namayek.com/v1/webhooks/{endpointId}/test" \ -H "X-Namayek-Key: $NAMAYEK_SECRET_KEY"curl -sS -X POST "https://api.namayek.com/v1/webhooks/{endpointId}/rotate-secret" \ -H "X-Namayek-Key: $NAMAYEK_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"overlapHours":24}'قالب درخواست
بدنه یک پاکت JSON با UTF-8 و حداکثر ۶۴ کیلوبایت است:
{ "id": "evt_6sV0wQm2b3Xr8Lk1aZpQ9T", "type": "backup.succeeded", "created": "2026-10-23T10:00:00.000Z", "apiVersion": "2026-10", "project": { "id": "<uuid>", "slug": "default" }, "data": { } }| فیلد | نوع / مقدار | توضیح |
|---|---|---|
id | string | شناسه رویداد (همان Namayek-Event-Id). |
type | string | نوع رویداد. |
created | ISO 8601 | زمان رخ دادن رویداد به UTC. |
apiVersion | string | نسخه قالب (اکنون 2026-10). |
project | { id, slug } | پروژهای که رویداد مال آن است. |
data | object | داده ویژه هر رویداد؛ هرگز راز یا کلید در آن نیست. |
سرآیندها
POST /hooks/namayek HTTP/1.1Host: example.comContent-Type: application/json; charset=utf-8User-Agent: Namayek-Webhooks/1.0 (+https://namayek.com/docs/webhooks)Namayek-Event-Id: evt_6sV0wQm2b3Xr8Lk1aZpQ9TNamayek-Event-Type: billing.low_balanceNamayek-Delivery-Id: dlv_2N7xQ4pK9mR1Namayek-Attempt: 1Namayek-Signature: t=1792800000,v1=4d8512824d55c04b585dd61349811a29ec8305237211bcf43344d6c34850724d| سرآیند | نوع / مقدار | توضیح |
|---|---|---|
Content-Type | application/json; charset=utf-8 | بدنه JSON با UTF-8، حداکثر ۶۴ کیلوبایت. |
User-Agent | Namayek-Webhooks/1.0 (+…/docs/webhooks) | شناسه فرستنده. |
Namayek-Event-Id | evt_… | شناسه یکتای رویداد؛ برای جلوگیری از پردازش تکراری. |
Namayek-Event-Type | string | نوع رویداد، مثلا billing.low_balance. |
Namayek-Delivery-Id | string | شناسه این تحویل (ارسال دوباره همان رویداد شناسه تازه دارد). |
Namayek-Attempt | 1، 2، … | شماره تلاش. |
Namayek-Signature | t=…,v1=…[,v1=…] | زمان یونیکس و امضای HMAC-SHA256؛ در دوره چرخش راز دو v1 میآید. |
امضا
سرآیند Namayek-Signature زمان (t، ثانیه یونیکس) و یک یا چند امضای v1 دارد. امضا HMAC-SHA256 با کل راز (همراه پیشوند whsec_) روی t، یک نقطه و بدنه خام است:
v1 = hex( HMAC-SHA256( key = utf8(secret شامل "whsec_"), message = utf8(t + "." + rawBody) ) )- بدنه را خام (همان بایتها، پیش از تجزیه JSON) امضا کنید.
- با مقایسه زمانثابت در برابر هر v1 تطبیق دهید؛ در دوره چرخش راز، امضای راز قبلی هم میآید.
- اگر
|now − t| > 300ثانیه بود، درخواست را رد کنید (جلوگیری از تکرار).
این بردار آزمون را با پیادهسازی خود امتحان کنید:
secret = whsec_Tq7Rk2mWx9Lp4Vn8Bz3Hc6Jd1Fs5Gy0Et = 1792800000body = {"id":"evt_0000000000000000000001","type":"webhook.test"}v1 = 4d8512824d55c04b585dd61349811a29ec8305237211bcf43344d6c34850724dنمونه کد تایید
import crypto from "node:crypto"; const TOLERANCE_SECONDS = 300; /** rawBody همان بایتهای دریافتی است، پیش از JSON.parse */export function verifyNamayekSignature(rawBody, header, secret, now = Date.now()) { const parts = String(header || "").split(",").map((p) => p.trim().split("=")); const t = Number(parts.find(([k]) => k === "t")?.[1]); const signatures = parts.filter(([k, v]) => k === "v1" && /^[0-9a-f]{64}$/.test(v || "")).map(([, v]) => v); if (!Number.isInteger(t) || signatures.length === 0) return false; if (Math.abs(Math.floor(now / 1000) - t) > TOLERANCE_SECONDS) return false; // replay const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`, "utf8").digest(); return signatures.some((sig) => crypto.timingSafeEqual(expected, Buffer.from(sig, "hex")));} // Express: بدنه خام را نگه دارید// app.post("/hooks/namayek", express.raw({ type: "application/json" }), (req, res) => {// if (!verifyNamayekSignature(req.body.toString("utf8"), req.get("Namayek-Signature"), process.env.NAMAYEK_WEBHOOK_SECRET)) {// return res.sendStatus(400);// }// const event = JSON.parse(req.body.toString("utf8"));// res.sendStatus(200); // سریع پاسخ دهید؛ پردازش در صف// });<?phpconst NAMAYEK_TOLERANCE = 300; function verify_namayek_signature(string $rawBody, string $header, string $secret, ?int $now = null): bool { $now = $now ?? time(); $t = null; $signatures = []; foreach (explode(',', $header) as $part) { [$k, $v] = array_pad(explode('=', trim($part), 2), 2, ''); if ($k === 't' && ctype_digit($v)) { $t = (int) $v; } if ($k === 'v1' && preg_match('/^[0-9a-f]{64}$/', $v)) { $signatures[] = $v; } } if ($t === null || !$signatures) { return false; } if (abs($now - $t) > NAMAYEK_TOLERANCE) { return false; } // replay $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret); foreach ($signatures as $sig) { if (hash_equals($expected, $sig)) { return true; } } return false;} $raw = file_get_contents('php://input');if (!verify_namayek_signature($raw, $_SERVER['HTTP_NAMAYEK_SIGNATURE'] ?? '', getenv('NAMAYEK_WEBHOOK_SECRET'))) { http_response_code(400); exit;}$event = json_decode($raw, true);http_response_code(200);import hashlib, hmac, re, time TOLERANCE_SECONDS = 300 def verify_namayek_signature(raw_body: bytes, header: str, secret: str, now: float | None = None) -> bool: t, signatures = None, [] for part in (header or "").split(","): key, _, value = part.strip().partition("=") if key == "t" and value.isdigit(): t = int(value) elif key == "v1" and re.fullmatch(r"[0-9a-f]{64}", value): signatures.append(value) if t is None or not signatures: return False if abs(int(now if now is not None else time.time()) - t) > TOLERANCE_SECONDS: # replay return False expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() return any(hmac.compare_digest(expected, sig) for sig in signatures) # Flask:# @app.post("/hooks/namayek")# def hook():# if not verify_namayek_signature(request.get_data(), request.headers.get("Namayek-Signature", ""), os.environ["NAMAYEK_WEBHOOK_SECRET"]):# abort(400)# event = request.get_json()# return "", 200package namayek import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "regexp" "strconv" "strings" "time") const toleranceSeconds = 300 var sigRe = regexp.MustCompile(`^[0-9a-f]{64}$`) // VerifySignature: rawBody همان بایتهای دریافتی است، پیش از json.Unmarshal.func VerifySignature(rawBody []byte, header, secret string, now time.Time) bool { var t int64 = -1 var signatures []string for _, part := range strings.Split(header, ",") { kv := strings.SplitN(strings.TrimSpace(part), "=", 2) if len(kv) != 2 { continue } switch kv[0] { case "t": if v, err := strconv.ParseInt(kv[1], 10, 64); err == nil { t = v } case "v1": if sigRe.MatchString(kv[1]) { signatures = append(signatures, kv[1]) } } } if t < 0 || len(signatures) == 0 { return false } if d := now.Unix() - t; d > toleranceSeconds || d < -toleranceSeconds { // replay return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(strconv.FormatInt(t, 10) + ".")) mac.Write(rawBody) expected := []byte(hex.EncodeToString(mac.Sum(nil))) for _, sig := range signatures { if hmac.Equal(expected, []byte(sig)) { return true } } return false}تلاش دوباره
| پاسخ شما | نتیجه | توضیح |
|---|---|---|
| 2xx در مهلت | موفق | تحویل انجام شد؛ پاسخ را سریع بدهید و کار سنگین را در صف خودتان انجام دهید. |
| 3xx | ناموفق | تغییر مسیر هرگز دنبال نمیشود. |
| 410 Gone | پایان | تحویل متوقف و وبهوک غیرفعال میشود. |
| دیگر پاسخها یا مهلت | تلاش دوباره | طبق برنامه تلاش دوباره؛ پس از آخرین تلاش، تحویل «dead» میشود. |
تلاشهای بعدی با این فاصله از تلاش قبلی انجام میشوند (پیشفرض؛ تیم نمایک میتواند آن را تنظیم کند):
| تلاش | فاصله از تلاش قبلی |
|---|---|
| 2 | 1 دقیقه |
| 3 | 5 دقیقه |
| 4 | 30 دقیقه |
| 5 | 2 ساعت |
| 6 | 6 ساعت |
| 7 | 12 ساعت |
| 8 | 24 ساعت |
پس از چند شکست پیاپی وبهوک خودکار غیرفعال و به شما اطلاع داده میشود. پس از رفع مشکل آن را فعال کنید و تحویلهای ازدسترفته را از فهرست تحویلها دوباره بفرستید.
رویدادها
| رویداد | محصول | توضیح |
|---|---|---|
webhook.test | حساب | رویداد آزمایشی از دکمه «آزمایش» پنل یا POST …/test. |
wallet.charged | حساب | شارژ آنلاین کیف پول نشست؛ مبلغ، شناسه تراکنش و سهمی که همان لحظه بدهی را تسویه کرد. |
billing.low_balance | حساب | موجودی کیف پول از آستانه هشدار کمتر شد. |
billing.outstanding_created | حساب | موجودی برای هزینه مصرف کافی نبود و بدهی ثبت شد؛ همراه با پایان مهلت پیش از توقف. |
billing.suspended | حساب | سرویس به دلیل بدهی متوقف شد. |
billing.unsuspended | حساب | سرویس دوباره فعال شد. |
billing.statement_ready | حساب | صورتحساب ماه شمسی گذشته آماده شد؛ همراه با هزینه مصرف و بدهی پایان دوره. |
project.spend_cap_reached | حساب | سقف هزینه ماهانه پروژه پر شد. |
api_key.expiring | حساب | کلیدی بهزودی منقضی میشود. |
api_key.revoked | حساب | کلیدی باطل شد. |
backup.succeeded / failed / missed | پشتیبانگیری | نتیجه هر نوبت پشتیبانگیری. |
restore.completed / failed | پشتیبانگیری | نتیجه بازگردانی. |
agent.offline / online | پشتیبانگیری | قطع و وصل شدن عامل. |
نکتههای امنیتی
- فقط نشانی https عمومی پذیرفته میشود؛ نشانی شبکه داخلی، localhost و IP خصوصی رد میشوند و تغییر مسیر دنبال نمیشود.
- راز امضا را در متغیر محیطی نگه دارید و با چرخش (با دوره همپوشانی تا ۷۲ ساعت) عوض کنید.
- بدنه رویداد هرگز کلید، راز یا داده محرمانه ندارد؛ برای جزئیات بیشتر API را با کلید خودتان صدا بزنید.
- سریع پاسخ ۲xx بدهید و پردازش سنگین را در صف خودتان انجام دهید تا مهلت تمام نشود.
آخرین بهروزرسانی: