Media
Photos, videos and voice notes share one upload flow: ask for an upload, send the file to storage, then complete it. Completing creates the message.
Before you start
Section titled “Before you start”- A conversation exists and the viewer has a session token. See Conversations.
- The
policyrow from the sender to the peer hasphotoson, andchat.photosis on. Videos needchat.videosand voice notes needchat.voice. - Your
limitsboundphotos_per_day(photos and videos together) andvoice_per_day. - A site that wants a consent step sets
photo_consent_requiredon the policy row toward the receiver.
export MC=https://app.mondochat.io/v1export SESSION=<session token>export ID=<conversation id>-
Ask for an upload with
POST /v1/photos/uploads.content_typeis one ofimage/jpeg,image/png,image/webp,image/heic,video/mp4oraudio/mp4.bytesis the file size.Terminal window curl -X POST $MC/photos/uploads -H "Authorization: Bearer $SESSION" \-H 'content-type: application/json' \-d "{\"conversation_id\": \"$ID\", \"content_type\": \"image/jpeg\", \"bytes\": 482113}"The answer is
{photo_id, url, fields, expires_at}: a presigned POST valid for 5 minutes. It pins the exact key, the content type you declared, and a size from 1 byte tochat.photo_max_bytes(chat.video_max_bytesfor video,chat.voice_max_bytesfor voice). The row ispendinguntil you complete it. -
Send the file to storage with a multipart POST. Put every entry of
fieldsfirst, then the file as the last field,file.const form = new FormData()for (const [k, v] of Object.entries(upload.fields)) form.append(k, v)form.append('file', file)await fetch(upload.url, { method: 'POST', body: form }) -
Complete it with
POST /v1/photos/{id}/complete. The body is optional for a photo.Terminal window curl -X POST $MC/photos/$PHOTO_ID/complete -H "Authorization: Bearer $SESSION" \-H 'content-type: application/json' -d '{"caption": "Sunset", "once": false}'Field Meaning captionTrimmed, at most chat.text_max_length, else 422. Shown under the media to whoever may see it, kept out of previews and webhooktext.onceMakes it view once. 403 policy_deniedwith reasonview_oncewhenchat.view_onceis off.reply_to_id,forwardedAs on a text message; see Messages. videoRequired for a video: {frames, width, height, duration_ms}.voiceRequired for a voice note: {duration_ms, wave}.A photo must decode as JPEG, PNG, WebP or HEIC. Anything else answers 422 and the object is deleted. The server strips metadata, writes the renditions and stores the display size in
widthandheight(the long edge is at most 2048 px). The answer is thePhoto{id, kind, status, width, height, thumb_url, full_url, uploader_id}. -
For a video, record it in the sender’s browser first. The rules:
- The file is an H.264 and AAC MP4. The server reads the first 16 bytes, requires an MP4 and never decodes or transcodes it.
video.framesis 1 to 20 base64 JPEG frames of at most 200 KB each. The first is the poster. They are what moderation looks at.video.widthandvideo.heightare 1 to 16384.duration_msis at mostchat.video_max_seconds(900 by default) plus one second.- The upload is at most
chat.video_max_bytes(104857600 by default). Withoutchat.videos, the request answers 403policy_deniedwith reasonvideos. - A missing or invalid
videoanswers 422 and leaves the upload pending, so you can retry. A file that is not an MP4 answers 422 and is deleted.
-
For a voice note, record
audio/mp4. The rules:voice.duration_msis at mostchat.voice_max_seconds(900 by default) plus one second.voice.waveis 40 characters of0-9a-v, the level bars.- The upload is at most
chat.voice_max_bytes(10485760 by default). Withoutchat.voice, the request answers 403policy_deniedwith reasonvoice. - The message
textis the preview🎤 m:ssand it has no caption. - A missing or invalid
voiceanswers 422 and leaves the upload pending. A file that is not an MP4 answers 422 and is deleted.
-
Handle photo consent. When
photo_consent_requiredapplies to the receiver and they have not answered, the message isheld: the sender sees it and the receiver sees a placeholder. The receiver answers withPUT /v1/conversations/{id}/photo-consentand{"consent": "yes"}or{"consent": "no"}.yesreleases the held items and publishesmessage.updatedandphotos.updated.norejects the held items and deletes them from storage. Later uploads toward that receiver are refused with 403policy_deniedand reasonphotos_off.- The receiver can change the answer at any time. Items held for moderation are not released by consent.
-
Read the media.
thumb_urlandfull_urlare signed links. A client that finds a link expired fetches the gallery again for fresh ones.Link Lifetime Photo thumb_urlandfull_url, videothumb_urlThey end at the close of the 5-minute window after the current one, so a link lives 5 to 10 minutes. Every link issued in one window is identical. Video full_url1 hour, so a video plays to the end. Open endpoint, photo 120 seconds. Open endpoint, video or voice note 1 hour. List a conversation’s photos and videos with
GET /v1/conversations/{id}/photos?cursor=.... It answers{items, next_cursor}newest first. View-once media and voice notes are never listed. -
Open view-once media and voice notes with
POST /v1/conversations/{id}/messages/{message_id}/open. The answer is{url, caption, expires_at}.- A view-once message carries no media URL and no caption, in lists, events or the gallery, for anyone. Only its recipient may open it, while it is
sentand not redacted. - The first open stamps
opened_atand publishesmessage.updated. An open in the next 60 seconds returns the media again, so a failed load can retry. Later opens answer 410gone. The sender gets 404. - A voice note has no URL in lists or events for anyone but moderators. Its recipient gets the audio from the open endpoint, and the first open marks it played with
opened_at. The sender of a voice note that is not view once may open it too, which changes nothing. - While either user has
read_receiptsoff, the sender sees noopened_aton their own view-once messages. A plain voice note’s played state stays visible to its sender.
- A view-once message carries no media URL and no caption, in lists, events or the gallery, for anyone. Only its recipient may open it, while it is
-
Let a user upload an avatar. It needs
chat.avatar_upload, which is off by default; without it the request answers 403policy_deniedwith reasonavatar_upload. CallPOST /v1/me/avatarwith{content_type, bytes}, upload as in step 2, thenPOST /v1/me/avatar/{id}/complete. The avatar stayshelduntil photo moderation approves it, and then it wins over theavataryour server sends.
Check it works
Section titled “Check it works”- Upload a small JPEG through steps 1 to 3. The answer has
statusreadyand athumb_urlthat loads. - Send a photo with
"once": trueas one user. List the conversation as the peer: the message has no URL and no caption. Open it as the peer and you get a URL and the caption. Open it as the sender and you get 404. - Open it as the peer again after more than 60 seconds. The answer is 410
gone.
When it fails
Section titled “When it fails”| Answer | Cause |
|---|---|
403 policy_denied |
The reason says which rule: photos, videos, voice, photos_off (the peer refused photos), view_once, avatar_upload, forward, or inbound (a guest toward a peer who takes messages from members only). |
403 limit_reached |
photos_per_day or voice_per_day is reached. |
403 forbidden with reason undelivered |
The peer blocked the sender and chat.refuse_blocked_send is on. |
| 422 | The file is too big, is not the declared format, or video or voice is missing or invalid. |
| 404 | Opening a view-once item as its sender, or opening a held item as its receiver. |
410 gone |
A view-once item was opened more than 60 seconds ago. |
Limits
Section titled “Limits”- An upload ticket lasts 5 minutes. A
pendingupload that is never completed is deleted after 24 hours. - Photos and videos count together against
photos_per_day. Voice notes count againstvoice_per_day. - Video moderation reads the frames the sender’s browser sends, and voice moderation reads a transcript of the note.
- A view-once link that was already handed out works until it expires. Nothing stops a screenshot.
- Moderators always see the media and the caption of a view-once message.
- Browsers that cannot encode H.264 and AAC are not offered video.
Related topics
Section titled “Related topics”- Request an upload, complete an upload, open a message, list photos, set photo consent
- Messages and Moderation
- Settings reference for
chat,limitsandpolicy - Errors