Stream Chat allows you to upload images, videos, and other files to the Stream CDN or your own CDN. Uploaded files can be used as message attachments, user avatars, or channel images.
Stream's UI SDKs (React, React Native, Flutter, SwiftUI, Jetpack Compose, etc.) handle file uploads automatically through their message composer components. The upload process, progress tracking, and attachment handling are built into these components. Use the methods described on this page only if you need custom upload behavior or are building a custom UI.
Files uploaded to a channel can be attached to messages. You can either upload a file first and then attach it to a message, or let the SDK handle the upload when sending a message with attachments.
// Upload an image to the channel// Note: actual file upload uses multipart form data$response = $client->uploadChannelImage("messaging", "general", new Models\UploadChannelRequest( // file upload parameters));
// Upload imagevar imgResp = await chat.UploadChannelImageAsync("messaging", channelId, new UploadChannelRequest { File = "/path/to/image.png", User = new OnlyUserID { ID = userId } });// Upload filevar fileResp = await chat.UploadChannelFileAsync("messaging", channelId, new UploadChannelFileRequest { File = "/path/to/file.pdf", User = new OnlyUserID { ID = userId } });
Files not tied to a specific channel can be used for user avatars and channel images, i.e. stored in the image or image_url field of a user or channel object.
Like channel attachments, standalone Stream CDN URLs expire (see Access Control and Link Expiration). They are refreshed automatically only when stored in the image or image_url field of a user or channel object, which are re-signed when the user or channel is retrieved (e.g. when querying users or channels). A Stream CDN URL kept anywhere else (a different custom field, your own database, etc.) is not refreshed and stops working once it expires. For those use cases, do not use the Stream CDN.
val client = ChatClient.instance()// Upload an imageval uploadResult = client.uploadImage( file = imageFile, progressCallback = progressCallback,).await()when (uploadResult) { is Result.Success -> { val imageUrl = uploadResult.value.file // Use the URL (e.g., update user avatar) client.updateUser(user.copy(image = imageUrl)).await() } is Result.Failure -> { // Handle error }}
let channelId = ChannelId(type: .messaging, id: "general")let channelController = chatClient.channelController(for: channelId)channelController.uploadAttachment( localFileURL: imageLocalFileUrl, type: .image, progress: { progressValue in // Track upload progress (0.0 to 1.0) }, completion: { result in switch result { case .success(let uploadedFile): // Use the URL (e.g., update user avatar) chatClient.currentUserController().updateUserData( imageURL: uploadedFile.remoteURL ) case .failure(let error): // Handle upload error } })
final client = StreamChatClient('api-key');// Upload an imagefinal image = AttachmentFile(path: 'imagePath/image.png', size: 1024);final response = await client.uploadImage( image, onUploadProgress: (count, total) { // Update progress UI },);final imageUrl = response.file;// Use the URL (e.g., update user avatar)final user = User(id: 'user-id', image: imageUrl);await client.updateUser(user);
// Uploading standalone files via a client-level endpoint is not yet available in the Unity SDK.// Please let us know if you'd like this feature implemented: https://github.com/GetStream/stream-chat-unity/issues// As a workaround, upload via channel.UploadImageAsync / channel.UploadFileAsync// and reuse the returned URL when upserting a user with Client.UpsertUsersAsync.
Stream allows every extension and MIME type by default. Your app can be configured to be more restrictive through file_upload_config and image_upload_config:
Setting
Effect
allowed_file_extensions
Only these extensions are accepted. Everything else is rejected.
blocked_file_extensions
These extensions are rejected. Everything else is accepted.
allowed_mime_types
Only these MIME types are accepted. Follows the type/subtype format.
blocked_mime_types
These MIME types are rejected.
size_limit
Maximum accepted size in bytes. 0 falls back to the 100 MB default.
Set them in the Dashboard under Chat Overview > Upload Configuration, or with the App Settings endpoint.
Upload restrictions live on the app, not on the device. A client SDK cannot read or change them, so the same limits apply to every upload from every SDK, and a client integration has to handle the rejection rather than prevent it.
Rejection happens at upload time, before the message is sent. The upload call fails and returns no attachment URL, so the message never gets the attachment. Handle the failure and tell the user which rule they hit. A file over the size limit returns HTTP 413 with Stream code 22, listed in API error codes.
If your app restricts uploads, mirror the same rules in your own file picker. Users get immediate feedback instead of waiting for an upload to fail.
Stream CDN URLs include a signature that validates access to the file. Only channel members can access files uploaded to that channel.
Behavior
Description
Access control
URLs are signed and only accessible by channel members
Link expiration
URLs expire after 14 days
Automatic refresh
Message attachment links are refreshed when messages are retrieved (e.g., when querying a channel). Standalone uploads are refreshed only when stored in image / image_url on a user or channel object, and re-signed when that user or channel is retrieved.
Manual refresh
Call getMessage to retrieve fresh URLs for expired attachments
To check when a link expires, examine the Expires query parameter in the URL (Unix timestamp).
Append query parameters to Stream CDN image URLs to resize images on the fly.
Parameter
Type
Values
Description
w
number
Width in pixels
h
number
Height in pixels
resize
string
clip, crop, scale, fill
Resizing mode
crop
string
center, top, bottom, left, right
Crop anchor position
Images can only be resized if the source image has 16,800,000 pixels or fewer. An image of 4000x4000 pixels (16,000,000) would be accepted, but 4100x4100 (16,810,000) would fail.
val client = ChatClient.Builder("api-key", context) .fileUploader(MyCustomFileUploader()) .build()
ChatClient client = new ChatClient.Builder("api-key", context) .fileUploader(new MyCustomFileUploader()) .build();
class CustomCDN: CDNClient { static var maxAttachmentSize: Int64 { 20 * 1024 * 1024 } func uploadAttachment( _ attachment: AnyChatMessageAttachment, progress: ((Double) -> Void)?, completion: @escaping (Result<URL, Error>) -> Void ) { // Upload to your CDN // Call progress() to report upload progress // Call completion() with the result }}// Assign to ChatClientConfigconfig.customCDNClient = CustomCDN()
final client = StreamChatClient( 'api-key', attachmentFileUploaderProvider: (httpClient) => MyCustomFileUploader(httpClient),);
// Upload to your CDN and obtain the file URLvar fileUrl = "file-url-to-your-cdn";await channel.SendNewMessageAsync(new StreamSendMessageRequest{ Text = "Message with file attachment", Attachments = new List<StreamAttachmentRequest> { new StreamAttachmentRequest { AssetUrl = fileUrl, } }});
chat.uploadChannelFile(channelType, channelId, UploadChannelFileRequest.builder() // file data .build()).execute();