> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs-parschat.parstechai.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs-parschat.parstechai.com/_mcp/server.

# رویدادهای وب‌هوک

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

> **Warning**
>
> **نسخه پیش‌انتشار `v1`، منتشرشده در 2026-09-14.** این صفحه به شکل API متعهد است،
> نه به تاریخ مشخصی. دقیقاً همان چیزی را می‌سازیم که اینجا مستند شده است. شکل API تا
> **2026-10-14** هنوز ممکن است تغییر کند؛ پس از آن، تغییرات از [نسخه‌بندی و پایداری](/documentation/reference/versioning-stability) پیروی می‌کنند.
>
> این بنر با راه‌اندازی هر مسیر، صفحه به صفحه برداشته می‌شود. تا وقتی اینجاست، بر اساس قرارداد
> پیاده‌سازی کنید و فرض کنید آن مسیر هنوز قابل فراخوانی نیست.

## تحویل \[#delivery]

```http
POST <your url>
Content-Type: application/json
X-ParsChat-Signature: <hex hmac-sha256 of raw body, keyed with your secret>
X-ParsChat-Event: message
X-ParsChat-Delivery: dlv_9fK2mQ
```

به محض اینکه رویداد را ذخیره کردید، `2xx` برگردانید و پردازش را بعد از آن انجام دهید. پردازشگر
کُند به timeout می‌انجامد، و timeout به تلاش دوباره.

## تأیید امضا \[#verify-the-signature]

**`Python`**

```python Python
import hashlib
import hmac

def is_authentic(raw_body: bytes, header_signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header_signature)
```

**`Node.js`**

```javascript Node.js
const crypto = require("crypto");

function isAuthentic(rawBody, headerSignature, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)           // a Buffer, not a parsed object
    .digest("hex");
  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(headerSignature ?? "", "utf8");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express: capture the raw body BEFORE express.json() parses it
// app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));
```

**`PHP`**

```php PHP
function is_authentic(string $rawBody, string $headerSignature, string $secret): bool
{
    $expected = hash_hmac('sha256', $rawBody, $secret);
    return hash_equals($expected, $headerSignature);
}

// $rawBody = file_get_contents('php://input');
```

> **Warning**
>
> **امضا را روی بایت‌های خام و پیش از تجزیه JSON تأیید کنید.** سریال‌سازی دوباره ترتیب کلیدها و
> فاصله‌ها را تغییر می‌دهد و امضا هرگز مطابقت نخواهد داشت. از `compare_digest` استفاده کنید، نه `==`.

## اگر اندپوینت شما احراز هویت خودش را لازم دارد \[#if-your-endpoint-needs-its-own-authentication]

برخی شرکا نمی‌توانند `POST` بدون احراز هویت بپذیرند. ممکن است درگاه یا WAF یک بانک، پیش از آنکه
درخواست اصلاً به برنامه برسد، توکن bearer یا کلید API بخواهد. امضای ما ثابت می‌کند محتوای رویداد
از طرف ما آمده است، اما قاعده زیرساخت شما را برآورده نمی‌کند.

می‌توانید مجموعه‌ای از هدرها به ما بدهید تا آن‌ها را عیناً به هر تحویل اضافه کنیم:

```json
{
  "Authorization": "Bearer <a token you issue to us>",
  "X-Your-Api-Key": "…",
  "X-Client-Id": "…"
}
```

یک توکن bearer ساده هم کار می‌کند. این هم فقط یک هدر `Authorization` است، مثل هر هدر دیگری.

|                     |                                                                                                       |
| ------------------- | ----------------------------------------------------------------------------------------------------- |
| چه کسی تنظیم می‌کند | **خودتان**، در پنل پارس‌چت، در تنظیمات پیشرفته اختیاری                                                |
| نحوه نگهداری        | رمزگذاری‌شده در حالت ذخیره، و هیچ اندپوینت خواندنی آن را برنمی‌گرداند                                 |
| تعویض               | خودتان در پنل تغییرش دهید؛ تحویل‌های در جریان از مقداری استفاده می‌کنند که هنگام ارسال معتبر بوده است |

آدرس وب‌هوک و این توکن هر دو در پنل شما هستند. فقط **آدرس بازگشت (return URL)** به دست ما می‌رسد،
چون همین آدرس تعیین می‌کند مرورگر یک مشتری واقعی در میانه ثبت‌نام به کجا فرستاده شود.

> **Warning**
>
> **چیزی که اصالت را ثابت می‌کند همچنان امضاست.** هدرهای شما درخواست را از دروازه خودتان عبور
> می‌دهند؛ `X-ParsChat-Signature` به شما می‌گوید بدنه واقعاً از ماست. **حتی وقتی احراز هویت خودتان
> موفق بود، امضا را تأیید کنید.** در غیر این صورت، کسی که توکن شما را به دست آورده می‌تواند
> رویدادهای جعلی برایتان بفرستد.

> **Info**
>
> **پشتیبانی نمی‌شود: mTLS.** اگر زیرساخت شما گواهی کلاینت را الزامی می‌کند، پیش از یکپارچه‌سازی به ما
> اطلاع دهید. این کار به جای تغییر تنظیمات، نیازمند پیاده‌سازی در سمت ما است.

### یک گیرنده کامل \[#a-complete-receiver]

```python
import hashlib
import hmac
import os

from fastapi import FastAPI, Header, Request, Response

app = FastAPI()
SECRET = os.environ["PARSCHAT_WEBHOOK_SECRET"]

@app.post("/parschat/events")
async def receive(
    request: Request,
    x_parschat_signature: str = Header(default=""),
    x_parschat_event: str = Header(default=""),
    x_parschat_delivery: str = Header(default=""),
) -> Response:
    raw = await request.body()

    expected = hmac.new(SECRET.encode(), raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, x_parschat_signature):
        return Response(status_code=401)

    # Store first, process later. A slow handler becomes a timeout,
    # and a timeout becomes a retry of an event you already have.
    enqueue(x_parschat_delivery, x_parschat_event, raw)
    return Response(status_code=200)
```

> **Info**
>
> **`X-ParsChat-Delivery` در تلاش‌های دوباره یک رویداد ثابت می‌ماند.** آن را ذخیره کنید و شناسه
> تحویلی را که قبلاً پردازش کرده‌اید نادیده بگیرید. این کار وقتی پاسخ `2xx` شما در مسیر گم شود و ما
> رویدادی را که پردازش کرده بودید دوباره بفرستیم، رفتار شما را درست نگه می‌دارد.

## یک رویداد آزمایشی برای خودتان بفرستید \[#send-yourself-a-test-event]

پیش از آنکه منتظر ترافیک واقعی بمانید، از ما بخواهید هر وقت خواستید رویدادی برایتان بفرستیم:

```bash
curl -X POST https://api-chat.parstechai.com/v1/webhooks/test \
  -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxx"
```

```json
{
  "delivery_id": "dlv_4mN6pL",
  "status": "pending"
}
```

**این یک تحویل واقعی است**، نه شبیه‌سازی: با secret شما امضا می‌شود، هدرهای سفارشی شما را دارد، و
با همان زمان‌بندی هر رویداد دیگری دوباره تلاش می‌شود. بنابراین تأیید امضا و قواعد فایروال شما را
می‌آزماید، نه فقط اینکه نام میزبان شما resolve می‌شود یا نه.

فیلد `data` آن `"test": true` دارد تا پردازشگر شما بتواند آن را کنار بگذارد و از روی آن گفتگو نسازد.

```python
if event["data"].get("test"):
    return 200
```

فراخوانی به محض صف‌شدن رویداد `202` برمی‌گرداند؛ این ثابت نمی‌کند اندپوینت شما آن را پذیرفته
است. `delivery_id` را در گزارش تحویل (پایین‌تر) جستجو کنید تا ببینید واقعاً چه پاسخی داده‌اید.

> **Note**
>
> پاسخ `422` یعنی هنوز هیچ آدرس وب‌هوکی برای حساب شما تنظیم نشده است. ابتدا آن را در پنل پارس‌چت
> تنظیم کنید.

## تلاش دوباره \[#retries]

هر پاسخی غیر از `2xx`، از جمله timeout، با فاصله‌گذاری فزاینده (backoff) دوباره تلاش می‌شود:
**۶ تلاش در حدود یک ساعت.**

| تلاش | پس از    |
| ---- | -------- |
| ۱    | فوراً    |
| ۲    | ۱۵ ثانیه |
| ۳    | ۱ دقیقه  |
| ۴    | ۵ دقیقه  |
| ۵    | ۱۵ دقیقه |
| ۶    | ۱ ساعت   |

پس از آن، تحویل به **صف نامه‌های مرده (dead-letter)** می‌رود و در پنل دیده می‌شود، تا به جای
کشف یک شکاف بی‌صدا، ببینید چه چیزی شکست خورده است.

> **Warning**
>
> **تلاش دوباره را تنها تور ایمنی خود ندانید.** پس از آخرین تلاش، رویداد به‌طور خودکار دوباره
> فرستاده نمی‌شود. برای پر کردن شکاف‌ها از [API مصرف](/documentation/guides/usage-reports) استفاده کنید.

## گزارش تحویل \[#the-delivery-log]

وقتی رویدادی که انتظارش را داشتید هرگز نرسید، این گزارش پاسخ می‌دهد: *آیا واقعاً آن را فرستادید؟*

```bash
curl "https://api-chat.parstechai.com/v1/webhooks/deliveries?status=failed" \
  -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxx"
```

```json
{
  "data": [
    {
      "delivery_id": "dlv_7bX4tK",
      "event": "chat",
      "status": "failed",
      "attempt_count": 3,
      "last_status_code": 502,
      "last_error": "upstream returned 502",
      "next_attempt_at": "2026-09-20T11:24:00Z",
      "delivered_at": null,
      "created_at": "2026-09-20T11:08:11Z"
    }
  ],
  "pagination": { "limit": 50, "offset": 0, "total_count": 1, "has_more": false }
}
```

`delivery_id` همان مقداری است که در بدنه رویداد می‌رسد، پس می‌توانید هر ردیف گزارش را با چیزی که
دریافت کرده‌اید یا نکرده‌اید تطبیق دهید.

| `status`        | معنا                                                               |
| --------------- | ------------------------------------------------------------------ |
| `pending`       | در صف است، یا منتظر تلاش دوباره بعدی                               |
| `delivered`     | اندپوینت شما `2xx` برگرداند                                        |
| `failed`        | یک تلاش شکست خورد. دوباره تلاش می‌شود؛ `next_attempt_at` را ببینید |
| `dead_lettered` | هر ۶ تلاش انجام شد. **دیگر تلاش نمی‌کنیم**                         |

هنگام اشکال‌زدایی، `last_status_code` و `last_error` جفت مفیدی هستند: یک `502` که
`next_attempt_at` آن مقدار دارد گذراست و خودبه‌خود درست می‌شود، اما شش `500` که به
`dead_lettered` ختم شده، شکافی است که خودتان باید پر کنید.

> **Note**
>
> **بدنه پیام‌ها در آن نیست.** این یک گزارش تحویل است، نه راه دومی برای خواندن گفتگوهایتان؛ گفتگوها
> تابع اندپوینت‌های [گفتگوها](/documentation/guides/replying-to-customers) و
> [مصرف](/documentation/guides/usage-reports) هستند که قواعد خودشان را اعمال می‌کنند.

## تنظیمات خود را بررسی کنید \[#check-your-configuration]

```bash
curl https://api-chat.parstechai.com/v1/webhooks \
  -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxx"
```

```json
{
  "url": "https://partner.example/parschat/events",
  "secret_set": true,
  "events": ["message", "chat", "reaction", "is_typing"],
  "updated_at": "2026-09-12T08:15:00Z"
}
```

فقط‌خواندنی است، پس می‌توانید تنظیمات خود را از داخل کد بررسی کنید بی‌آنکه بتوانید تغییرش دهید.
secret **هرگز برگردانده نمی‌شود**، فقط اینکه تنظیم شده یا نه.

## پوشش رویداد \[#the-envelope]

همه رویدادها شکل بیرونی یکسانی دارند:

```json
{
  "schema_version": "1.0.0",
  "event": "message",
  "delivery_id": "dlv_9fK2mQ",
  "occurred_at": "2026-09-20T11:09:02Z",
  "robot_id": "rbt_8fK2mQ",
  "external_id": "shop_10422",
  "channel_id": "chn_3pQ7xL",
  "data": { }
}
```

`schema_version` از semver پیروی می‌کند. **فیلدها بدون افزایش نسخه major اضافه می‌شوند**، پس
فیلدهایی را که نمی‌شناسید نادیده بگیرید. هیچ فیلدی بدون نسخه major جدید و اطلاع‌رسانی حذف یا
تغییرکاربری نمی‌شود.

## انواع رویداد \[#event-types]

| رویداد               | چه زمانی ارسال می‌شود                                                       |
| -------------------- | --------------------------------------------------------------------------- |
| `message`            | پیامی ارسال یا دریافت می‌شود، **از جمله پیوست‌ها**                          |
| `message.generating` | هوش مصنوعی نوشتن پاسخ را شروع کرده است. نشانگر «در حال نوشتن» را نمایش دهید |
| `chat`               | وضعیت یک گفتگو تغییر می‌کند                                                 |
| `reaction`           | واکنشی اضافه یا حذف می‌شود                                                  |
| `is_typing`          | یک **انسان** در طرف مقابل شروع به نوشتن می‌کند یا متوقف می‌شود              |

> **Info**
>
> `message.generating` و `is_typing` دو واقعیت متفاوت‌اند. اولی یعنی **هوش مصنوعی** در حال نوشتن است،
> دومی یعنی یک اپراتور **انسانی** در حال تایپ است. شاید بخواهید آن‌ها را متفاوت نمایش دهید.

محتوای کامل همه رویدادها: [مرجع رویدادهای وب‌هوک](/documentation/reference/webhook-event-reference).

## انواع پیام \[#message-types]

پیوست‌ها به صورت رویدادهای عادی `message` با `type` غیرمتنی می‌رسند، هرگز به صورت رویداد جداگانه:

`text` · `voice` · `image` · `video` · `multimedia` · `document` · `poll` · `form` · `template`

## چهار فیلدی که توضیح لازم دارند \[#four-fields-worth-explaining]

#### external\_id: کلید idempotency شما

اگر پیامی با `external_id` ای بفرستید که قبلاً استفاده کرده‌اید، پیام تکراری نمی‌سازیم.
از آن استفاده کنید تا تلاش دوباره بی‌خطر باشد.

#### reply\_to\_id: پاسخ‌های رشته‌ای

`message_id` پارس‌چتی پیامی که این پیام به آن پاسخ می‌دهد، تا بتوانید پیام نقل‌قول‌شده را نمایش دهید.
وقتی پیام پاسخ نیست، `null` است.

#### markdown\_content: قالب‌بندی غنی

وقتی محتوا قالب‌بندی دارد مقدار می‌گیرد. `text` همان نسخه ساده باقی می‌ماند، پس می‌توانید از هر دو
استفاده کنید: markdown در نمای گفتگوی غنی، و متن ساده در اعلان یا جایگزین پیامکی. هر کدام ممکن است
`null` باشد، پس هرگز فرض نکنید هر دو وجود دارند.

#### form\_data: پاسخ‌های ساختاریافته فرم

فهرستی از `{type, situation, key, value}`، که وقتی `type` برابر `form` است وجود دارد.

```json
{
  "type": "form",
  "text": null,
  "form_data": [
    { "type": "text", "situation": "question", "key": "Your name", "value": "Sara" },
    { "type": "single_select", "situation": "rate", "key": "Service rating", "value": "excellent" }
  ]
}
```

فرم‌ها فقط مخصوص اینستاگرام نیستند. مشتری‌ای که از طریق ابزارک (ویجت) وصل می‌شود همین شکل را
تولید می‌کند.