> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs-parschat.parstechai.com/documentation/reference/webhook-event-reference/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. > **Warning** > > **Pre-release `v1`, published 2026-09-14.** This page commits to the shape of the API, not to a > date. We will build exactly what is documented here. The shape can still change until > **2026-10-14**; after that, changes follow [Versioning and stability](/documentation/reference/versioning-stability). > > The banner comes off one page at a time as each route goes live. While it is here, build against > the contract and assume the route is not callable yet. > **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.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": { "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" }, "created_at": "2026-09-20T11:09:02Z" } } ``` #### Telegram ```json { "schema_version": "1.0.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" }, "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.0.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` | ### Triggered by a comment, with an attachment ```json { "schema_version": "1.0.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" }, "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.0.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" }, "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.0.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.0.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.0.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.0.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.0.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.0.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.0.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.0.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.0.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.0.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.0.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.0.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`. > Every event we send, with a full worked payload.