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

# Replying to customers

> Answer end customers from your own panel with text, files and voice notes.

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