وبهوکها
وقتی پرداختها تغییر میکنند رویداد امضاشده بگیرید، امضا را دقیق بررسی کنید و تکرار و تلاش دوباره را مدیریت کنید.
نقطهٔ دریافت وبهوک نشانیای روی سرور شماست که بزن به کارت هر وقت اتفاقی در حسابتان بیفتد صدایش میزند: رسیدی میرسد، تیم شما تأییدش میکند، پرداختی موفق میشود. بهجای پرسیدن مداوم از وبهوک استفاده کنید تا سفارش حتی وقتی مشتری پیش از برگشتن صفحهٔ پرداخت را میبندد هم بهروز شود.
افزودن نقطهٔ دریافت
نقطههای دریافت را از توسعهدهندگان ← وبهوکها یا با POST /v1/webhook_endpoints اضافه کنید:
| پارامتر | توضیح |
|---|---|
url | الزامی. نقطههای دریافت واقعی باید https باشند؛ نقطههای آزمایشی میتوانند http باشند. |
enabled_events | الزامی. نوع رویدادها، یا ["*"] برای همه. |
description | برای خودتان. |
api_version | نسخهٔ شیءهای داخل رویدادها؛ پیشفرض نسخهٔ فعلی است. |
پاسخ، secret (whsec_…) را فقط یک بار دارد. آن را کنار بقیهٔ رازهایتان نگه دارید؛ مالک یا مدیر میتواند دوباره در داشبورد ببیندش. نقطههای آزمایشی و واقعی کلید جداگانه دارند.
curl https://a.bezanbecard.com/v1/webhook_endpoints \
-H "Authorization: Bearer sk_test_…" \
-H "Idempotency-Key: endpoint-shop-1" \
-H "Content-Type: application/json" \
-d '{"url":"https://shop.example/webhooks/bezanbecard","enabled_events":["payment.succeeded","payment.partially_paid","payment.overpaid"]}'نقطهٔ دریافتی که به نشانی خصوصی یا loopback میرسد در محیط اصلی رد میشود (webhook_destination_private) و پیش از هر تحویل دوباره بررسی میشود.
نوع رویدادها
| نوع | کِی |
|---|---|
payment.created | پرداختی ساخته شد (با API، داشبورد، لینک پرداخت یا افزونه). |
payment.processing | مشتری رسید فرستاد یا گفت پرداخت کرده است. |
payment.partially_paid | واریز رسیده ولی کمتر از مبلغ است. |
payment.requires_review | پرداخت به تصمیم یک نفر نیاز دارد. |
payment.succeeded | پرداخت کامل شد. |
payment.overpaid | بیشتر از مبلغ رسید، یا پول به پرداخت لغوشده رسید. |
payment.overpayment_resolved | برگشت پول، اعتبار یا نگه داشتن را ثبت کردید. |
payment.canceled | پرداخت لغو شد. |
payment.expired | صفحهٔ پرداخت بدون دریافت پول بسته شد. |
payment.updated | توضیح، metadata یا مشخصات مشتری تغییر کرد. |
receipt.created | رسیدی رسید، از صفحهٔ پرداخت، داشبورد شما یا API. |
receipt.approved | یکی از اعضا، تأیید خودکار یا API رسیدی را تأیید کرد. |
receipt.rejected | یکی از اعضا یا API رسیدی را رد کرد. |
deposit.created | واریزی پیدا یا ثبت شد. |
deposit.matched | واریزی به پرداخت وصل شد. واریز بانکیای که جای واریز یک رسید را گرفته، آن را در replaces نام میبرد: همان انتقال است، نه پول تازه. |
deposit.unmatched | واریزی جدا شد یا وصلنشده ماند. |
deposit.ignored | واریزی «پرداخت مشتری نیست» علامت خورد. |
payment_link.created، payment_link.updated | لینک پرداختی تغییر کرد. |
customer.created، customer.updated، customer.deleted | مشتریای تغییر کرد. |
device.online، device.offline، device.revoked | گوشی اپ دریافت پیامک (اختیاری) برگشت، یک ساعت خاموش ماند یا حذف شد. |
card.created، card.updated | کارت یا حساب مقصدی تغییر کرد. |
شیء رویداد
{
"id": "evt_TestVector000001",
"object": "event",
"type": "payment.succeeded",
"livemode": false,
"api_version": "2026-10-01",
"created": 1793000000,
"data": {
"object": { "id": "pay_TestVector000001", "object": "payment", "status": "succeeded", "amount_received": 12500470 },
"previous_attributes": { "status": "processing" }
},
"request": { "id": null, "idempotency_key": null }
}data.object همان شیئی است که API در لحظهٔ رویداد برمیگرداند. رویدادها هیچوقت شمارهٔ کارت یا شبا را ندارند و رویدادهای رسید فایلها را فقط با شناسه، اندازه و SHA-256 توصیف میکنند، هرگز با پیوندی به آنها. previous_attributes فیلدهای تغییرکرده را با مقدار قبلیشان نگه میدارد. request درخواست APIای را که رویداد را ساخته نام میبرد و برای تغییرهایی که هیچ درخواستی نساخته (پیامک بانک، انقضای زمانبندیشده) null است.
بررسی امضا
هر تحویل این هدرها را دارد:
Bezanbecard-Signature: t=1793000000,v1=0b7a0c3ee135084e8ffaa56e2ebe4f4ea6062ba462e8e92b3e705fda3089b0ef
User-Agent: Bezanbecard/1.0 (+https://bezanbecard.com/docs/webhooks)برای بررسی:
- بدنهٔ خام درخواست را پیش از هر تبدیل JSON بخوانید. JSON بازسازیشده جور درنمیآید.
- هدر را با
,بهtو یک یا چند مقدارv1جدا کنید. هدری کهtیا هیچv1ی ندارد رد میشود. - HMAC-SHA256 را روی
`${t}.${rawBody}`با کل رشتهٔ کلید نقطهٔ دریافت (همراهwhsec_) بهصورت هگز با حروف کوچک حساب کنید. - آن را در زمان ثابت با هر
v1مقایسه کنید؛ هر کدام جور باشد کافی است. هنگام تعویض کلید، هدر دو مقدارv1دارد. - اگر
tبیش از ۳۰۰ ثانیه با ساعت شما فاصله داشت، در هر دو جهت، رد کنید.
کتابخانهها همهٔ اینها را انجام میدهند:
import express from 'express';
import Bezanbecard from '@bezanbecard/node';
const app = express();
app.post('/webhooks/bezanbecard', express.raw({ type: 'application/json' }), (req, res) => {
let event;
try {
event = Bezanbecard.webhooks.constructEvent(req.body, req.headers['bezanbecard-signature'], process.env.BEZANBECARD_WEBHOOK_SECRET);
} catch (error) {
return res.status(400).send('Invalid signature');
}
if (event.type === 'payment.succeeded') fulfilOrder(event.data.object.reference);
res.json({ received: true });
});بدون کتابخانه
import crypto from 'node:crypto';
export function verifyBezanbecardSignature(rawBody, header, secret, tolerance = 300, now = Math.floor(Date.now() / 1000)) {
const pairs = String(header ?? '').split(',').map((part) => part.trim().split('='));
const t = pairs.find(([key]) => key === 't')?.[1];
const signatures = pairs.filter(([key]) => key === 'v1').map(([, value]) => value ?? '');
if (!t || !/^\d+$/.test(t) || signatures.length === 0) return false;
if (Math.abs(now - Number(t)) > tolerance) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return signatures.some((signature) => Buffer.byteLength(signature) === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)));
}بردارهای آزمون
بررسیکنندهٔ خود را با این مقدارها که همهٔ کتابخانههای رسمی مشترکاً به کار میبرند امتحان کنید. هر مورد را با now = 1793000100 بسنجید، مگر خلافش گفته شده باشد.
secret whsec_dGVzdF9zZWNyZXRfZm9yX3ZlY3RvcnNfMDEyMzQ1Njc4OQ
payload {"id":"evt_TestVector000001","object":"event","type":"payment.succeeded","livemode":false,"api_version":"2026-10-01","created":1793000000,"data":{"object":{"id":"pay_TestVector000001","object":"payment","amount":12500470,"amount_received":12500470,"currency":"irr","status":"succeeded"},"previous_attributes":{"status":"processing"}},"request":{"id":null,"idempotency_key":null}}| مورد | هدر | نتیجه |
|---|---|---|
| درست | t=1793000000,v1=0b7a0c3ee135084e8ffaa56e2ebe4f4ea6062ba462e8e92b3e705fda3089b0ef | معتبر |
دو v1 هنگام تعویض کلید، دومی درست است | t=1793000000,v1=6739d862bae1405a4d5c5cdfc7169d22c315366fc966c6eedb2c5ccbdd31b7fe,v1=0b7a0c3ee135084e8ffaa56e2ebe4f4ea6062ba462e8e92b3e705fda3089b0ef | معتبر |
| کلید اشتباه | t=1793000000,v1=24e6795b5099d83284613681e1ee80acf4de695f0e9eaedc9816af7bb29e1fd2 | نامعتبر |
بدنهٔ دستکاریشده ("amount":12500471) با هدر درست | t=1793000000,v1=0b7a0c3e…b0ef | نامعتبر |
خیلی قدیمی (now = 1793000301) | هدر درست | نامعتبر |
خیلی جلوتر از ساعت (now = 1792999699) | هدر درست | نامعتبر |
بدون v1 | t=1793000000 | نامعتبر |
بدون t | v1=0b7a0c3e…b0ef | نامعتبر |
| فقط طرح ناشناخته | t=1793000000,v0=0b7a0c3e…b0ef | نامعتبر |
| هگز با حروف بزرگ | t=1793000000,v1=0B7A0C3E…B0EF | نامعتبر |
پاسخ دادن و تلاش دوباره
ظرف ۱۰ ثانیه با هر کد 2xx پاسخ دهید و کارهای کند را بعد انجام دهید. هر پاسخ دیگری شکست است، ریدایرکت هم: بزن به کارت هیچوقت ریدایرکت را دنبال نمیکند.
تحویل ناموفق بعد از ۱ دقیقه، ۵ دقیقه، ۳۰ دقیقه، ۲ ساعت، ۵ ساعت و ۱۰ ساعت و بعد هر ۱۲ ساعت دوباره فرستاده میشود، تا ۳ روز بعد از اولین تلاش (روی هم ۱۱ تلاش). نقطهٔ دریافتی که ۳ روز پشت سر هم ناموفق باشد غیرفعال میشود و مالکها، مدیرها و توسعهدهندهها خبردار میشوند؛ بعد از رفع مشکل آن را در داشبورد دوباره فعال کنید.
هر تحویلی را میتوانید از صفحهٔ نقطهٔ دریافت دستی دوباره بفرستید و یک رویداد آزمایشی هم بفرستید که هیچوقت ذخیره نمیشود.
تکرار و ترتیب
تحویل دستکم یک بار و بدون ترتیب است. یک رویداد ممکن است دو بار برسد و payment.succeeded ممکن است پیش از payment.processing برسد. پس:
- شناسهٔ
idهر رویدادی را که پردازش میکنید ذخیره کنید و شناسههای تکراری را رد کنید. - وقتی ترتیب مهم است، شیء را دوباره بخوانید (
GET /v1/payments/:id) و بر اساس وضعیت فعلیاش عمل کنید، نه فقط بر اساس رویداد.
سرورتان مدتی خاموش بود؟ رویدادها ۳۰ روز نگه داشته میشوند: آنها را با GET / فهرست کنید.
تعویض کلید امضا
کلید نقطهٔ دریافت را از صفحهٔ آن در داشبورد تعویض کنید. کلید قبلی تا ۲۴ ساعت (به انتخاب شما؛ ۰ یعنی همین حالا قطع شود) هنوز امضا میکند و در این مدت هر تحویل دو امضای v1 دارد که امضای تازه اول است. کلید تازه را جایگزین کنید و بگذارید کلید قبلی منقضی شود.