رفتن به محتوای اصلی

وب‌هوک‌ها

وقتی پرداخت‌ها تغییر می‌کنند رویداد امضاشده بگیرید، امضا را دقیق بررسی کنید و تکرار و تلاش دوباره را مدیریت کنید.

نقطهٔ دریافت وب‌هوک نشانی‌ای روی سرور شماست که بزن به کارت هر وقت اتفاقی در حسابتان بیفتد صدایش می‌زند: رسیدی می‌رسد، تیم شما تأییدش می‌کند، پرداختی موفق می‌شود. به‌جای پرسیدن مداوم از وب‌هوک استفاده کنید تا سفارش حتی وقتی مشتری پیش از برگشتن صفحهٔ پرداخت را می‌بندد هم به‌روز شود.

افزودن نقطهٔ دریافت

نقطه‌های دریافت را از توسعه‌دهندگان ← وب‌هوک‌ها یا با 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کارت یا حساب مقصدی تغییر کرد.

شیء رویداد

JSON
{
  "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 است.

بررسی امضا

هر تحویل این هدرها را دارد:

HTTP
Bezanbecard-Signature: t=1793000000,v1=0b7a0c3ee135084e8ffaa56e2ebe4f4ea6062ba462e8e92b3e705fda3089b0ef
User-Agent: Bezanbecard/1.0 (+https://bezanbecard.com/docs/webhooks)

برای بررسی:

  1. بدنهٔ خام درخواست را پیش از هر تبدیل JSON بخوانید. JSON بازسازی‌شده جور درنمی‌آید.
  2. هدر را با , به t و یک یا چند مقدار v1 جدا کنید. هدری که t یا هیچ v1ی ندارد رد می‌شود.
  3. HMAC-SHA256 را روی `${t}.${rawBody}` با کل رشتهٔ کلید نقطهٔ دریافت (همراه whsec_) به‌صورت هگز با حروف کوچک حساب کنید.
  4. آن را در زمان ثابت با هر v1 مقایسه کنید؛ هر کدام جور باشد کافی است. هنگام تعویض کلید، هدر دو مقدار v1 دارد.
  5. اگر 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 بسنجید، مگر خلافش گفته شده باشد.

Text
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)هدر درستنامعتبر
بدون v1t=1793000000نامعتبر
بدون tv1=0b7a0c3e…b0efنامعتبر
فقط طرح ناشناختهt=1793000000,v0=0b7a0c3e…b0efنامعتبر
هگز با حروف بزرگt=1793000000,v1=0B7A0C3E…B0EFنامعتبر

پاسخ دادن و تلاش دوباره

ظرف ۱۰ ثانیه با هر کد 2xx پاسخ دهید و کارهای کند را بعد انجام دهید. هر پاسخ دیگری شکست است، ریدایرکت هم: بزن به کارت هیچ‌وقت ریدایرکت را دنبال نمی‌کند.

تحویل ناموفق بعد از ۱ دقیقه، ۵ دقیقه، ۳۰ دقیقه، ۲ ساعت، ۵ ساعت و ۱۰ ساعت و بعد هر ۱۲ ساعت دوباره فرستاده می‌شود، تا ۳ روز بعد از اولین تلاش (روی هم ۱۱ تلاش). نقطهٔ دریافتی که ۳ روز پشت سر هم ناموفق باشد غیرفعال می‌شود و مالک‌ها، مدیرها و توسعه‌دهنده‌ها خبردار می‌شوند؛ بعد از رفع مشکل آن را در داشبورد دوباره فعال کنید.

هر تحویلی را می‌توانید از صفحهٔ نقطهٔ دریافت دستی دوباره بفرستید و یک رویداد آزمایشی هم بفرستید که هیچ‌وقت ذخیره نمی‌شود.

تکرار و ترتیب

تحویل دست‌کم یک بار و بدون ترتیب است. یک رویداد ممکن است دو بار برسد و payment.succeeded ممکن است پیش از payment.processing برسد. پس:

  • شناسهٔ id هر رویدادی را که پردازش می‌کنید ذخیره کنید و شناسه‌های تکراری را رد کنید.
  • وقتی ترتیب مهم است، شیء را دوباره بخوانید (GET /v1/payments/:id) و بر اساس وضعیت فعلی‌اش عمل کنید، نه فقط بر اساس رویداد.

سرورتان مدتی خاموش بود؟ رویدادها ۳۰ روز نگه داشته می‌شوند: آن‌ها را با GET /v1/events?type=payment.succeeded&created[gte]=… فهرست کنید.

تعویض کلید امضا

کلید نقطهٔ دریافت را از صفحهٔ آن در داشبورد تعویض کنید. کلید قبلی تا ۲۴ ساعت (به انتخاب شما؛ ۰ یعنی همین حالا قطع شود) هنوز امضا می‌کند و در این مدت هر تحویل دو امضای v1 دارد که امضای تازه اول است. کلید تازه را جایگزین کنید و بگذارید کلید قبلی منقضی شود.