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.

The list of conversations #

GET /api/v1/conversations conversations:read
FieldTypeReq.Description
statusstringnoopen, pending, resolved or closed
sinceRFC 3339noonly the ones changed after this time
cursorRFC 3339nocontinuation of the previous page
limitintno1…100, 25 by default
Example call
curl -s "https://api.widgetchat.ru/api/v1/conversations?status=open&since=2026-08-22T00:00:00Z" -H "Authorization: Bearer wck_…"
Response
{"data": [ /* conversation objects */ ], "nextCursor": "…", "hasMore": true}

One conversation #

GET /api/v1/conversations/{id} conversations:read

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 #

FieldTypeReq.Description
iduuidyesconversation id
statusstringyesopen, pending, resolved, closed
channelstringyesalways app: the message comes from the widget
visitor.idstringyesthe visitor's permanent id
visitor.namestringnothe name, if the site passed it
visitor.emailstringnothe email, if the site passed it
visitor.organizationstringnothe organization from the signed token
visitor.accountIdstringnoyour customer id from the signed token
visitor.rolestringnothe visitor's role from the signed token
sourcestringnowhere 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
createdAtRFC 3339yeswhen it was created
lastMessageAtRFC 3339|nullnotime of the last message
lastReadAtRFC 3339|nullnohow 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 #

POST /api/v1/conversations/{id}/close conversations:write

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.

Response
{"data": {"id": "6c82f8e2-…", "status": "closed"}}

Messages of a conversation #

GET /api/v1/conversations/{id}/messages conversations:read
FieldTypeReq.Description
cursorRFC 3339nocontinuation of the previous page
limitintno1…100, 25 by default
Response
{"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 #

FieldTypeReq.Description
iduuidyesmessage id
directionstringyesincoming is written by the visitor, outgoing by support
senderTypestringyescustomer, admin, system
contentstringyesthe text
entities[]objectnotext markup as ranges, see the Text formatting section; an empty list for plain text
messageTypestringyestext, image, document, audio, video
originstringyeswhere it was written: widget, telegram, api, bitrix24, amocrm, retailcrm, unknown
createdAtRFC 3339yeswhen it was recorded
editedAtRFC 3339|nullnowhen it was edited
attachments[]objectnoattachments, 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 #

POST /api/v1/conversations/{id}/messages conversations:write
FieldTypeReq.Description
contentstringnotext up to 4096 characters; required unless file is given
entities[]objectnotext markup: bold, italic, underline, strikethrough, monospace, quote and a link with a caption (see the section below)
file.urlstringnoa link to the file: we download it ourselves, up to 50 MB
file.namestringnothe file name for the conversation
file.typestringnoimage, document, audio, video
file_url, file_name, file_typestringnothe same as flat fields, for integration builders that cannot send nested objects
Request
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"}}
Response
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.

FieldTypeReq.Description
typestringyesbold, italic, underline, strikethrough, code, pre, blockquote, text_link
offsetintyesthe start of the range from the beginning of the text, in UTF-16 units
lengthintyesthe length of the range in the same units
urlstringnothe address for type: text_link; http, https, mailto and tel schemes are allowed
languagestringnothe code block language for type: pre
Request
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 #

FieldTypeReq.Description
iduuidyesfile id
typestringyesimage, document, audio, video
namestringnofile name
sizeintnosize in bytes
mimeTypestringnocontent type
urlstringnoa signed link with an expiry
urlExpiresAtRFC 3339nowhen the link stops working

The signed link #

GET /media/signed/{id}?exp=…&sig=…

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 #

GET /api/v1/media/{id} media:read

Serves the file itself with its content type. Useful when the signed link expired or when your server code fetches the file.

Example call
curl -s -o act.pdf https://api.widgetchat.ru/api/v1/media/9f2c… -H "Authorization: Bearer wck_…"