Skip to navigation

Webhook event reference

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.

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.

{
"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"
}
}
FieldNotes
roleclient, ai, or operator. sender is null for ai and operator
typetext · voice · image · video · multimedia · document · poll · form · template
sourceInstagram only: direct · comment · story_reply · story_mention · reel
external_idYour idempotency key
reply_to_idThe message_id this replies to, or null
markdown_contentRich rendering; text stays plain. Either may be null
form_dataPresent when type is form
senderThe 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

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

{
"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"
}
}

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

A form submission

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

{
"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" }
}

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.

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

{
"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"
}
}

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.

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 transcriptUnaffected. Keep it
Reading the conversation afterwardsStill works
The next conversation with that userStarts 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.

{
"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" }
}
}

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

{
"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" }
}
}

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.