val request = QueryChannelsRequest(
filter = Filters.and(
Filters.eq("type", "messaging"),
Filters.`in`("members", listOf("thierry")),
),
offset = 0,
limit = 10,
querySort = QuerySortByField.descByName("lastMessageAt")
).apply {
watch = true
state = true
}
client.queryChannels(request).enqueue { result ->
if (result is Result.Success) {
val channels: List<Channel> = result.value
} else {
// Handle Result.Failure
}
}Querying Channels
Channel lists are a core part of most messaging applications, and our SDKs make them easy to build using the Channel List components. These lists are powered by the Query Channels API, which retrieves channels based on filter criteria, sorting options, and pagination settings.
Here’s an example of how you can query the list of channels:
const filter = { type: "messaging", members: { $in: ["thierry"] } };
const sort = [{ last_message_at: -1 }];
const options = { limit: 15 };
const channels = await chatClient.queryChannels(filter, sort, options);final filter = Filter.in_('members',['thierry']);
final sort = [SortOption("last_message_at", direction: SortOption.DESC)];
final channels = await client.queryChannels(
filter: filter,
sort: sort,
watch: true,
state: true,
).last;
channels.forEach((Channel c) {
print("${c.extraData['name']} ${c.cid}");
});$filter = ['members' => ['$in' => ['elon', 'jack', 'jessie'] ]];
$sort = ['last_message_at' => 1]; // sorting direction (1 or -1)
$options = ['limit' => 10];
$channels = $client->queryChannels($filter,$sort,$options);let controller = chatClient.channelListController(
query: .init(
filter: .and([.equal(.type, to: .messaging), .containMembers(userIds: ["thierry"])]),
sort: [.init(key: .lastMessageAt, isAscending: false)],
pageSize: 10
)
)
controller.synchronize { error in
if let error = error {
// handle error
print(error)
} else {
// access channels
print(controller.channels)
// load more if needed
controller.loadNextChannels(limit: 10) { error in
// handle error / access channels
}
}
}const FFilter Filter = FFilter::And({
FFilter::In(TEXT("members"), {TEXT("thierry")}),
FFilter::Equal(TEXT("type"), TEXT("messaging")),
});
const TArray<FChannelSortOption> SortOptions{{EChannelSortField::LastMessageAt, ESortDirection::Descending}};
Client->QueryChannels(
Filter,
SortOptions,
EChannelFlags::State,
{}, // Pagination options
[](const TArray<UChatChannel*> ReceivedChannels)
{
// Do something with ReceivedChannels
});resp, err := c.QueryChannels(ctx, &QueryOption{
Filter: map[string]interface{}{
"members": map[string]interface{}{
"$in": []string{ "elon", "jack", "jessie" },
},
},
Sort: []*SortOption{{Field: "last_message_at", Direction: 1}}, // sorting direction (1 or -1)
Limit: 10,
})client.query_channels(
{"members": {"$in": ["elon", "jack", "jessie"]}},
{"last_message_at": 1},
limit=10,
)# require 'stream-chat'
client.query_channels({
'members' => { '$in' => ['elon', 'jack', 'jessie'] } },
sort: { 'last_message_at' => 1 },
limit: 10
)await channelClient.QueryChannelsAsync(QueryChannelsOptions.Default
.WithFilter(new Dictionary<string, object>
{
{ "members", new Dictionary<string, object> { { "$in", new[] {"elon", "jack", "jessie"} } } },
})
.WithSortBy(new SortParameter { Field = "last_message_at", Direction = SortDirection.Ascending })
.WithLimit(10));var filters = new List<IFieldFilterRule>
{
// Return only channels where local user is a member
ChannelFilter.Members.In(Client.LocalUserData.UserId),
};
var channels = await Client.QueryChannelsAsync(filters);// Android SDK
FilterObject filter = Filters.and(
Filters.eq("type", "messaging"),
Filters.in("members", Arrays.asList("thierry"))
);
int offset = 0;
int limit = 10;
QuerySortByField<Channel> sort = QuerySortByField.descByName("last_message_at");
int messageLimit = 0;
int memberLimit = 0;
QueryChannelsRequest request = new QueryChannelsRequest(filter, offset, limit, sort, messageLimit, memberLimit)
.withWatch()
.withState();
client.queryChannels(request).enqueue(result -> {
if (result.isSuccess()) {
List<Channel> channels = result.data();
} else {
// Handle result.error()
}
});
// Backend SDK
Channel.list()
.user(user)
.filterCondition("type", "messaging")
.filterConditions(FilterCondition.in("members", "thierry"))
.sort(Sort.builder().field("last_message_at").direction(Direction.DESC).build())
.watch(true)
.state(true)
.request()Query Parameters
| Name | Type | Description | Default | Optional |
|---|---|---|---|---|
| filters | object | Filter criteria for channel fields. See Queryable Fields for available options. | {} | |
| sort | object or array of objects | Sorting criteria based on field and direction. You can sort by last_updated, last_message_at, updated_at, created_at, member_count, unread_count, or has_unread. Direction can be ascending (1) or descending (-1). Multiple sort options can be provided. | [{last_updated: -1}] | |
| options | object | Additional query options. See Query Options. | {} |
An empty filter matches all channels in your application. In production, always include at least members: { $in: [userID] } to return only channels the user belongs to.
The API only returns channels that the user has permission to read. For messaging channels, this typically means the user must be a member. Include appropriate filters to match your channel type’s permission model.
Common Filters
Messaging and Team Channels
For most messaging applications, filter by channel type and membership:
val filter = Filters.and(
Filters.eq("type", "messaging"),
Filters.`in`("members", listOf("thierry")),
)const filter = { members: { $in: ["thierry"] }, type: "messaging" };final filter = Filter.and([
Filter.equal('type', 'messaging'),
Filter.in_('members', ['thierry']),
]);$filter = ['members' => ['$in' => ['thierry']], 'type' => 'messaging'];let currentUserChannels = chatClient.channelListController(
query: .init(
filter: .and([
.equal(.type, to: .messaging),
.containMembers(userIds: [chatClient.currentUserId!])
])
)
)
currentUserChannels.synchronize()const FFilter Filter = FFilter::And({
FFilter::Equal(TEXT("type"), TEXT("messaging")),
FFilter::In(TEXT("members"), {TEXT("thierry")}),
});filter := map[string]interface{}{
"type": "messaging",
"members": map[string]interface{}{
"$in": []string{"thierry"},
},
}filter = {"type": "messaging", "members": {"$in": ["thierry"]}}filter = {"type" => "messaging", "members" => {"$in" => ["thierry"]}}var filter = new Dictionary<string, object>
{
{ "type", "messaging" },
{ "members", new Dictionary<string, object> { { "$in", new[] {"thierry"} } } },
};// Android SDK
FilterObject filter = Filters.and(
Filters.eq("type", "messaging"),
Filters.in("members", Arrays.asList("thierry"))
);
// Backend SDK
Map<String, Object> filter = FilterCondition.and(
FilterCondition.eq("type", "messaging"),
FilterCondition.in("members", "thierry")
);var filters = new List<IFieldFilterRule>
{
ChannelFilter.Type.EqualsTo(ChannelType.Messaging),
ChannelFilter.Members.In(Client.LocalUserData.UserId),
};
var channels = await Client.QueryChannelsAsync(filters);Filtering by Tags
Use filter_tags to categorize and query channels by topic, priority, department, or any custom categorization.
The filter_tags field supports two operators:
$in: Returns channels that have any of the specified tags$eq: Returns channels that have exactly the specified tags (no more, no less)
// Find channels with any of the specified tags
val filter = Filters.`in`("filter_tags", listOf("premium", "vip"))
// Find channels with exactly these tags
val exactFilter = Filters.eq("filter_tags", listOf("support", "urgent"))
val request = QueryChannelsRequest(filter = filter, limit = 30)
client.queryChannels(request).enqueue()// Find channels with any of the specified tags
const filter = { filter_tags: { $in: ["premium", "vip"] } };
// Find channels with exactly these tags
const exactFilter = { filter_tags: { $eq: ["support", "urgent"] } };
const channels = await chatClient.queryChannels(filter, sort);// Find channels with any of the specified tags
$filter = ['filter_tags' => ['$in' => ['premium', 'vip']]];
$channels = $client->queryChannels($filter, $sort);// Find channels with any of the specified tags
let channelListController = chatClient.channelListController(
query: .init(
filter: .in(.filterTags, values: ["premium", "vip"])
)
)
channelListController.synchronize { error in
if let error {
// handle error
}
}// Find channels with any of the specified tags
filter := map[string]interface{}{
"filter_tags": map[string]interface{}{
"$in": []string{"premium", "vip"},
},
}
resp, err := c.QueryChannels(ctx, &QueryOption{
Filter: filter,
Sort: sort,
})# Find channels with any of the specified tags
filter = {"filter_tags": {"$in": ["premium", "vip"]}}
channels = client.query_channels(filter, sort)# Find channels with any of the specified tags
filter = {"filter_tags" => {"$in" => ["premium", "vip"]}}
channels = client.query_channels(filter, sort: sort)// Find channels with any of the specified tags
var filter = new Dictionary<string, object>
{
{ "filter_tags", new Dictionary<string, object> { { "$in", new[] {"premium", "vip"} } } },
};
await channelClient.QueryChannelsAsync(QueryChannelsOptions.Default.WithFilter(filter));
// Backend SDK
Map<String, Object> filter = FilterCondition.in("filter_tags", "premium", "vip");Example use cases for filter tags:
- Support Tiers:
["premium", "standard", "basic"] - Departments:
["sales", "support", "billing"] - Priority Levels:
["urgent", "high", "normal", "low"] - Topics:
["technical", "account", "feature-request"] - Status:
["active", "archived", "pending"]
Combining tags with other filters:
// Find urgent channels assigned to the current agent
const filter = {
filter_tags: { $in: ["urgent"] },
agent_id: currentAgent.id,
type: "messaging",
};
const urgentChannels = await chatClient.queryChannels(filter, [
{ last_message_at: -1 },
]);Limits: Channels can have up to 10 tags, with each tag limited to 255 characters. Tags are automatically sorted and deduplicated.
Channel Queryable Built-In Fields
The following fields can be used in your filter criteria:
| Name | Type | Description | Supported Operators | Example |
|---|---|---|---|---|
| frozen | boolean | Channel frozen status | $eq | false |
| type | string or list of string | Channel type | $in, $eq | messaging |
| id | string or list of string | Channel ID | $in, $eq | general |
| cid | string or list of string | Full channel ID (type:id) | $in, $eq | messaging:general |
| members | string or list of string | User IDs of channel members | $in, $eq | marcelo or [thierry, marcelo] |
| invite | string (pending, accepted, rejected) | Invite status | $eq | pending |
| joined | boolean | Whether the current user has joined the channel | $eq | true |
| muted | boolean | Whether the current user has muted the channel | $eq | true |
| member.user.name | string | Name property of a channel member | $autocomplete, $eq | marc |
| created_by_id | string | ID of the user who created the channel | $eq | marcelo |
| hidden | boolean | Whether the current user has hidden the channel | $eq | false |
| last_message_at | string (RFC3339 timestamp) | Time of the last message | $eq, $gt, $lt, $gte, $lte, $exists | 2021-01-15T09:30:20.45Z |
| member_count | integer | Number of members | $eq, $gt, $lt, $gte, $lte | 5 |
| created_at | string (RFC3339 timestamp) | Channel creation time | $eq, $gt, $lt, $gte, $lte, $exists | 2021-01-15T09:30:20.45Z |
| updated_at | string (RFC3339 timestamp) | Channel update time | $eq, $gt, $lt, $gte, $lte | 2021-01-15T09:30:20.45Z |
| team | string | Team associated with the channel | $eq | stream |
| last_updated | string (RFC3339 timestamp) | Time of last message, or channel creation time if no messages exist | $eq, $gt, $lt, $gte, $lte | 2021-01-15T09:30:20.45Z |
| disabled | boolean | Channel disabled status | $eq | false |
| has_unread | boolean | Whether the user has unread messages (only true supported, max 100 channels) | true | true |
| app_banned | string | Filter by application-banned users (only for 2-member channels) | excluded, only | excluded |
| filter_tags | list of strings | Tags associated with the channel | $in, $eq | [premium, free] |
For best performance, use cid (full channel ID) instead of id when querying by channel identifier. The cid field is indexed for faster lookups.
For supported query operators, see Query Syntax Operators.
The app_banned filter only works on direct message channels with exactly 2 members.
Query Options
| Name | Type | Description | Default | Optional |
|---|---|---|---|---|
| state | boolean | Return channel state | true | ✓ |
| watch | boolean | Subscribe to real-time updates for returned channels | true | ✓ |
| limit | integer | Number of channels to return (max 30) | 10 | ✓ |
| offset | integer | Number of channels to skip (max 1000) | 0 | ✓ |
| message_limit | integer | Messages to include per channel (max 300) | 25 | ✓ |
| member_limit | integer | Members to include per channel (max 100) | 100 | ✓ |
Response
The API returns a list of ChannelState objects containing all information needed to render channels without additional API calls.
ChannelState Fields
| Field Name | Description |
|---|---|
| channel | Channel data |
| messages | Recent messages (based on message_limit) |
| watcher_count | Number of users currently watching |
| read | Read state for up to 100 members, ordered by most recently added (current user’s read state is always included) |
| members | Up to 100 members, ordered by most recently added |
| pinned_messages | Up to 10 most recent pinned messages |
[
{
"id": "f8IOxxbt",
"type": "messaging",
"cid": "messaging:f8IOxxbt",
"last_message_at": "2020-01-10T07:26:46.791232Z",
"created_at": "2020-01-10T07:25:37.63256Z",
"updated_at": "2020-01-10T07:25:37.632561Z",
"created_by": {
"id": "8ce4c6e11118ca103a0a7c633dcf60dd",
"role": "admin",
"created_at": "2019-08-27T17:33:14.442265Z",
"updated_at": "2020-01-10T07:25:36.402819Z",
"last_active": "2020-01-10T07:25:36.395796Z",
"banned": false,
"online": false,
"image": "https://ui-avatars.com/api/?name=mezie&size=192&background=000000&color=6E7FFE&length=1",
"name": "mezie",
"username": "mezie"
},
"frozen": false,
"config": {
"created_at": "2020-01-20T10:23:44.878185331Z",
"updated_at": "2020-01-20T10:23:44.878185458Z",
"name": "messaging",
"typing_events": true,
"read_events": true,
"connect_events": true,
"search": true,
"reactions": true,
"replies": true,
"mutes": true,
"uploads": true,
"url_enrichment": true,
"max_message_length": 5000,
"automod": "disabled",
"automod_behavior": "flag",
"commands": [
{
"name": "giphy",
"description": "Post a random gif to the channel",
"args": "[text]",
"set": "fun_set"
}
]
},
"name": "Video Call"
}
]Pagination
Use limit and offset to paginate through results:
// Get the first 10 channels
val filter = Filters.`in`("members", "thierry")
val offset = 0
val limit = 10
val request = QueryChannelsRequest(filter, offset, limit)
client.queryChannels(request).enqueue { result ->
if (result is Result.Success) {
val channels = result.value
} else {
// Handle Result.Failure
}
}
// Get the second 10 channels
val nextRequest = QueryChannelsRequest(
filter = filter,
offset = 10, // Skips first 10
limit = limit
)
client.queryChannels(nextRequest).enqueue { result ->
if (result is Result.Success) {
val channels = result.value
} else {
// Handle Result.Failure
}
}const filter = { members: { $in: ["thierry"] } };
const sort = { last_message_at: -1 };
// Get channels 11-30
const channels = await authClient.queryChannels(filter, sort, {
limit: 20,
offset: 10,
});final filter = Filter.in_('members', ['thierry']);
final sort = [SortOption("last_message_at", direction: SortOption.DESC)];
final response = await client.queryChannels(
filter: filter,
sort: sort,
paginationParams: PaginationParams(
limit: 20,
offset: 10,
),
);
response.first.forEach((ChannelState f) {
print("${f.channel.extraData['name']} ${f.channel.cid}");
});$filter = ['members' => ['$in' => ['thierry'] ]];
$sort = ['last_message_at' => -1];
$options = ['limit' => 20, 'offset' => 10];
$channels = $client->queryChannels($filter,$sort,$options);let controller = chatClient.channelListController(
query: .init(
filter: .containMembers(userIds: ["thierry"]),
pageSize: 10
)
)
// Get the first 10 channels
controller.synchronize { error in
if let error = error {
// handle error
print(error)
} else {
// Access channels
print(controller.channels)
// Get the next 10 channels
controller.loadNextChannels { error in
// handle error / access channels
print(error ?? controller.channels)
}
}
}const FFilter Filter = FFilter::In(TEXT("members"), {TEXT("thierry")});
const TArray<FChannelSortOption> SortOptions{
{EChannelSortField::LastMessageAt, ESortDirection::Descending},
};
const FPaginationOptions PaginationOptions{
20, // Limit
10, // Offset
};
Client->QueryChannels(
Filter,
SortOptions,
EChannelFlags::State,
PaginationOptions,
[](const TArray<UChatChannel*> ReceivedChannels)
{
// Started watching channels
});resp, err := c.QueryChannels(ctx, &QueryOption{
Filter: map[string]interface{}{
"members": map[string]interface{}{
"$in": []string{ "thierry" },
},
},
Sort: []*SortOption{{Field: "last_message_at", Direction: -1}},
Limit: 20,
Offset: 10,
})client.query_channels(
{"members": {"$in": ["thierry"]}},
{"last_message_at": -1},
limit=20,
offset=10,
)client.query_channels({
'members' => { '$in' => ['thierry'] } },
sort: { 'last_message_at' => -1 },
limit: 20,
offset: 10,
)await channelClient.QueryChannelsAsync(QueryChannelsOptions.Default
.WithFilter(new Dictionary<string, object>
{
{ "members", new Dictionary<string, object> { { "$in", new[] {"thierry"} } } },
})
.WithSortBy(new SortParameter { Field = "last_message_at", Direction = SortDirection.Descending })
.WithLimit(20)
.WithOffset(10);// Android SDK
// Get the first 10 channels
FilterObject filter = Filters.in("members", "thierry");
int offset = 0;
int limit = 10;
QuerySorter<Channel> sort = new QuerySortByField<>();
int messageLimit = 0;
int memberLimit = 0;
QueryChannelsRequest request = new QueryChannelsRequest(filter, offset, limit, sort, messageLimit, memberLimit);
client.queryChannels(request).enqueue(result -> {
if (result.isSuccess()) {
List<Channel> channels = result.data();
} else {
// Handle result.error()
}
});
// Get the second 10 channels
int nextOffset = 10; // Skips first 10
QueryChannelsRequest nextRequest = new QueryChannelsRequest(filter, nextOffset, limit, sort, messageLimit, memberLimit);
client.queryChannels(nextRequest).enqueue(result -> {
if (result.isSuccess()) {
List<Channel> channels = result.data();
} else {
// Handle result.error()
}
});
// Backend SDK
// Get first 10 channels
Map<String, Object> filter = FilterCondition.in("members", "thierry");
Integer offset = 0;
Integer limit = 10;
Integer messageLimit = 0;
Integer memberLimit = 0;
ChannelListRequest req = Channel.list()
.user(testUserRequestObject)
.filterConditions(filter)
.watch(true)
.state(true)
.messageLimit(0)
.memberLimit(0)
ChannelListResponse channels = req.limit(limit).offset(offset).request();
// Get the second 10 channels
Integer nextOffset = 10;
ChannelListResponse nextChannels = req.limit(limit).offset(nextOffset).request();var filters = new List<IFieldFilterRule>
{
// Return only channels where local user is a member
ChannelFilter.Members.In(Client.LocalUserData.UserId),
};
// Pass limit and offset to control pagination
// Limit - records per page
// Offset - records to skip
var channels = await Client.QueryChannelsAsync(filters, limit: 30, offset: 60);Always include members: { $in: [userID] } in your filter to ensure consistent pagination results. Without this filter, channel list changes may cause pagination issues.
Best Practices
Channel Creation and Watching
A channel is not created in the API until one of the following methods is called. Each method has subtle differences:
channel.create();
channel.query();
channel.watch();channelClient.create(memberIds = listOf("user-1", "user-2"), extraData = emptyMap()).enqueue()
channelClient.query(QueryChannelRequest()).enqueue()
channelClient.watch().enqueue()Only one of these is necessary. For example, calling watch automatically creates the channel in addition to subscribing to real-time updates—there’s no need to call create separately.
With queryChannels, a user can watch up to 30 channels in a single API call. This eliminates the need to watch channels individually using channel.watch() after querying. Using queryChannels can substantially decrease API calls, reducing network traffic and improving performance when working with many channels.
Filter Best Practices
Channel lists often form the backbone of the chat experience and are typically one of the first views users see. Use the most selective filter possible:
- Filter by CID is the most performant query you can use
- For social messaging (DMs, group chats), use at minimum
typeandmembers: { $in: [userID] } - Avoid overly complex queries with more than one AND or OR statement
- Use custom fields on channels to simplify filters
- Filtering by type alone is not recommended—always include additional criteria
// Most performant: Filter by CID
const filter = { cid: channelCID };
// Recommended for social messaging
const filter = { type: "messaging", members: { $in: [userID] } };
// Not recommended: type alone
const filter = { type: "messaging" };// Most performant: Filter by CID
val filter = Filters.eq("cid", channelCid)
// Recommended for social messaging
val filter = Filters.and(
Filters.eq("type", "messaging"),
Filters.`in`("members", listOf(userId)),
)
// Not recommended: type alone
val filter = Filters.eq("type", "messaging")If your filter returns more than 50% of channels in the application, consider adding more selective criteria. Contact support if you plan on having millions of channels and need guidance on optimal filters.
Sort Best Practices
Always specify a sort parameter in your query. The default is last_updated (the more recent of created_at and last_message_at).
The most optimized sort options are:
last_updated(default)last_message_at
const sort = { last_message_at: -1 };val sort = QuerySortByField.descByName<Channel>("last_message_at")Sorting on custom fields is possible but has scalability implications—it performs well up to approximately 2,000-3,000 channels. Enterprise customers can contact support for optimization options.
For the full list of supported query operators, see Query Syntax Operators.