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

مستنداترویدادها

وب‌هوک

رویدادهای تحویل، برگشت و شکایت را همان لحظه به سرور خودتان بگیرید، با امضای HMAC.

آخرین ویرایش: ۲۱ آبان ۱۴۰۵

به‌جای اینکه وضعیت هر ایمیل را بپرسید، سندک خودش وقتی چیزی اتفاق افتاد به نشانی شما POST می‌کند. وب‌هوک‌ها را در پنل، بخش «وب‌هوک‌ها»، می‌سازید.

نکته: تعداد وب‌هوک‌ها به پلن بستگی دارد. پلن رایگان وب‌هوک ندارد؛ سقف هر پلن را در تعرفه ببینید.

نشانی

نشانی باید https و عمومی باشد، روی پورت 443 یا 8443، و نام کاربری و رمز نداشته باشد. نشانی‌هایی که به شبکه‌ی داخلی یا آدرس خصوصی اشاره می‌کنند رد می‌شوند و ریدایرکت دنبال نمی‌شود. این قاعده‌ها از سرور سندک در برابر سوءاستفاده محافظت می‌کنند.

ساخت و تغییر نشانی وب‌هوک نیاز به تأیید رمز عبور دارد.

رویدادها

هر وب‌هوک مشخص می‌کند کدام رویدادها را می‌خواهد:

رویدادمعنا
message.acceptedپیام پذیرفته و در صف قرار گرفت
message.deferredگیرنده موقتاً نپذیرفت؛ سندک دوباره امتحان می‌کند
message.deliveredسرور گیرنده ایمیل را پذیرفت
message.bouncedبرگشت خورد (نشانی وجود ندارد، رد شد، یا مهلت تمام شد)
message.complainedگیرنده آن را اسپم گزارش کرد
message.openedباز شد (فقط با ردیابی)
message.clickedروی پیوندی کلیک شد (فقط با ردیابی)
email.receivedنامه‌ای به یکی از صندوق‌های شما رسید

توجه: delivered یعنی سرور گیرنده ایمیل را پذیرفت، نه اینکه کاربر آن را در Inbox دید. ممکن است در اسپم قرار بگیرد.

درخواستی که می‌گیرید

هدرها
POST /your/webhook HTTP/1.1
Content-Type: application/json
User-Agent: Sendak-Webhooks/1
X-Sendak-Event-Id: 01JCX9…
X-Sendak-Event-Type: message.delivered
X-Sendak-Signature: t=1731400000,v1=5a7c…
بدنه
{
  "id": "01JCX9…",
  "type": "message.delivered",
  "created_at": "2026-11-12T12:04:03+00:00",
  "data": {
    "message_id": "01JCX8…",
    "from_domain": "shop.ir",
    "recipient": "ali@gmail.com",
    "status": "delivered",
    "response_code": 250,
    "tags": ["otp"],
    "metadata": { "order": "1201" }
  }
}

فیلدهای data بسته به رویداد کم یا زیاد می‌شوند. این‌ها همیشه هستند: message_id، from_domain، recipient، status. فیلدهای response_code، classification (دسته‌ی برگشت)، attempt، tags و metadata فقط وقتی مقدار دارند می‌آیند.

message_id همان شناسه‌ای است که هنگام ارسال در پاسخ API گرفتید.

بررسی امضا

هدر X-Sendak-Signature به شکل t=<زمان یونیکس>,v1=<امضا> است. امضا HMAC-SHA256 رشته‌ی <t>.<بدنه‌ی خام> با راز وب‌هوک است. امضا را روی بدنه‌ی خام حساب کنید، نه روی JSON دوباره‌ساخته‌شده.

Node.js
import crypto from 'node:crypto'

export function verify(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
  const t = Number(parts.t)

  // A request older than five minutes is refused, so a captured one cannot be replayed later.
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false

  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
  const given = Buffer.from(parts.v1 ?? '', 'hex')
  const wanted = Buffer.from(expected, 'hex')

  return given.length === wanted.length && crypto.timingSafeEqual(given, wanted)
}
PHP
function verifySendak(string $rawBody, string $header, string $secret, int $tolerance = 300): bool
{
    parse_str(str_replace(',', '&', $header), $parts);
    $t = (int) ($parts['t'] ?? 0);

    if ($t === 0 || abs(time() - $t) > $tolerance) {
        return false;
    }

    return hash_equals(hash_hmac('sha256', $t.'.'.$rawBody, $secret), (string) ($parts['v1'] ?? ''));
}
Python
import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t = int(parts.get("t", "0"))

    if t == 0 or abs(time.time() - t) > tolerance:
        return False

    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

پاسخ دادن

فقط پاسخ 2xx موفق است. سریع پاسخ بدهید (مثلاً رویداد را در صف خودتان بگذارید و 200 برگردانید) و کار سنگین را بعداً انجام دهید.

تلاش دوباره

اگر پاسخ 2xx نگیریم، تا هشت بار دوباره امتحان می‌کنیم. فاصله‌ها بعد از هر شکست:

تلاشفاصله تا بعدی
۱۱ دقیقه
۲۵ دقیقه
۳۳۰ دقیقه
۴۲ ساعت
۵۶ ساعت
۶۱۲ ساعت
۷۲۴ ساعت
۸۲۴ ساعت

در مجموع حدود سه روز. اگر هر هشت تلاش یک رویداد شکست بخورد، آن رویداد «ناموفق» ثبت می‌شود. بعد از ۱۰ رویداد ناموفق پشت‌سرهم، وب‌هوک خاموش می‌شود و ایمیل اخطار می‌گیرید؛ بعد از رفع مشکل باید دوباره روشنش کنید. یک پاسخ موفق شمارنده را صفر می‌کند.

در پنل، برای هر وب‌هوک فهرست تحویل‌ها با کد پاسخ هست و می‌توانید هر کدام را دستی دوباره بفرستید.

رویداد تکراری

ممکن است یک رویداد بیش از یک بار برسد (مثلاً پاسخ شما در راه گم شده باشد). X-Sendak-Event-Id در همه‌ی تلاش‌های همان رویداد ثابت است؛ با آن رویداد تکراری را کنار بگذارید. ترتیب رسیدن رویدادها هم تضمین نمی‌شود: مثلاً ممکن است delivered قبل از accepted برسد.

چرخاندن راز

اگر راز وب‌هوک لو رفت، «چرخش رمز امضا» را بزنید و راز تازه را همان لحظه در سرور خودتان بگذارید. تا آن لحظه، درخواست‌ها با راز قدیمی تأیید نمی‌شوند.

آزمایش

از پنل، «ارسال آزمایشی» یک رویداد نمونه با نوع webhook.test می‌فرستد تا نشانی و امضا را بدون ایمیل واقعی بررسی کنید. این رویداد فقط یک بار و همان لحظه فرستاده می‌شود و نتیجه‌اش (کد پاسخ) در پنل دیده می‌شود.

رویداد آزمایشی
{
  "id": "test",
  "type": "webhook.test",
  "created_at": "2026-11-12T12:00:00+03:30",
  "data": { "message": "This is a test from Sendak." }
}