Send a message
Chat
Send a message
REST mirror of the chat:message:send socket event. Same service, same dedup, same gating.
POST
Send a message
Overview
Inserts a new message into the conversation and bumps the per-participant unread counters. The request must include a client-generatedclientMessageId so retries (e.g. on a flaky network) are idempotent: the server’s unique index on (senderId, clientMessageId) collapses duplicates into a single row.
This endpoint is the REST mirror of the chat:message:send socket event. Both transports converge on the same messageService.sendMessage, so validation, dedup, badging, and (Phase 5) push fan-out are identical regardless of which one the client picks. See Socket events for the WebSocket variant and the REST-vs-WS parity discussion.
The recipient’s UI is updated either by the socket broadcast (chat:message:new) or by the recipient calling List messages — push notifications are deferred to Phase 5.
Authentication
Bearer <accessToken> required. requireOnboarded middleware applied.
Additional service-level gates (checked in this order):
- Conversation exists → otherwise
404 NOT_FOUND. - Caller is a participant → otherwise
403 FORBIDDEN. - Conversation
status === "ACTIVE"→ otherwise403 CONVERSATION_BLOCKED. - No block exists between sender and the other participant (in either direction) → otherwise
403 CONVERSATION_BLOCKED.
Path parameters
| Name | Type | Required | Notes | Example |
|---|---|---|---|---|
id | string | yes | 24-char hex ObjectId of the conversation. | 66400a8f1c2b4d5e6f7a9000 |
Query parameters
None.Request body
| Field | Type | Required | Allowed values | Example |
|---|---|---|---|---|
clientMessageId | string | yes | 1..64 chars. Any opaque token the client uses to dedup retries — a ULID or UUID is recommended. Must be unique per (sender, message); reusing it for a different message returns the original message row with deduped: true. | cm_01HZQ7K3M4N5P6Q7R8S9T0V1W2 |
messageType | enum | no | text (default), image, video, file. Determines whether text or media is required. | image |
text | string | conditional | For text messages: required, 1..4000 chars (whitespace-only rejected). For media messages: optional caption, 0..4000 chars. | Hi! Loved your kitchen. |
media | object | conditional | Required when messageType is image/video/file. See Media object. Must be null/absent for text messages. |
Media object
Upload the file first via the presign flow withfileType: "CHAT_MEDIA", PUT the bytes to the returned URL, then post the resulting CDN url here.
| Field | Type | Required | Notes |
|---|---|---|---|
url | string (URL) | yes | Public CDN url of the uploaded file. |
thumbnailUrl | string (URL) | null | no | Poster/thumbnail (image variant or video frame). |
fileName | string | null | no | Original filename — shown on file bubbles. |
mimeType | string | null | no | e.g. image/jpeg, video/mp4, application/pdf. |
sizeBytes | integer | null | no | File size — shown on file bubbles. |
width / height | integer | null | no | Pixel dimensions for image/video (lets the client size the bubble before load). |
durationSec | number | null | no | Video duration in seconds. |
Example payload — text
Example payload — image (with optional caption)
A captionless media message (
text: "") shows a typed placeholder in the inbox preview — 📷 Photo, 🎥 Video, or 📎 File. The chat:message:new socket broadcast carries the full message (including media) so recipients render the attachment live.Response
201 Created — fresh insert
A new messages row was persisted. The conversation’s lastMessage, lastMessageAt, and the recipient’s unreadCount were bumped. If a WebSocket client is subscribed to conversation:<id>, a chat:message:new event has been broadcast.
200 OK — dedup (idempotent retry)
The same clientMessageId was previously used. The server returns the original message row unchanged. No new row was inserted, no broadcast was emitted, no unread counter was incremented a second time.
Message object
See the Message object schema.Error responses
| Status | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_FAILED | clientMessageId missing or wrong length, text empty/whitespace-only/over 4000 chars, or id not a 24-char hex ObjectId. |
| 401 | UNAUTHENTICATED | Missing, malformed, or expired access token. |
| 403 | ONBOARDING_INCOMPLETE | Caller has not finished onboarding. |
| 403 | FORBIDDEN | Caller is not a participant of this conversation. |
| 403 | CONVERSATION_BLOCKED | Either conversation status !== "ACTIVE", or a block exists in either direction between the participants. |
| 404 | NOT_FOUND | No conversation with that id. |
Example error — 403 CONVERSATION_BLOCKED
Example error — 400 VALIDATION_FAILED
Side effects
On a fresh insert (201):
- One
messagesrow inserted. - The conversation’s
lastMessage,lastMessageAtupdated to the new message. - Every recipient (every participant except the sender) gets
unreadCount += 1. - If a WebSocket socket is subscribed to
conversation:<id>it receives achat:message:newbroadcast (Socket events page). - (Phase 5) A
notification.pushBullMQ job is enqueued for each muted-off recipient.
200): no side effects at all.
See also
- Socket events —
chat:message:send— the WebSocket variant; same service. - List messages — history retrieval.
- Mark conversation read — resets the recipient’s unread counter.
- Match → Conversation handshake — how a conversation gets created in the first place.