> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs-parschat.parstechai.com/documentation/guides/replying-to-customers/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs-parschat.parstechai.com/_mcp/server. # Replying to customers > Answer end customers from your own panel with text, files and voice notes. > **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. Your operators answer end customers from **your** panel, not ours. A conversation arrives on your webhook, your agent types a reply, and you post it back through this API. The message reaches the customer on whichever channel the conversation belongs to, so the same call covers Instagram and Telegram. ## Send a text reply Every event carries a `conversation_id`. That is all you need to reply. **`cURL`** ```bash cURL curl -X POST https://api-chat.parstechai.com/v1/conversations/cnv_4dR8nW/messages \ -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "text": "Yes, we deliver on Fridays. Shall I reserve one for you?", "external_id": "op_reply_5521" }' ``` **`Python`** ```python Python message = requests.post( f"{BASE}/conversations/{conversation_id}/messages", headers={"Authorization": f"Bearer {API_KEY}"}, json={ "text": "Yes, we deliver on Fridays. Shall I reserve one for you?", "external_id": "op_reply_5521", }, timeout=10, ).json() ``` **`Node.js`** ```javascript Node.js const res = await fetch( `${BASE}/conversations/${conversationId}/messages`, { method: "POST", headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ text: "Yes, we deliver on Fridays. Shall I reserve one for you?", external_id: "op_reply_5521", }), }, ); const message = await res.json(); ``` **`Response`** ```json Response { "conversation_id": "cnv_4dR8nW", "message_id": "msg_9wY7zA", "external_id": "op_reply_5521", "role": "operator", "type": "text", "text": "Yes, we deliver on Fridays. Shall I reserve one for you?", "attachments": [], "status": "sent", "created_at": "2026-09-20T11:33:05Z" } ``` > **Info** > > `external_id` is your idempotency key. Send the same one twice and you get the original message > back rather than a duplicate, so retrying after a timeout is safe. A `status` of `queued` means the channel is rate limited and we will deliver the message when capacity frees. Nothing is discarded. ## Send a file or a voice note Two steps: upload the file, then send a message referencing it. #### Upload the file `POST /v1/attachments` as `multipart/form-data`. You get back an `id`. #### Send the message Pass that id as `attachment_ids`. Text is optional when you send a file. With several files, the text is sent once, as the caption of the first. It is two steps rather than one so that a large upload failing mid-transfer can be retried on its own, without re-sending the message. > **Note** > > **`conversation_id` is required at upload.** An attachment belongs to the channel it will be sent > on, so we need the destination before the file can be stored. One upload serves one conversation > — to send the same file to two conversations, upload it twice. ### 1. Upload **`cURL`** ```bash cURL curl -X POST https://api-chat.parstechai.com/v1/attachments \ -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxx" \ -F "conversation_id=cnv_4dR8nW" \ -F "file=@product-photo.jpg" ``` **`Python`** ```python Python with open("product-photo.jpg", "rb") as fh: attachment = requests.post( f"{BASE}/attachments", headers={"Authorization": f"Bearer {API_KEY}"}, data={"conversation_id": "cnv_4dR8nW"}, files={"file": fh}, timeout=60, ).json() ``` **`Response`** ```json Response { "id": "att_5kR2nP", "identifier": "9f2c41b8e7d4", "name": "product-photo.jpg", "url": "https://cdn.parstechai.com/a/9f2c41b8e7d4.jpg", "type": "image" } ``` ### 2. Send it ```bash curl -X POST https://api-chat.parstechai.com/v1/conversations/cnv_4dR8nW/messages \ -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "attachment_ids": ["att_5kR2nP"], "external_id": "op_photo_77" }' ``` **`Response`** ```json Response { "conversation_id": "cnv_4dR8nW", "message_id": "msg_1xZ8bC", "external_id": "op_photo_77", "role": "operator", "type": "image", "text": "", "attachments": [], "status": "sent", "created_at": "2026-09-20T11:35:41Z" } ``` > **Note** > > **`attachments` comes back empty on the send response**, even when the message carried one. Use > the `id` you got from the upload rather than expecting it echoed here. The file is on the message > — the `message` webhook event delivers it with its `attachments` populated. Size is a **plan dimension**, checked per file at upload: | Kind | Basic | Professional | Accepted types | | ----------------- | ----: | -----------: | ---------------------------------------------------------------------------------------------- | | **Image** | 2 MB | **4 MB** | `image/jpeg` · `image/png` | | **Video** | 10 MB | **20 MB** | `video/mp4` · `video/quicktime` (.mov) · `video/webm` · `video/ogg` · `video/x-msvideo` (.avi) | | **Voice / audio** | 5 MB | **10 MB** | `audio/aac` · `audio/mp4` · `audio/x-m4a` · `audio/wav` | | **Document** | 5 MB | **10 MB** | `application/pdf` | The usual alternative spellings are accepted too (`image/jpg`, `video/avi`, `audio/m4a`, `audio/wave`, `audio/x-wav`, `audio/vnd.wave`). This is exactly what Instagram delivers: **GIF, WebP and other formats are refused at upload**, rather than accepted and then failing when you send. Every plan with API access sends all four types — the difference between tiers is size, not which kinds of file you may send. If your agreement sets different caps, those apply instead. A file type outside this list, or a file over your plan's cap, is rejected with `422 validation_failed` before it is stored; for the size cap, `details` carries `kind`, `cap_mb` and `size_bytes`. A kind of file your plan does not include is `403 attachment_not_in_plan`. Unsent uploads are discarded after 24 hours. Full limits, and how they relate to Instagram's own, are on [Rate limits](/documentation/guides/rate-limits#files-voice-and-documents). > **Info** > > A voice note is just an attachment with `type: voice`. Upload the audio file the same way; we > infer the type from the media type, or you can pass `type=voice` explicitly. ## Show a typing indicator Call this when your agent starts typing, so the customer sees the same cue they would in any chat app. ```bash curl -X POST https://api-chat.parstechai.com/v1/conversations/cnv_4dR8nW/typing \ -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "is_typing": true }' ``` The indicator expires by itself after a few seconds. Send it again while the agent keeps typing rather than sending a stop. ## Build an operator queue Filter conversations by status to find the ones waiting for a human. ```bash curl "https://api-chat.parstechai.com/v1/conversations?status=operator_attention" \ -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxx" ``` **`Response`** ```json Response { "data": [ { "id": "cnv_4dR8nW", "robot_id": "rbt_8fK2mQ", "channel_id": "chn_3pQ7xL", "status": "operator_attention", "client": { "id": "iguser_88213", "username": "sara.k" }, "last_message_at": "2026-09-20T11:20:44Z", "unread_count": 2, "created_at": "2026-09-20T11:08:59Z" } ], "pagination": { "limit": 50, "offset": 0, "total_count": 1, "has_more": false } } ``` `unread_count` is the number of customer messages since the last reply from an operator, the AI or an agent: what is still waiting for an answer. You do not have to poll this. The `chat` webhook event fires the moment a conversation enters `operator_attention`, so use the event to update your queue and this endpoint to rebuild it after a restart. ## Close a conversation ```bash curl -X POST https://api-chat.parstechai.com/v1/conversations/cnv_4dR8nW/close \ -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxx" ``` > **Warning** > > Closing is a context boundary, not a deletion. The transcript stays and remains readable. What > changes is that the next conversation with that customer starts without the earlier history as > AI context. See [`context_reset`](/documentation/reference/webhook-event-reference). > Answer end customers from your own panel with text, files and voice notes.