Vardast

دریافت وبهوک‌های وردست

Developers
مسیر وبهوک: مشتری به وردست به سرور شما، با امضای HMAC

این صفحه برای کیست؟

این راهنما برای برنامه‌نویسی است که می‌خواهد لیدها و سفارش‌های وردست را روی سرورِ خودش — CRM، سایت، یا هر سیستمِ دیگری — تحویل بگیرد. در پایانِ این راهنما، هر بار مشتری در چت لید بگذارد یا سفارش ثبت کند، اطلاعاتش خودبه‌خود و امن به سرورِ شما می‌رسد.

تصویرِ کلی

وردست را مثلِ یک پیک تصور کنید: هر رویداد (لید یا سفارش) را داخلِ یک بسته می‌گذارد، رویش یک مُهرِ امنیتی (امضا) می‌زند، و به آدرسِ سرورِ شما می‌رساند. کارِ شما سه چیز است: بسته را بگیرید، مُهرش را وارسی کنید که جعلی نباشد، و یک «دریافت شد» به وردست برگردانید.

روالِ کار — در ۴ قدم

کلِ مسیری که باید طی کنید این است:

  1. در داشبورد: یک Webhook می‌سازید و آدرسِ سرورتان را وارد می‌کنید. وردست در عوض یک «کلیدِ امضا» به شما می‌دهد.
  2. روی سرورِ خودتان: یک آدرس (endpoint) می‌سازید که درخواست‌های POST را بپذیرد.
  3. به‌صورت خودکار: وردست یک پیامِ آزمایشی می‌فرستد؛ سرورِ شما باید جوابِ درست بدهد تا اتصال «فعال» شود.
  4. از این پس: هر لید و سفارش به‌صورت خودکار به سرورِ شما فرستاده می‌شود.

قدم ۱ — ساختِ Webhook در داشبورد

به این مسیر بروید: داشبورد ← گزارش‌ها ← تنظیمات گزارش ← تبِ Webhook. (این قابلیت روی پلنِ Standard و بالاتر فعال است.) سه چیز پر می‌کنید:

  • آدرس سرور: آدرسِ HTTPSی که می‌خواهید رویدادها به آن برسند — مثلِ https://api.shoma.com/vardast-webhook.
  • نام: یک نامِ دلخواه تا بعداً این اتصال را بشناسید (مثلِ «CRM اصلی»).
  • رویدادها: تیک بزنید کدام رویدادها برایتان فرستاده شوند (لید جدید، سفارش جدید).

بعد از ساخت، وردست فقط یک‌بار یک «کلیدِ امضا» (مثلِ whsec_…) نشان می‌دهد. همان‌جا کپی و در جای امن ذخیره‌اش کنید؛ دیگر نمایش داده نمی‌شود. این کلید را در قدمِ ۳ برای وارسیِ امضا لازم دارید.

قدم ۲ — ساختِ سرورِ گیرنده

روی سرورِ خودتان یک آدرس بسازید که درخواستِ POST را بپذیرد — همان آدرسی که در قدمِ ۱ وارد کردید. نمونهٔ کاملِ کد (Node.js، Python، PHP) در قدمِ ۳ آمده؛ همان یک قطعه کد هر سه کارِ لازم را انجام می‌دهد: وارسیِ امضا، پاسخ به پیامِ آزمایشی، و دریافتِ رویداد.

هر پارامتر چیست؟

وقتی وردست به سرورِ شما POST می‌زند، اطلاعات در دو جا می‌آید: یک «بسته» (بدنهٔ JSON، به‌نامِ envelope) و چند «هدر». معنیِ هر کدام:

داخلِ بسته (بدنهٔ JSON)

  • id — شناسهٔ یکتای این رویداد. اگر یک id را دو بار گرفتید یعنی تکراری است؛ بارِ دوم را نادیده بگیرید.
  • type — نوعِ رویداد: lead.created برای لید یا order.created برای سفارش.
  • channel_id — کدام کانالِ شما (اینستاگرام، تلگرام و …) این رویداد را ساخته است.
  • created_at — زمانِ وقوعِ رویداد.
  • data — خودِ اطلاعات: نام، شمارهٔ تماس، آدرس، اقلامِ سفارش و …

هدرها (کنارِ بسته)

  • X-Vardast-Event — نوعِ رویداد (همان type).
  • X-Vardast-Event-Id — همان id؛ برای تشخیصِ رویدادِ تکراری.
  • X-Vardast-Timestamp — زمانِ ارسال؛ در وارسیِ امضا استفاده می‌شود.
  • X-Vardast-Signature — مُهرِ امنیتی؛ با کلیدِ امضا ساخته شده. با آن مطمئن می‌شوید بسته اصیل و دست‌نخورده است.

نمونهٔ یک بسته:

{
  "id": "evt_8f14e45f0b2c4a91",
  "type": "lead.created",
  "api_version": "2026-06-01",
  "created_at": "2026-06-13T09:30:00Z",
  "channel_id": "a1b2c3d4-...",
  "data": {
    "full_name": "Ali Rezaei",
    "phone_number": "+989121234567",
    "username": "ali.rezaei",
    "platform": "IG",
    "contact_id": "...",
    "created_at": "2026-06-13T09:30:00Z"
  }
}

قدم ۳ — وارسیِ امضا (مهم‌ترین بخش)

چرا؟ تا مطمئن شوید بسته واقعاً از وردست آمده، نه از یک نفرِ دیگر که آدرسِ سرورتان را حدس زده. چطور؟ با کلیدِ امضا، خودتان روی «timestamp + متنِ خامِ بسته» یک امضا می‌سازید و با مقدارِ X-Vardast-Signature مقایسه می‌کنید؛ اگر یکی بود، اصیل است. سه نکته:

  • امضا را روی متنِ خامِ بسته حساب کنید، نه روی JSONی که برنامه‌تان دوباره ساخته — وگرنه نتیجه فرق می‌کند.
  • اگر X-Vardast-Timestamp بیش از ۵ دقیقه قدیمی بود، درخواست را رد کنید.
  • هنگامِ تعویضِ کلید، ممکن است دو امضا با کاما در هدر بیاید؛ اگر یکی‌شان خورد بپذیرید.

Node.js (Express)

const crypto = require('crypto');

function verifyVardast(rawBody, headers, secret) {
  const ts = headers['x-vardast-timestamp'];
  const sigHeader = headers['x-vardast-signature'];
  if (!ts || !sigHeader) return false;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // replay window

  const expected = 'v1=' + crypto
    .createHmac('sha256', secret)
    .update(`${ts}.${rawBody}`)
    .digest('hex');

  return sigHeader.split(',').some((sig) => {
    sig = sig.trim();
    return sig.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  });
}

// express.raw, NOT express.json
app.post('/vardast-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const rawBody = req.body.toString('utf8');
  if (!verifyVardast(rawBody, req.headers, process.env.VARDAST_WEBHOOK_SECRET)) {
    return res.status(401).send('invalid signature');
  }
  const event = JSON.parse(rawBody);
  if (event.type === 'webhook.verification') {
    return res.status(200).send(event.data.challenge);
  }
  // dedupe by event.id, then enqueue for async processing
  return res.status(200).send('ok');
});

Python (Flask)

import hmac, hashlib, time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = "whsec_..."  # from your environment

def verify(raw_body: bytes, headers) -> bool:
    ts = headers.get("X-Vardast-Timestamp")
    sig_header = headers.get("X-Vardast-Signature")
    if not ts or not sig_header:
        return False
    if abs(time.time() - int(ts)) > 300:        # replay window
        return False
    expected = "v1=" + hmac.new(
        SECRET.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return any(hmac.compare_digest(s.strip(), expected)
               for s in sig_header.split(","))

@app.post("/vardast-webhook")
def webhook():
    raw = request.get_data()        # raw bytes, NOT request.json
    if not verify(raw, request.headers):
        abort(401)
    event = request.get_json()
    if event["type"] == "webhook.verification":
        return event["data"]["challenge"], 200
    # dedupe by event["id"], then enqueue for async processing
    return "ok", 200

PHP

<?php
$secret = getenv('VARDAST_WEBHOOK_SECRET');
$raw    = file_get_contents('php://input');     // raw body
$ts     = $_SERVER['HTTP_X_VARDAST_TIMESTAMP'] ?? '';
$sigHdr = $_SERVER['HTTP_X_VARDAST_SIGNATURE'] ?? '';

if ($ts === '' || $sigHdr === '' || abs(time() - (int)$ts) > 300) {
    http_response_code(401); exit('invalid');
}
$expected = 'v1=' . hash_hmac('sha256', $ts . '.' . $raw, $secret);
$ok = false;
foreach (explode(',', $sigHdr) as $sig) {
    if (hash_equals(trim($sig), $expected)) { $ok = true; break; }
}
if (!$ok) { http_response_code(401); exit('invalid signature'); }

$event = json_decode($raw, true);
if ($event['type'] === 'webhook.verification') {
    http_response_code(200); echo $event['data']['challenge']; exit;
}
// dedupe by $event['id'], then enqueue for async processing
http_response_code(200); echo 'ok';

قدم ۴ — پاسخِ درست بدهید

  • پیامِ آزمایشی (فعال‌سازی): اگر type برابرِ webhook.verification بود، مقدارِ data.challenge را در بدنهٔ پاسخ برگردانید و کدِ 2xx بدهید. همین کار، اتصالِ شما را «فعال» می‌کند. تا وقتی این جوابِ درست داده نشود، هیچ رویدادِ واقعی‌ای نمی‌آید.
  • رویدادهای عادی: سریع — زیرِ ۱۰ ثانیه — یک کدِ 2xx بدهید. پردازشِ سنگین را بعد از پاسخ انجام دهید (در صف بگذارید).

بعد از اتصال — نکاتی که خوب است بدانید

  • تلاشِ مجدد: اگر سرورتان جواب نداد، وردست ۷ بار طی حدودِ ۳۳ ساعت دوباره می‌فرستد.
  • غیرفعال‌سازیِ خودکار: اگر ۳ روزِ پیاپی همهٔ تلاش‌ها شکست بخورد، اتصال خودکار غیرفعال و به شما اطلاع داده می‌شود؛ بعد از رفعِ مشکل، از داشبورد دوباره فعالش کنید.
  • چرخشِ کلید: اگر کلید لو رفت، از داشبورد «چرخشِ کلید» بزنید؛ کلیدِ قبلی ۲۴ ساعت معتبر می‌ماند تا فرصتِ به‌روزرسانیِ سرور داشته باشید.

چک‌لیستِ نهایی

  • در داشبورد یک Webhook ساختم و کلیدِ امضا را ذخیره کردم.
  • روی سرورم یک endpoint ساختم که POST می‌پذیرد.
  • امضا را روی هر درخواست و روی متنِ خام وارسی می‌کنم.
  • به پیامِ webhook.verification با برگرداندنِ data.challenge جواب می‌دهم.
  • زیرِ ۱۰ ثانیه 2xx می‌دهم و با id رویدادهای تکراری را حذف می‌کنم.