Files overview
Images, voice, video and files in messages, user and group avatars, chat room covers, and group files can all be uploaded to and stored by this service. File content is written by the uploader straight to object storage and read straight from object storage (or a CDN) when downloading, without going through IM APIs.
| You want to | See |
|---|---|
| Upload an image and send it as an image message | Upload an image and send it |
| Set a user's avatar | Set a user's avatar |
| Download attachments and group files | Get download addresses |
| Look up or delete files, check storage usage | Download and manage files |
| See each purpose's size limit, who can download and how long it's kept | This page: File purposes |
Each file is given a purpose (purpose) when uploaded, which decides its size limit, who can download it and how long it's kept. A completed upload gives one of two kinds of address:
| Address | For | Can it be opened directly? | How to use it |
|---|---|---|---|
| File address | Message attachments, group files | No; exchange it for a short-lived download address first | Put it in the message's body |
| Public address | User avatars, group avatars, chat room covers | Yes, by anyone | Put it in avatar_url of the user, group or chat room profile |
Client uploads
When users send images, change avatars or upload group files from clients, the client calls the same upload APIs as OpenAPI with a User Token (under the /client/v1/media prefix, without owner, rate-limited per user); the SDKs wrap this. This section only covers the APIs your server calls through OpenAPI.
File purposes
Purpose purpose | Content | Allowed kinds kind | Size limit | Who can download | How long it's kept |
|---|---|---|---|---|---|
attachment message attachment | Files in image, voice, video and file messages | image, voice, video, file | max_upload_bytes in the runtime policy, 100 MB by default; images also at most 20 MB, voice at most 5 MB | Your server; any signed-in user of this app (holding the file address is enough) | attachment_retention_days from upload, 30 days by default |
user_avatar user avatar | The avatar in a user's profile | image | 5 MB | Anyone (public address) | Kept while in use, see below |
group_avatar group avatar | The avatar in a group's profile | image | 5 MB | Anyone (public address) | As for user avatars |
chatroom_avatar chat room cover | avatar_url in a chat room profile, see Covers | image | 5 MB | Anyone (public address) | As for user avatars; deleted 7 days after the room is dissolved |
group_file group file | A group's shared files | image, video, file | As for attachments | Your server; current group members | No expiry by default, or set days with group_file_retention_days; deleted when the group is dissolved |
For the runtime policy items, see Runtime policy. Also:
File names:
namewhen creating the upload, 1 to 255 characters, same rules asnamein file messages (not only whitespace, no/,\, control characters or text-direction characters). Required forfilekinds and group files, and used as the saved file name when downloading.Group files: up to 1000 per group. With
group_file_enabledoff in the runtime policy, members can't upload group files from clients, but your server still can. In clients only current members can see the group file list and download, and not after leaving.Avatars (including chat room covers) are kept based on whether they're in use:
Case Avatar file Not set in a profile within 24 hours of upload Deleted automatically The profile's avatar is changed to another address or cleared Deleted after 7 days, leaving time for clients still caching the old profile A user avatar set in another user's profile (not the ownerfrom upload)Doesn't count as use. A group avatar counts for the first group to use it, a cover for the first chat room The user is deleted Their user avatar is deleted right away The group is dissolved The group avatar is kept The chat room is dissolved The cover is deleted after 7 days Ownership: files belong to the app that uploaded them; users and servers of other apps can't download them.
File addresses
Completed message attachments and group files get a file address, and images also get a thumbnail address:
https://im.example.com/media/v1/f/{file_id}/{secret}
https://im.example.com/media/v1/f/{file_id}/{secret}/thumbfile_idis the file ID used by APIs such as Get a file and Delete a file;secretis a random 22-character key that can't be guessed.- The address's host depends on how the service is deployed and isn't necessarily your OpenAPI address. Use the
urlandthumbnail_urlreturned by the API as is, without building or modifying them; an address with query parameters added is no longer recognized as a file address. - File addresses never change, so you can store them long-term, put them in messages and use them as client cache keys. After a file expires or is deleted, the address stays the same; it just can't be downloaded.
File addresses can't be downloaded directly. Opening one returns 401 unauthenticated, and the App Token can't request it directly either. Exchange it for a download address first:
Your server: call Get download addresses, up to 100 addresses at a time, for any of the app's attachments and group files.
Clients: users exchange them with a User Token (the same batch API as OpenAPI,
POST /client/v1/media/download-urls), or request the file addressGET /media/v1/f/{file_id}/{secret}(…/thumbfor thumbnails) with the User Token, and after checking the server returns a302to the download address:httpGET /media/v1/f/99606093402996736/DC0neNm6HBbxGW9bN6g_iA/thumb HTTP/1.1 Host: im.example.com Authorization: Bearer <User Token> HTTP/1.1 302 Found Cache-Control: no-store Location: https://storage.example.com/im-private/t99604914749046784/a99604957103128576/m/99606093402996736_thumb?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20261002T203000Z&X-Amz-Expires=5400&X-Amz-SignedHeaders=host&X-Amz-Signature=52ca87a0...Mobile image loading libraries can work this way and cache images by the unchanging file address. When following the redirect, don't carry the
Authorizationheader to the download address: object storage rejects requests that have both signature parameters andAuthorization(400), and the User Token would leak to the storage service. Most HTTP libraries drop it when redirecting to another host. File addresses don't return CORS headers, so web pages should use the exchange API.
Who can exchange addresses:
| Caller | Message attachments | Group files |
|---|---|---|
| Your server (OpenAPI) | All | All, without checking membership |
| Clients (signed-in users of this app) | Any user, without checking whether they're in the attachment's conversation | Current group members; others get not_group_member |
Attachments aren't permission-checked by conversation: the same address appears in other conversations when forwarded, and since the key in the address can't be guessed, anyone holding it has seen the message. Client exchanges (including requesting file addresses directly) are limited to 600 files per user per minute; beyond that you get 429 rate_limited with details.reason download_rate.
Results of an exchange: usable files get a download address; expired or deleted files get file_expired (410 when requesting the file address directly), blocked files file_blocked (403), and files from another app, with a wrong key or still uploading get not_found (404).
Only accepting this service's addresses
When the app enables media_url_only in its runtime policy, image, voice, video and file messages sent from clients can only use this service's file addresses (or hosts in media_allowed_hosts), and user avatars, group avatars and chat room covers set from clients can only use this app's avatar public addresses (or https addresses on hosts in media_allowed_hosts). Addresses written by your server aren't restricted, see Runtime policy.
Thumbnail addresses
Image attachments and group files have a thumbnail address, the file address plus /thumb, pointing to a thumbnail at most 480 pixels on its long edge for display in message lists. If the original is at most 480 pixels and 100 KB, the thumbnail is the original. Other kinds have no thumbnail, and requesting /thumb returns not_found.
Public addresses
Completed avatars (user avatars, group avatars and chat room covers) get a public address anyone can open without signing in:
https://cdn.example.com/t99604914749046784/a99604957103128576/u/99606376459796480_thhts5lb.jpgAvatars are shown in bulk in conversation lists, friend lists and group member lists, and exchanging each one for a download address would be too costly, so they're kept in public storage. Public addresses never change either; just put one in avatar_url of the user, group or chat room profile. Responses carry Cache-Control: public, max-age=86400, so CDNs and clients can cache them for a day. Public addresses neither need nor accept exchange.
Upload flow
Uploading takes three steps, see Upload files:
- Create the upload: submit the purpose, kind, size and file name (plus the group ID for group files); the server checks the purpose and kind, size limits and the app's storage quota, and returns a file ID and upload address.
- Upload the content directly to object storage.
- Single upload: files up to 32 MB are written with one
PUTto the returned upload address. - Multipart upload: files over 32 MB are split into 16 MB parts (the last can be smaller), at most 128 parts. Request part upload addresses as needed and upload parts in parallel in any order; after a network interruption, list the uploaded parts and upload only the missing ones.
- Single upload: files up to 32 MB are written with one
- Complete the upload: the server checks the uploaded size, detects the format from the content and processes images; once it passes, the file becomes usable and you get the file object with the file address (or an avatar's public address), thumbnail address, dimensions and size.
The server decides between single and multipart upload from size. Avatars, images and voice are all at most 20 MB, so they're always single uploads; only videos and file kinds can be multipart.
- Validity: single uploads must complete within 1 hour of creation, multipart within 24 hours; each part's upload address is valid for 1 hour and can be requested again. Uploads still incomplete when they expire are discarded and their content deleted. Cancel uploads you no longer need.
- Repeated calls: completing can be retried, and completed uploads return the original result. Creating isn't deduplicated: each call creates a new file ID, and uploads never completed are deleted when they expire without counting toward storage.
- Only the creator can continue: uploads created through OpenAPI can only be continued, completed or canceled by the same app through OpenAPI (with any of the app's App Tokens); uploads created by clients or the console aren't visible to the OpenAPI upload APIs.
- Files still uploading can't be downloaded and don't count toward storage.
Format checks and image processing
When completing an upload, the server reads the start of the file to detect its format, ignoring the content_type declared at creation and the file name's extension. Formats allowed per kind (the recorded content_type in parentheses):
Kind kind | Allowed formats |
|---|---|
image | JPEG (image/jpeg), PNG (image/png), GIF (image/gif), WebP (image/webp) |
voice | AAC (audio/aac), M4A and other MP4 containers (audio/mp4), MP3 (audio/mpeg), AMR (audio/amr), 3GP (audio/3gpp), Ogg (audio/ogg), WAV (audio/wav), WebM (audio/webm) |
video | MP4 (video/mp4), MOV (video/quicktime), WebM (video/webm), 3GP (video/3gpp) |
file | Any format. Recognized formats are recorded as such, such as application/pdf for PDF; others are judged by content, such as text/plain for plain text; anything undeterminable is application/octet-stream |
If the format doesn't match the kind, completion fails with 400 invalid_argument, details.reason unsupported_type and the detected format in details.content_type, and you need to create a new upload. Convert HEIC and other image formats to JPEG first, or upload them as file; SVG, HTML and other content that can contain scripts can only be uploaded as file.
Images (image attachments and group files):
- Width times height can't exceed 50 megapixels, or completion fails (
image_too_large); undecodable images fail too (invalid_image). - EXIF, XMP and other metadata (which may include capture location and device model) are removed, including PNG text chunks and GIF comments; orientation, color profiles and animation loop counts are kept. Images aren't recompressed, so quality is unchanged, but the stored size may be slightly smaller than the original, so rely on
sizefrom completion. widthandheightare the display dimensions after applying orientation.- A thumbnail at most 480 pixels on its long edge is generated as JPEG, or PNG when there are transparent pixels; GIFs use the first frame. Originals at most 480 pixels and 100 KB, and animated WebP, use the original as the thumbnail.
- The SHA-256 of the stored content is computed, the
sha256in the file object.
Avatars: scaled to at most 640 pixels on the long edge (smaller ones aren't enlarged) and re-encoded as JPEG, or PNG when there are transparent pixels; GIFs use the first frame; all metadata is removed. The uploaded original isn't kept, and size, width, height and sha256 in the file object are all post-processing values.
Voice, video and file kinds (including images uploaded as file) aren't processed at all: no transcoding, no cover generation, no metadata removal (such as capture location in videos) and no virus scanning. Process them before uploading if needed, and upload a frame as a separate image for a video's cover.
Download response headers: download addresses return the detected Content-Type. Images, voice and video can be displayed or played in browsers; file kinds and all group files carry Content-Disposition: attachment with the original file name, so browsers download rather than open them.
Download addresses
Exchanged download addresses are signed object storage (or CDN) addresses; just GET them, without and never with an Authorization header. Range requests are supported (resuming downloads, seeking in videos).
- Valid for 60 to 90 minutes: the signing time is aligned to 30 minutes and valid for 90, with the expiry in
expires_atof the exchange result. The same file exchanged within the same 30 minutes gets exactly the same address, so browsers and CDNs can cache by address. - Exchange again before expiry; don't store download addresses or put them in messages, which should hold the unchanging file address.
- Exchanging a thumbnail address gives the thumbnail's download address.
- Anyone with a leaked download address can use it until it expires, so don't share it.
Expiry and deletion
Files are deleted in these cases:
| Case | Files deleted |
|---|---|
| Past retention | Attachments uploaded more than attachment_retention_days ago (30 by default); with group_file_retention_days set, group files older than that. Retention counts from upload, not from when the message was sent. expires_at in the file object is computed from the app's current retention, and a background job deletes files after it, possibly a little later (usually within an hour); after shortening retention, files already expired under the new value are deleted right away |
| Avatars no longer used | Avatars not used within 24 hours of upload; avatars replaced 7 days ago; covers of chat rooms dissolved 7 days ago |
| A user is deleted | Their user avatar. If the app enables erase_messages_on_user_delete, attachments and group files owned by them are deleted too |
| A group is dissolved | All its group files; the group avatar is kept |
| Explicit deletion | Your server deletes the file or it's deleted in the console, or a group file is deleted by the owner, an admin or the uploader in a client |
After a file is deleted, the message itself stays and the address in body doesn't change; recalling a message doesn't delete its attachments. Exchanging gets file_expired, requesting the file address directly returns 410, and clients should show "File expired". Deletion records are kept for about another 30 days, after which exchanging gets not_found; clients can show the same thing.
Download addresses exchanged before deletion may still work until the content is removed from storage (about 10 minutes); with a CDN, cached content can be served until the download address expires. CDN caches of avatar public addresses expire within a day.
Blocking violating files
Violating files are blocked: when moderation post-review finds a violation, or reviewers confirm one, under platform rules or your rules; the platform can also block files directly. After blocking:
| Area | Result |
|---|---|
| Downloads | Exchanging gets file_blocked, requesting the file address directly returns 403, and thumbnails likewise; already exchanged download addresses usually stop working within a minute (CDN caches can be served until the download address expires) |
| Avatars | Removed from the public address (usually within a minute); CDN caches expire within a day |
| File content | Kept as evidence for 180 days, then deleted; it can be unblocked in that time, after which the file is usable again (if past retention, it's then deleted by retention) |
| Who can unblock | Blocked under your rules (block_rule_source tenant): unblocked automatically when your reviewers change the review record to no violation. Under platform rules (platform): only the platform |
| Deletion and usage | You can't delete blocked files (403 file_blocked); they no longer count toward storage |
| Messages and profiles referring to it | Blocking itself doesn't change them. Moderation actions also recall the message and clear a blocked avatar; otherwise recall the message or update the profile yourself |
Getting a file shows blocked files with status blocked, plus when, why and under which rules they were blocked. If you disagree with a platform block, contact us.
Storage usage and quota
Usage is the space and file count of the app's usable files (status active), broken down by purpose, with images including thumbnails. Uploading, deleted and blocked files don't count. Get it with Get storage usage, or see "File storage" in the console.
Quota: when the platform enables file storage limits, it's the smaller of the platform's storage quota for the app (1024 GB by default, with 1 GB being 230 bytes) and the plan's storage capacity, see Plans and billing. With limits off, there's no quota, uploads aren't rejected and storage isn't billed.
- Over quota: when creating an upload, if used space plus this
sizeexceeds the quota, you get409 limit_exceeded(storage_quota, with the quota and used bytes indetails.limit_bytesanddetails.used_bytes). Only new uploads are rejected; downloads aren't affected and existing files aren't deleted. - Soft limit: concurrent uploads are each checked when created, so final usage may slightly exceed the quota.
- What to do: delete files you no longer need or shorten attachment retention; for a larger quota, change plans or contact us.
Data structures
File object
Returned when completing uploads, getting files and listing group files:
| Field | Type | Description |
|---|---|---|
file_id | String | The file ID |
purpose | String | attachment, user_avatar, group_avatar, chatroom_avatar or group_file, see File purposes |
kind | String | image, voice, video or file |
status | String | pending uploading / active usable / blocked / deleted (including failed, canceled and expired uploads) |
url | String | The file address for attachments and group files, the public address for avatars and covers. Uploading and deleted files have addresses too, but can't be downloaded |
thumbnail_url | String | The thumbnail address, only for image attachments and group files, null otherwise; also null for uploading and deleted files |
name | String | The file name given at creation; null if none |
content_type | String | The format detected from the content, such as image/jpeg, see Format checks and image processing. null before completion |
size | Number | Size in bytes: after metadata removal for images, after processing for avatars, as uploaded otherwise; the declared size before completion |
width | Number | Width in pixels for images and avatars; null otherwise |
height | Number | Height in pixels for images and avatars; null otherwise |
owner | String | The owning user's username: for user avatars, the user using it; for client uploads, the uploader; your server can set it with owner when uploading. null if none or deleted |
group_id | String | The group ID for group files, null otherwise |
via | String | How it was uploaded: client, openapi server, or console |
created_at | String | When the upload was created; retention counts from here |
expires_at | String | Expiry: for attachments and group files with retention set, computed from the app's current retention and updated if it changes; null otherwise |
in_use | Boolean | Whether an avatar is currently used by a user, group or chat room profile; false for other purposes |
sha256 | String | SHA-256 (hex) of the stored content, only for image attachments, group files and avatars, null otherwise |
created_by | String | Who uploaded it: the internal ID of the Client Secret used for OpenAPI uploads, the console account ID for console uploads, null for client uploads |
blocked_at | String | When it was blocked; null if not blocked |
blocked_by | String | Who blocked it: system (moderation automatically, or following reviewers' conclusions) / platform; null if not blocked |
block_rule_source | String | Rules behind the block: platform the platform's rules / tenant your app's rules; null if not blocked |
block_reason | String | Why it was blocked; null if not blocked |
deleted_at | String | When it was deleted; null if not deleted |
delete_reason | String | Why it was deleted, see below; null if not deleted |
Values of delete_reason:
| Value | Description |
|---|---|
expired | An attachment or group file passed its retention |
deleted | Deleted by your server or the console, or a group file deleted in a client |
unused | An avatar not used within 24 hours of upload |
released | An avatar replaced 7 days ago, or a cover of a chat room dissolved 7 days ago |
user_deleted | The owning user was deleted |
group_dismissed | The group was dissolved |
upload_failed | Completion checks failed, such as a format not matching the kind |
canceled | The upload was canceled |
upload_expired | The upload wasn't completed in time |
evidence_expired | A blocked file's evidence retention ended |
An image attachment just uploaded:
{
"file_id": "99606093402996736",
"purpose": "attachment",
"kind": "image",
"status": "active",
"url": "https://im.example.com/media/v1/f/99606093402996736/DC0neNm6HBbxGW9bN6g_iA",
"thumbnail_url": "https://im.example.com/media/v1/f/99606093402996736/DC0neNm6HBbxGW9bN6g_iA/thumb",
"name": "photo.jpg",
"content_type": "image/jpeg",
"size": 269803,
"width": 1080,
"height": 720,
"owner": null,
"group_id": null,
"via": "openapi",
"created_at": "2026-10-02T20:39:03.259Z",
"expires_at": "2026-11-01T20:39:03.259Z",
"in_use": false,
"sha256": "6c458b05f583eca6c08cd7296ec4d2811c68cf59cbc3c8b30feeb8cdc45994ef",
"created_by": "99604957107322880",
"blocked_at": null,
"blocked_by": null,
"block_rule_source": null,
"block_reason": null,
"deleted_at": null,
"delete_reason": null
}