Conversations, messages and attachments
The list of conversations, one conversation and closing it; reading messages, replying with a file and markup, idempotency; attachments by key and signed links.
On this page
The list of conversations #
| Field | Type | Req. | Description |
|---|---|---|---|
| status | string | no | open, pending, resolved or closed |
| since | RFC 3339 | no | only the ones changed after this time |
| cursor | RFC 3339 | no | continuation of the previous page |
| limit | int | no | 1…100, 25 by default |
curl -s "https://api.widgetchat.ru/api/v1/conversations?status=open&since=2026-08-22T00:00:00Z" -H "Authorization: Bearer wck_…"{"data": [ /* conversation objects */ ], "nextCursor": "…", "hasMore": true}One conversation #
The conversation id is a UUID. Another site's conversation answers 404 rather than 403: the existence of someone else's ids is information too.
The conversation object #
| Field | Type | Req. | Description |
|---|---|---|---|
| id | uuid | yes | conversation id |
| status | string | yes | open, pending, resolved, closed |
| channel | string | yes | always app: the message comes from the widget |
| visitor.id | string | yes | the visitor's permanent id |
| visitor.name | string | no | the name, if the site passed it |
| visitor.email | string | no | the email, if the site passed it |
| visitor.organization | string | no | the organization from the signed token |
| visitor.accountId | string | no | your customer id from the signed token |
| visitor.role | string | no | the visitor's role from the signed token |
| source | string | no | where the conversation was started: widget for the chat window on the site. The widget does not send the address of the page the visitor wrote from |
| createdAt | RFC 3339 | yes | when it was created |
| lastMessageAt | RFC 3339|null | no | time of the last message |
| lastReadAt | RFC 3339|null | no | how far the visitor has read |
The visitor object carries only what the site itself passed with a signed token. Internal marks of the conversation (the Telegram topic id, the open channel chat) are never served outside.
Close a conversation #
A closed conversation stops taking replies: the visitor's next message starts a new one. Closing it again is not an error and produces no event.
{"data": {"id": "6c82f8e2-…", "status": "closed"}}Messages of a conversation #
| Field | Type | Req. | Description |
|---|---|---|---|
| cursor | RFC 3339 | no | continuation of the previous page |
| limit | int | no | 1…100, 25 by default |
{"data": [{
"id": "7735af47-2e36-45fd-9543-83f67b6d7eef",
"direction": "outgoing",
"senderType": "admin",
"content": "Your order is on the way",
"entities": [{"type": "bold", "offset": 0, "length": 4}],
"messageType": "text",
"origin": "api",
"createdAt": "2026-08-22T09:15:04.281Z",
"editedAt": null,
"attachments": []
}], "hasMore": false}The message object #
| Field | Type | Req. | Description |
|---|---|---|---|
| id | uuid | yes | message id |
| direction | string | yes | incoming is written by the visitor, outgoing by support |
| senderType | string | yes | customer, admin, system |
| content | string | yes | the text |
| entities[] | object | no | text markup as ranges, see the Text formatting section; an empty list for plain text |
| messageType | string | yes | text, image, document, audio, video |
| origin | string | yes | where it was written: widget, telegram, api, bitrix24, amocrm, retailcrm, unknown |
| createdAt | RFC 3339 | yes | when it was recorded |
| editedAt | RFC 3339|null | no | when it was edited |
| attachments[] | object | no | attachments, see the Attachments section below |
🔴 origin exists so you do not loop: receiving an event about your own reply, your system must not send it a second time.
Reply to a visitor #
| Field | Type | Req. | Description |
|---|---|---|---|
| content | string | no | text up to 4096 characters; required unless file is given |
| entities[] | object | no | text markup: bold, italic, underline, strikethrough, monospace, quote and a link with a caption (see the section below) |
| file.url | string | no | a link to the file: we download it ourselves, up to 50 MB |
| file.name | string | no | the file name for the conversation |
| file.type | string | no | image, document, audio, video |
| file_url, file_name, file_type | string | no | the same as flat fields, for integration builders that cannot send nested objects |
POST /api/v1/conversations/6c82f8e2-…/messages
Idempotency-Key: reply-4417
Content-Type: application/json
{"content": "Your order is on the way",
"file": {"url": "https://example.com/act.pdf", "name": "Invoice.pdf", "type": "document"}}201 {"data": {"id": "7735af47-…", "content": "Your order is on the way",
"createdAt": "2026-08-22T09:15:04.281Z", "direction": "outgoing", "status": "queued"}}
200 {"data": {"id": "7735af47-…", "duplicate": true}}The file is fetched before the message is written: an unreachable link will not leave an empty reply in the conversation. Requests to your address bypass proxies and never go into an internal network. An attachment can also be sent as the flat fields file_url, file_name, file_type, which suits integration builders; the nested file object wins. A file name or type without a link is a validation_error rather than a silent text-only send: otherwise the scenario would believe the attachment went out.
Text formatting #
In the chat window the text is shown with markup: bold, italic, underline, strikethrough, monospace, a quote and a link hidden behind a short caption instead of a long address.
The operator has nothing to learn for that: they format the reply with the usual Telegram tools (select the text → Bold, Link and so on), and the visitor sees the same. Through the API the markup is set with the entities field.
Markup is passed as ranges, not tags: we neither accept nor serve HTML in a conversation. A range says "from this character, this many characters, this style". Offsets are counted in UTF-16 units, the same way the Telegram Bot API and browser strings count them: an emoji takes two units, a Latin letter one.
| Field | Type | Req. | Description |
|---|---|---|---|
| type | string | yes | bold, italic, underline, strikethrough, code, pre, blockquote, text_link |
| offset | int | yes | the start of the range from the beginning of the text, in UTF-16 units |
| length | int | yes | the length of the range in the same units |
| url | string | no | the address for type: text_link; http, https, mailto and tel schemes are allowed |
| language | string | no | the code block language for type: pre |
POST /api/v1/conversations/6c82f8e2-…/messages
Content-Type: application/json
{"content": "Your order is on the way, track it here",
"entities": [
{"type": "bold", "offset": 0, "length": 4},
{"type": "text_link", "offset": 34, "length": 4, "url": "https://example.com/track/4417"}
]}Ranges that cannot be shown are dropped while the text stays whole: an unknown type, a range outside the text and a link with a foreign scheme (javascript:, data:) silently become plain text. Ranges must not overlap: nest them the way Telegram does: a link inside bold, not half of it.
Idempotency-Key #
The header makes a retry safe: the connection can drop after the request was already accepted. Sending again with the same key returns 200 and duplicate: true instead of a second message.
Up to 128 characters from A-Z a-z 0-9 . _ : - are allowed. The key works within your access key: two different access keys with the same Idempotency-Key do not interfere.
What status: queued means #
The reply is accepted for delivery and goes into the visitor's window through the same queue as an operator reply from Telegram. There is no separate delivered flag in the API: the message.created event arrives on the usual terms, and whether the visitor saw the reply is reflected by the conversation's lastReadAt field.
Attachments #
| Field | Type | Req. | Description |
|---|---|---|---|
| id | uuid | yes | file id |
| type | string | yes | image, document, audio, video |
| name | string | no | file name |
| size | int | no | size in bytes |
| mimeType | string | no | content type |
| url | string | no | a signed link with an expiry |
| urlExpiresAt | RFC 3339 | no | when the link stops working |
The signed link #
The link needs no key: another server opens it, and it has no entrance of ours. The protection is the signature and the one-hour expiry. Conversations must not be served from a guessable address, so open attachment links do not exist.
Download with the key #
Serves the file itself with its content type. Useful when the signed link expired or when your server code fetches the file.
curl -s -o act.pdf https://api.widgetchat.ru/api/v1/media/9f2c… -H "Authorization: Bearer wck_…"