> 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.

# Webhook event reference

> Every event we send, with a full worked payload.

> **Info**
>
> Every event below is also in the **API Reference**, under **Webhook events**, where each variant
> is selectable from a dropdown next to the payload.

Delivery, signature verification and retries: [Webhook events](/documentation/guides/webhook-events).

Every event shares the same envelope; only `data` differs.

## `message`

The envelope is the same on every channel. What differs is inside `data`: Instagram carries a
`source` and an Instagram `sender`, Telegram carries a Telegram user id and no `source`.

#### Instagram DM

```json
{
  "schema_version": "1.1.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": {
    "conversation_id": "cnv_4dR8nW",
    "message_id": "msg_1aB2cD",
    "external_id": "cli_msg_1",
    "source": "direct",
    "role": "client",
    "type": "text",
    "text": "Hi, do you have these roses in stock?",
    "markdown_content": null,
    "reply_to_id": null,
    "form_data": null,
    "attachments": [],
    "is_edited": false,
    "sender": {
      "id": "iguser_88213",
      "username": "sara.k",
      "name": "Sara Karimi",
      "avatar_url": "https://api-chat.parstechai.com/v1/files/avatars/gAAAAABpK3xQ8bT2vY0mN4cR7wE1sH9dL6kJ5fG3aZ2qX8uV"
    },
    "created_at": "2026-09-20T11:09:02Z"
  }
}
```

#### Telegram

```json
{
  "schema_version": "1.1.0",
  "event": "message",
  "delivery_id": "dlv_4nP8sT",
  "occurred_at": "2026-09-20T11:11:30Z",
  "robot_id": "rbt_8fK2mQ",
  "external_id": "shop_10422",
  "channel_id": "chn_7bT4kM",
  "data": {
    "conversation_id": "cnv_9xR3mK",
    "message_id": "msg_7hJ8kL",
    "external_id": "cli_msg_2",
    "role": "client",
    "type": "text",
    "text": "Do you deliver on Fridays?",
    "markdown_content": null,
    "reply_to_id": null,
    "form_data": null,
    "attachments": [],
    "is_edited": false,
    "sender": {
      "id": "tg_5512340",
      "username": "sara_k",
      "name": "Sara Karimi",
      "avatar_url": null
    },
    "created_at": "2026-09-20T11:11:30Z"
  }
}
```

`source` is absent on Telegram. It describes an Instagram entry point, so treat a missing
`source` as "not applicable to this channel" rather than a gap.

#### AI reply

```json
{
  "schema_version": "1.1.0",
  "event": "message",
  "delivery_id": "dlv_6kM2nQ",
  "occurred_at": "2026-09-20T11:09:07Z",
  "robot_id": "rbt_8fK2mQ",
  "external_id": "shop_10422",
  "channel_id": "chn_3pQ7xL",
  "data": {
    "conversation_id": "cnv_4dR8nW",
    "message_id": "msg_3cE4fG",
    "external_id": null,
    "source": "direct",
    "role": "ai",
    "type": "text",
    "text": "Yes, Dutch roses are in stock today.",
    "markdown_content": "Yes, **Dutch roses** are in stock today.",
    "reply_to_id": "msg_1aB2cD",
    "form_data": null,
    "attachments": [],
    "is_edited": false,
    "sender": null,
    "created_at": "2026-09-20T11:09:07Z"
  }
}
```

An AI or operator reply carries `sender: null`. `reply_to_id` points at the message it answers.

| Field              | Notes                                                                                                                         |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| `role`             | `client`, `ai`, or `operator`. `sender` is `null` for `ai` and `operator`                                                     |
| `type`             | `text` · `voice` · `image` · `video` · `multimedia` · `document` · `poll` · `form` · `template`                               |
| `source`           | Instagram only: `direct` · `comment` · `story_reply` · `story_mention` · `reel`                                               |
| `external_id`      | **Your idempotency key**                                                                                                      |
| `reply_to_id`      | The `message_id` this replies to, or `null`                                                                                   |
| `markdown_content` | Rich rendering; `text` stays plain. Either may be `null`                                                                      |
| `form_data`        | Present when `type` is `form`                                                                                                 |
| `sender`           | The end customer: `id`, `username`, `name` and `avatar_url`. Any of the last three is `null` when the channel did not tell us |

### Who sent it

`sender` is how you show a customer as a person rather than an id: their display `name` and
profile picture (`avatar_url`) arrive on every customer message, kept current by the channel.

`avatar_url` is a ParsChat link, not the channel's. It does not expire and always serves the
customer's current picture, so you can store it and use it directly as an `<img src>`. It is `null`
when we have no picture for that customer.

On Instagram, a customer who has only ever commented may arrive with `username` alone. Their first
direct message fills in `name` and `avatar_url`.

### Triggered by a comment, with an attachment

```json
{
  "schema_version": "1.1.0",
  "event": "message",
  "delivery_id": "dlv_2xK8pL",
  "occurred_at": "2026-09-20T11:14:20Z",
  "robot_id": "rbt_8fK2mQ",
  "external_id": "shop_10422",
  "channel_id": "chn_3pQ7xL",
  "data": {
    "conversation_id": "cnv_7mN2qS",
    "message_id": "msg_5eF6gH",
    "source": "comment",
    "media_id": "17895695668004550",
    "role": "client",
    "type": "image",
    "text": "",
    "attachments": [
      { "type": "image", "url": "https://cdn.parstechai.com/m/9fK2mQ.jpg", "mime": "image/jpeg" }
    ],
    "sender": {
      "id": "iguser_44190",
      "username": "ali.m",
      "name": "Ali Mohammadi",
      "avatar_url": "https://api-chat.parstechai.com/v1/files/avatars/gAAAAABpK3xR2mH7pL4nW9eT1vB6cY3sK8jD5fQ0aU2gZ7rX"
    },
    "created_at": "2026-09-20T11:14:20Z"
  }
}
```

`media_id` appears only when the event originated from a post or reel.

### A voice message

Attachments arrive as ordinary `message` events with a non-text `type`, never as a separate event.
`text` is empty and the payload is in `attachments`.

```json
{
  "schema_version": "1.1.0",
  "event": "message",
  "delivery_id": "dlv_6vB3nK",
  "occurred_at": "2026-09-20T11:31:12Z",
  "robot_id": "rbt_8fK2mQ",
  "external_id": "shop_10422",
  "channel_id": "chn_3pQ7xL",
  "data": {
    "conversation_id": "cnv_4dR8nW",
    "message_id": "msg_2vC5xN",
    "source": "direct",
    "role": "client",
    "type": "voice",
    "text": "",
    "attachments": [
      {
        "type": "voice",
        "url": "https://cdn.parstechai.com/m/2vC5xN.ogg",
        "mime": "audio/ogg",
        "duration_seconds": 14
      }
    ],
    "sender": {
      "id": "iguser_88213",
      "username": "sara.k",
      "name": "Sara Karimi",
      "avatar_url": "https://api-chat.parstechai.com/v1/files/avatars/gAAAAABpK3xQ8bT2vY0mN4cR7wE1sH9dL6kJ5fG3aZ2qX8uV"
    },
    "created_at": "2026-09-20T11:31:12Z"
  }
}
```

> **Warning**
>
> Attachment URLs are time-limited. Download the file when the event arrives rather than storing
> the URL and fetching it later.

### A form submission

```json
{
  "schema_version": "1.1.0",
  "event": "message",
  "delivery_id": "dlv_4jH7sV",
  "occurred_at": "2026-09-20T11:40:00Z",
  "robot_id": "rbt_8fK2mQ",
  "external_id": "shop_10422",
  "channel_id": "chn_3pQ7xL",
  "data": {
    "conversation_id": "cnv_4dR8nW",
    "message_id": "msg_8kP1rT",
    "role": "client",
    "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" }
    ],
    "attachments": [],
    "created_at": "2026-09-20T11:40:00Z"
  }
}
```

---

## `message.generating`

The AI has started composing a reply. **Show a typing indicator.**

```json
{
  "schema_version": "1.1.0",
  "event": "message.generating",
  "delivery_id": "dlv_5kL3nP",
  "occurred_at": "2026-09-20T11:09:03Z",
  "robot_id": "rbt_8fK2mQ",
  "external_id": "shop_10422",
  "channel_id": "chn_3pQ7xL",
  "data": { "conversation_id": "cnv_4dR8nW" }
}
```

> **Info**
>
> Hide the indicator when the matching `message` arrives. **There is no explicit "stopped" event.**
> A generation that fails produces no message, so treat it as a timeout after a few seconds.

---

## `chat`

A conversation changed state. `status` is one of `answered`, `operator_attention`,
`not_answered`, `closed`, or `pending`, and `previous_status` is what it was before.

Each one means something different for your side, so handle them separately rather than storing
the string.

#### answered

The AI or an operator replied. Nothing is waiting on anyone.

```json
{
  "schema_version": "1.1.0",
  "event": "chat",
  "delivery_id": "dlv_6hJ4kM",
  "occurred_at": "2026-09-20T11:09:08Z",
  "robot_id": "rbt_8fK2mQ",
  "external_id": "shop_10422",
  "channel_id": "chn_3pQ7xL",
  "data": {
    "conversation_id": "cnv_4dR8nW",
    "status": "answered",
    "previous_status": "not_answered",
    "changed_at": "2026-09-20T11:09:08Z"
  }
}
```

#### operator_attention

A human is needed. This is the event to raise an alert on: put the conversation in a queue, notify
your support team, or light up a badge in your panel.

```json
{
  "schema_version": "1.1.0",
  "event": "chat",
  "delivery_id": "dlv_7kL5nQ",
  "occurred_at": "2026-09-20T11:20:44Z",
  "robot_id": "rbt_8fK2mQ",
  "external_id": "shop_10422",
  "channel_id": "chn_3pQ7xL",
  "data": {
    "conversation_id": "cnv_4dR8nW",
    "status": "operator_attention",
    "previous_status": "answered",
    "changed_at": "2026-09-20T11:20:44Z"
  }
}
```

#### not_answered

A customer message is waiting and nothing has replied yet. Normal for a few seconds; if it
persists, something is wrong.

```json
{
  "schema_version": "1.1.0",
  "event": "chat",
  "delivery_id": "dlv_2mN6pR",
  "occurred_at": "2026-09-20T11:09:02Z",
  "robot_id": "rbt_8fK2mQ",
  "external_id": "shop_10422",
  "channel_id": "chn_3pQ7xL",
  "data": {
    "conversation_id": "cnv_4dR8nW",
    "status": "not_answered",
    "previous_status": "pending",
    "changed_at": "2026-09-20T11:09:02Z"
  }
}
```

#### pending

The conversation exists but is not yet active. You will usually see this as a `previous_status`
rather than on its own.

```json
{
  "schema_version": "1.1.0",
  "event": "chat",
  "delivery_id": "dlv_9pQ3sT",
  "occurred_at": "2026-09-20T11:08:59Z",
  "robot_id": "rbt_8fK2mQ",
  "external_id": "shop_10422",
  "channel_id": "chn_3pQ7xL",
  "data": {
    "conversation_id": "cnv_4dR8nW",
    "status": "pending",
    "previous_status": null,
    "changed_at": "2026-09-20T11:08:59Z"
  }
}
```

#### closed

The conversation ended. This one carries two extra fields, `context_reset` and `message_count`.
Read the section below before you act on it.

```json
{
  "schema_version": "1.1.0",
  "event": "chat",
  "delivery_id": "dlv_8mN3qR",
  "occurred_at": "2026-09-20T12:02:11Z",
  "robot_id": "rbt_8fK2mQ",
  "external_id": "shop_10422",
  "channel_id": "chn_3pQ7xL",
  "data": {
    "conversation_id": "cnv_4dR8nW",
    "status": "closed",
    "previous_status": "answered",
    "context_reset": true,
    "message_count": 14,
    "changed_at": "2026-09-20T12:02:11Z"
  }
}
```

### Closing starts a fresh context

When a chat closes, the prior history stops being sent as context to the AI. A later conversation
with the same end user starts clean.

> **Warning**
>
> **`context_reset` is not a deletion flag.** Nothing is deleted. The messages remain, they simply
> stop being replayed into the next conversation's context. A partner who treated this as a
> deletion instruction would discard a transcript we still hold.

|                                      |                              |
| ------------------------------------ | ---------------------------- |
| Your stored transcript               | Unaffected. Keep it          |
| Reading the conversation afterwards  | Still works                  |
| The next conversation with that user | Starts with no prior context |

---

## `reaction`

Someone added or removed a reaction on a message. `action` tells you which, so the same handler
covers both: apply it on `added`, and undo it on `removed`.

#### added

```json
{
  "schema_version": "1.1.0",
  "event": "reaction",
  "delivery_id": "dlv_3pQ9rT",
  "occurred_at": "2026-09-20T11:22:05Z",
  "robot_id": "rbt_8fK2mQ",
  "external_id": "shop_10422",
  "channel_id": "chn_3pQ7xL",
  "data": {
    "conversation_id": "cnv_4dR8nW",
    "message_id": "msg_1aB2cD",
    "action": "added",
    "emoji": "❤️",
    "by": { "id": "iguser_88213", "username": "sara.k" }
  }
}
```

#### removed

```json
{
  "schema_version": "1.1.0",
  "event": "reaction",
  "delivery_id": "dlv_5rT1vW",
  "occurred_at": "2026-09-20T11:23:40Z",
  "robot_id": "rbt_8fK2mQ",
  "external_id": "shop_10422",
  "channel_id": "chn_3pQ7xL",
  "data": {
    "conversation_id": "cnv_4dR8nW",
    "message_id": "msg_1aB2cD",
    "action": "removed",
    "emoji": "❤️",
    "by": { "id": "iguser_88213", "username": "sara.k" }
  }
}
```

The payload is identical apart from `action`, so match on the pair
(`message_id`, `emoji`, `by.id`) to find the reaction you stored.

---

## `is_typing`

A **human** started or stopped typing. This is distinct from `message.generating`, which is the
AI. You may want to render them differently: "Sara is typing" against "generating a reply".

#### started

```json
{
  "schema_version": "1.1.0",
  "event": "is_typing",
  "delivery_id": "dlv_7rS2tU",
  "occurred_at": "2026-09-20T11:25:10Z",
  "robot_id": "rbt_8fK2mQ",
  "external_id": "shop_10422",
  "channel_id": "chn_3pQ7xL",
  "data": {
    "conversation_id": "cnv_4dR8nW",
    "is_typing": true,
    "by": { "id": "iguser_88213", "username": "sara.k", "role": "client" }
  }
}
```

#### stopped

```json
{
  "schema_version": "1.1.0",
  "event": "is_typing",
  "delivery_id": "dlv_8sT4uV",
  "occurred_at": "2026-09-20T11:25:18Z",
  "robot_id": "rbt_8fK2mQ",
  "external_id": "shop_10422",
  "channel_id": "chn_3pQ7xL",
  "data": {
    "conversation_id": "cnv_4dR8nW",
    "is_typing": false,
    "by": { "id": "iguser_88213", "username": "sara.k", "role": "client" }
  }
}
```

#### operator typing

`by.role` says who it is. An operator typing is worth showing to the end customer; a client typing
is worth showing to the operator.

```json
{
  "schema_version": "1.1.0",
  "event": "is_typing",
  "delivery_id": "dlv_9uV5wX",
  "occurred_at": "2026-09-20T11:26:02Z",
  "robot_id": "rbt_8fK2mQ",
  "external_id": "shop_10422",
  "channel_id": "chn_3pQ7xL",
  "data": {
    "conversation_id": "cnv_4dR8nW",
    "is_typing": true,
    "by": { "id": "opr_4412", "username": "reza.h", "role": "operator" }
  }
}
```

> **Info**
>
> A `stopped` event is not guaranteed. A connection can drop mid-typing, so expire the indicator
> yourself after a few seconds rather than waiting for `is_typing: false`.