Messages
Before you start
Section titled “Before you start”- A conversation exists. See Conversations.
- User routes take
Authorization: Bearer <session token>. Server routes take your server secret and the app id; see Sessions and tokens. - The
policyrow from the sender’s role to the peer’s role hasmessageon. Yourlimitsboundmessages_per_minute(30 by default) andmessages_to_unanswered.
export MC=https://app.mondochat.io/v1export SESSION=<session token>export ID=<conversation id>-
Send a message with
POST /v1/conversations/{id}/messages. Generateclient_msg_idon the client for every message.Terminal window curl -X POST $MC/conversations/$ID/messages -H "Authorization: Bearer $SESSION" \-H 'content-type: application/json' \-d '{"client_msg_id": "7d6f3c0e-2b0e-4f5e-9a55-0e3c1a1f6a01", "kind": "text", "text": "Hello"}'const res = await fetch(`https://app.mondochat.io/v1/conversations/${id}/messages`, {method: 'POST',headers: { authorization: `Bearer ${session}`, 'content-type': 'application/json' },body: JSON.stringify({ client_msg_id: crypto.randomUUID(), kind: 'text', text: 'Hello' }),})const message = await res.json()The answer is 201 with the
Message. Send the sameclient_msg_idagain in the same conversation and you get 200 with the stored message and nothing else happens, so a retry after a timeout never duplicates it. Text is limited bychat.text_max_length(4000 by default); longer text answers 422.The user route accepts only
kindtextandphoto. Video and voice go through Media. Cards and system messages come from your server, see step 10. -
Quote a message with
reply_to_id. It must be a message of this conversation that the sender can see and that is not deleted or redacted, else 422. EveryMessagecarriesreply_to: {id, sender_id, kind, preview} | null. The preview is the quoted text up to 140 characters.reply_tois null once the quoted message is deleted, redacted or not visible to the viewer. -
Mark a send as forwarded with
"forwarded": true. The server stores the claim and checks nothing else. To forward for real, usePOST /v1/conversations/{id}/forward, where{id}is the target conversation:Terminal window curl -X POST $MC/conversations/$TARGET/forward -H "Authorization: Bearer $SESSION" \-H 'content-type: application/json' \-d '{"message_id": "<source message id>", "client_msg_id": "<new uuid>"}'The copy has
forwarded: true, noreply_to, and is never view once. Media is copied inside storage and never downloaded. The source must be a message of your app, in a conversation the caller is in, that is sent, not deleted, not redacted and not hidden by the caller. Anything else answers 404. A view-once message, a card or a system message answers 403policy_deniedwith reasonforward. The same gates as a send or an upload apply, andclient_msg_iddedupes as on send. -
Edit with
PATCH /v1/conversations/{id}/messages/{message_id}and{"text": "..."}. Only the sender’s own text message, not held, deleted or redacted, and only withinchat.edit_window_seconds(172800, two days, by default). Outside the window, or with the window at 0, the answer is 403policy_deniedwith reasonedit. The edit setsedited_at, publishesmessage.updatedand is moderated again. -
Delete with
DELETE /v1/conversations/{id}/messages/{message_id}.scopeEffect me(default)Hides the message from the viewer’s own lists and inbox preview. The peer still sees it. everyoneThe sender’s own non-card message, within chat.unsend_window_seconds(172800 by default). It becomes a tombstone withdeleted: trueand no text, caption, photo, card or quote, leaves the peer’s unread count and publishesmessage.updated. Outside the window, or with the window at 0, the answer is 403policy_deniedwith reasonunsend.Both answer 204.
-
React with
PUT /v1/conversations/{id}/messages/{message_id}/reactionand{"emoji": "👍"}. A user has one reaction per message, and a secondPUTreplaces it.DELETEon the same path removes it and answers 204, also when there was none.emojiis one grapheme cluster of at most 48 UTF-8 bytes, else 422. You can react only to a message you list that is not deleted, redacted, held or rejected, and a view-once message only after you opened it; anything else answers 404. A reaction is a write to the peer, so your policymessageand your message limits apply to it.Message.reactionsis[{emoji, count, mine}], and a change publishesmessage.updated. -
Show typing with
POST /v1/conversations/{id}/typing. It answers 204 and stores nothing. Send it at most every 3 seconds and show it for 5. -
Report delivery and reads.
POST /v1/conversations/{id}/deliveredwith{message_id}moves the delivered pointer forward and publishesdelivered.updatedto the peer when it moved.POST /v1/conversations/{id}/readwith{message_id}moves the read pointer forward, never back, and answers{unread_count}.read.updatedgoes to the peer only while both users haveread_receiptson.GET /v1/unreadanswers{count}for the whole inbox.GET /v1/conversations/{id}/messages/{message_id}/infoanswers{delivered_at, read_at}to the sender of that message.read_atis null while either user hasread_receiptsoff. Anyone else gets 404.
From your own server,
POST /v1/server/conversations/{id}/deliveredwith{external_id, message_id}does the same as the user route for that user, with no session. Use it when your push or email channel confirms delivery. It answers 204, and 404 for an unknown user, a user who is not in the conversation, another app’s conversation, or a message the user cannot see.Terminal window curl -X POST $MC/server/conversations/$ID/delivered \-H "Authorization: Bearer $MONDOCHAT_SERVER_SECRET" -H "Mondochat-App-Id: $MONDOCHAT_APP_ID" \-H 'content-type: application/json' -d '{"external_id": "bob", "message_id": "<message id>"}' -
List messages with
GET /v1/conversations/{id}/messages. The answer is{items, next_cursor, latest_seq}, newest first.Query Result before=<cursor>&limit=<n>The next older page. limitis at most 50.around=<message_id>That message, up to limit / 2older ones and the rest newer. 404 when the viewer cannot see it.after=<message_id>Newer messages, oldest first. Pass next_cursorback asafter.q=<text>Visible messages whose text or caption contains it, ignoring case, newest first. 1 to 100 characters after trimming, else 422. Not with aroundorafter.Search never matches a deleted, redacted or hidden message, another user’s held or undelivered message, or a view-once caption.
-
Post a system message or a card from your server with
POST /v1/server/messages.fromandtoare external ids and pick the existing conversation, or 404 when they have none. Asystemmessage has no sender. Acardis sent byfromand carries{type, data}. Send anIdempotency-Key.Terminal window curl -X POST $MC/server/messages \-H "Authorization: Bearer $MONDOCHAT_SERVER_SECRET" -H "Mondochat-App-Id: $MONDOCHAT_APP_ID" \-H 'Idempotency-Key: welcome-alice-bob' -H 'content-type: application/json' \-d '{"from": "alice", "to": "bob", "kind": "system", "text": "Welcome to the chat"}'Users never post cards. Offers go through the offers route and call cards come from calls; see Commerce and Calls. The
Cardtypes arecall_invite,call_ended,call_missed,purchase,offer,systemandsignup_invite.
Check it works
Section titled “Check it works”- Send a message and read the answer.
statusissentandseqis set. - Send it again with the same
client_msg_id. The answer is 200 and theidis the same. - List the conversation as the peer. The message is there,
read_by_peeris false, andunread_countin the inbox is 1. - Mark it read as the peer. The sender gets
read.updatedwhile both users haveread_receiptson.
When it fails
Section titled “When it fails”| Answer | Cause |
|---|---|
403 forbidden with reason you_blocked |
The sender blocked the peer. |
403 forbidden with reason undelivered |
The peer blocked the sender and chat.refuse_blocked_send is on. Nothing is stored. |
403 policy_denied, reason message |
The policy row from the sender’s role to the peer’s has message off. This covers reactions too. |
403 policy_denied, reason photos |
A photo send, with chat.photos off or the policy row’s photos off. |
403 policy_denied, reason inbound |
workspace.inbound is on, the sender is a guest and the peer takes messages from members only. |
403 policy_denied, reason edit, unsend or forward |
The edit or unsend window is closed or set to 0, or the message cannot be forwarded. |
403 limit_reached, reason messages_to_unanswered or photos_per_day |
The role’s limit in limits is reached. |
429 rate_limited |
limits.messages_per_minute is reached, or the app’s request rate. Wait Retry-After. |
| 404 | The message is not visible to the caller, or the caller is not a participant. |
| 422 | Text over chat.text_max_length, a bad reply_to_id, a bad emoji, or a malformed query. |
Limits
Section titled “Limits”- A blocked recipient is not an error. By default the send succeeds for the sender, is stored as
undelivered, and nothing reaches the peer. The sender sees one tick forever on it. Turn onchat.refuse_blocked_sendto refuse it instead. - Receipts: while either user has
read_receiptsoff,read_by_peeris false for both and noread.updatedis published.delivered_to_peerstill counts a message the peer read. - Held messages (moderation
hold, or photo consent) are visible to the sender only. A moderationblockredacts the message:redactedis true and the text is gone. - Every message has a
seqthat orders it inside its conversation. Keep the copy with the higherseq. See ordering. - Webhook bodies carry
textonly withwebhooks.message_text.
Related topics
Section titled “Related topics”- Send a message, edit, delete, forward, list messages
- Set a reaction, mark read, message info, post a server message
- message.created webhook
- Settings reference for
chat,limitsandpolicy - Errors