Skip to content

Messages

  • 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 policy row from the sender’s role to the peer’s role has message on. Your limits bound messages_per_minute (30 by default) and messages_to_unanswered.
Terminal window
export MC=https://app.mondochat.io/v1
export SESSION=<session token>
export ID=<conversation id>
  1. Send a message with POST /v1/conversations/{id}/messages. Generate client_msg_id on 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 same client_msg_id again 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 by chat.text_max_length (4000 by default); longer text answers 422.

    The user route accepts only kind text and photo. Video and voice go through Media. Cards and system messages come from your server, see step 10.

  2. 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. Every Message carries reply_to: {id, sender_id, kind, preview} | null. The preview is the quoted text up to 140 characters. reply_to is null once the quoted message is deleted, redacted or not visible to the viewer.

  3. Mark a send as forwarded with "forwarded": true. The server stores the claim and checks nothing else. To forward for real, use POST /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, no reply_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 403 policy_denied with reason forward. The same gates as a send or an upload apply, and client_msg_id dedupes as on send.

  4. 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 within chat.edit_window_seconds (172800, two days, by default). Outside the window, or with the window at 0, the answer is 403 policy_denied with reason edit. The edit sets edited_at, publishes message.updated and is moderated again.

  5. Delete with DELETE /v1/conversations/{id}/messages/{message_id}.

    scope Effect
    me (default) Hides the message from the viewer’s own lists and inbox preview. The peer still sees it.
    everyone The sender’s own non-card message, within chat.unsend_window_seconds (172800 by default). It becomes a tombstone with deleted: true and no text, caption, photo, card or quote, leaves the peer’s unread count and publishes message.updated. Outside the window, or with the window at 0, the answer is 403 policy_denied with reason unsend.

    Both answer 204.

  6. React with PUT /v1/conversations/{id}/messages/{message_id}/reaction and {"emoji": "👍"}. A user has one reaction per message, and a second PUT replaces it. DELETE on the same path removes it and answers 204, also when there was none. emoji is 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 policy message and your message limits apply to it. Message.reactions is [{emoji, count, mine}], and a change publishes message.updated.

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

  8. Report delivery and reads.

    • POST /v1/conversations/{id}/delivered with {message_id} moves the delivered pointer forward and publishes delivered.updated to the peer when it moved.
    • POST /v1/conversations/{id}/read with {message_id} moves the read pointer forward, never back, and answers {unread_count}. read.updated goes to the peer only while both users have read_receipts on.
    • GET /v1/unread answers {count} for the whole inbox.
    • GET /v1/conversations/{id}/messages/{message_id}/info answers {delivered_at, read_at} to the sender of that message. read_at is null while either user has read_receipts off. Anyone else gets 404.

    From your own server, POST /v1/server/conversations/{id}/delivered with {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>"}'
  9. 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. limit is at most 50.
    around=<message_id> That message, up to limit / 2 older ones and the rest newer. 404 when the viewer cannot see it.
    after=<message_id> Newer messages, oldest first. Pass next_cursor back as after.
    q=<text> Visible messages whose text or caption contains it, ignoring case, newest first. 1 to 100 characters after trimming, else 422. Not with around or after.

    Search never matches a deleted, redacted or hidden message, another user’s held or undelivered message, or a view-once caption.

  10. Post a system message or a card from your server with POST /v1/server/messages. from and to are external ids and pick the existing conversation, or 404 when they have none. A system message has no sender. A card is sent by from and carries {type, data}. Send an Idempotency-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 Card types are call_invite, call_ended, call_missed, purchase, offer, system and signup_invite.

  1. Send a message and read the answer. status is sent and seq is set.
  2. Send it again with the same client_msg_id. The answer is 200 and the id is the same.
  3. List the conversation as the peer. The message is there, read_by_peer is false, and unread_count in the inbox is 1.
  4. Mark it read as the peer. The sender gets read.updated while both users have read_receipts on.
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.
  • 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 on chat.refuse_blocked_send to refuse it instead.
  • Receipts: while either user has read_receipts off, read_by_peer is false for both and no read.updated is published. delivered_to_peer still counts a message the peer read.
  • Held messages (moderation hold, or photo consent) are visible to the sender only. A moderation block redacts the message: redacted is true and the text is gone.
  • Every message has a seq that orders it inside its conversation. Keep the copy with the higher seq. See ordering.
  • Webhook bodies carry text only with webhooks.message_text.