Batch Updates
You can perform batch updates on multiple channels at once. This is useful for making changes to a large number of channels without having to update each one individually, which could result in potentially thousands of API calls.
Batch updates run asynchronously by default: the request returns a task ID, and the updates are processed in the background. For bounded channel data updates, you can instead request synchronous execution.
When an asynchronous update is requested, a task will be created and the task ID will be returned in the response. You can use this task ID to check the status of the update operation.
Synchronous channel data updates
Set synchronous: true on a server-side PUT /channels/batch request to wait for a bounded updateData database write before continuing. This mode supports channel data updates only; member operations remain asynchronous.
Provide a root filter.cids using $eq or $in with 1 to 100 CIDs. A types filter can further restrict that selection. Types-only filters, root $or or $and, and more than 100 CIDs return HTTP 400. Split larger selections into separate requests. Requests with no fields to update or no matching channels also return HTTP 400.
For example, this request freezes two channels:
{
"operation": "updateData",
"synchronous": true,
"filter": {
"cids": { "$in": ["messaging:channel-a", "messaging:channel-b"] }
},
"data": { "frozen": true }
}A synchronous success includes a positive success_channels_count and no task_id:
{
"success_channels_count": 2
}The count is the number of matching channels loaded for the completed update, excluding missing and duplicate CIDs. It is not an affected-row count: a concurrent hard deletion can reduce the number of rows written.
Always inspect the response shape. If task_id is present, the operation was queued and you must wait for the asynchronous task before continuing, even if you sent synchronous: true. An older API node can ignore the new field during a rollout or rollback. Use an SDK release that exposes both synchronous and success_channels_count.
Database writes for one synchronous request commit together. Event delivery remains best effort and is not guaranteed by a successful response. Synchronous updates emit per-channel update events but do not emit channel_batch_update.started or channel_batch_update.completed task events. Disabling a channel can still schedule background thread read-state work.
A timeout does not prove the write failed: the database may already have committed. Read the channel state before deciding to retry. Repeating the same idempotent update may emit duplicate events. Concurrent channel updates are not serialized with this operation.
How to target channels
In order to perform batch updates, you need to target which channels you want to update.
You can do this by providing a filter that matches the channels you want to update.
The filters that are supported for batch updates, using the standard query operator syntax, are:
| Name | Description | Available Operators |
|---|---|---|
cids |
Filters channels by Channel ID (CID) in the format type:id (e.g., "messaging:channel1"). Must use explicit operators. Direct arrays are not supported. |
$eq: Single CID value (e.g., {"cids": {"$eq": "messaging:channel1"}}). $in: Multiple CID values (e.g., {"cids": {"$in": ["messaging:channel1", "livestream:channel2"]}}) |
types |
Filters channels by channel type (e.g., "messaging", "livestream", "team"). Must use explicit operators. Direct arrays are not supported. |
$eq: Single channel type (e.g., {"types": {"$eq": "messaging"}}). $in: Multiple channel types (e.g., {"types": {"$in": ["messaging", "livestream"]}}) |
Filter examples
// 1) Filter by type
const filters = {
types: { $in: ["messaging"] },
};
// 2) Filter by specific CIDs
const filtersByCIDS = {
channel_cids: {
$in: [
"messaging:3b11838a-7734-4ece-8547-4b8524257671",
"messaging:a266bee6-dc3c-4188-a37d-e554d4bfac34",
"messaging:40fef12a-0b7c-4bcf-bd97-3ddf604efed5",
"messaging:2a58963e-d769-4ce3-9309-bff93c14db57",
],
},
};Supported operations
You can perform different operations on the channels but only once at a time. The supported operations are:
| Operation Name | Description | Parameters |
|---|---|---|
| addMembers | Add members to the channels. | members |
| addMembersHideHistory | Add members without exposing older messages. | members, hide_history_before |
| removeMembers | Remove members from the channels. | members |
| addModerators | Add moderators to the channels. | members |
| demoteModerators | Remove moderator status from members in the channels. | members |
| hide | Hide the channels for members. | members |
| show | Show the channels for members. | members |
| archive | Archive the channels for members. | members |
| unarchive | Unarchive the channels for members. | members |
| updateData | Update the channel data for the channels. | channelData |
| assignRoles | Assign roles to members in the channels. | members |
| inviteMembers | Send invites to users to join the channels. | members |
Hiding history from added members
The addMembersHideHistory operation requires a hide_history_before timestamp.
New memberships receive this cutoff in every channel matched by the filter, so
those members cannot see messages created before that time. It is the batch
equivalent of the hide_history_before option when
adding members.
| Property | Type | Description |
|---|---|---|
hide_history_before |
string (RFC 3339) | Required. Hides messages created before this time from the members being added. Must be in the past. |
{
"operation": "addMembersHideHistory",
"filter": {
"cids": { "$in": ["messaging:channel-1", "messaging:channel-2"] }
},
"members": [{ "user_id": "jane" }],
"hide_history_before": "2024-01-01T10:00:00Z"
}Things worth knowing:
- The operation and cutoff are required together. Use
addMembersto add members without a history cutoff. - It applies only to memberships this operation creates or restores. A user who
is already a member of a matched channel is unaffected, even if their ID is
in
members. - When a soft-deleted membership is restored, the requested cutoff is applied only if that membership does not already have one. If it already has cutoff A, it keeps A instead of using the requested cutoff B.
- The timestamp must be in the past; a future value is rejected with
hide_history_before must be in the past. - Sending
hide_history_beforewith any other operation, includingaddMembers, is rejected rather than silently ignored. - This operation does not replace or clear a cutoff that a membership already carries.
Channel data update properties
When using the updateData operation, you can update the following channel properties. All properties are optional - only the provided values will be updated.
| Property | Type | Description |
|---|---|---|
frozen |
boolean | Freeze the channel to prevent new messages |
disabled |
boolean | Disable the channel |
custom |
object | Custom data fields for the channel |
team |
string | Team ID to assign the channel to |
config_overrides |
object | Override channel type configuration settings |
auto_translation_enabled |
boolean | Enable automatic message translation |
auto_translation_language |
string | Language code for auto translation |
Partial custom updates
custom replaces the whole custom object: every key you do not send is deleted
from every matched channel. When you only want to change some keys, use
custom_set and custom_unset instead. They merge the named keys into each
channel's existing custom data.
Both live at the root of the request, next to operation and filter, not
inside data.
| Property | Type | Description |
|---|---|---|
custom_set |
object | Keys to merge into each matched channel's existing custom object |
custom_unset |
array of strings | Keys to delete from each matched channel's existing custom object |
Batch custom merges are not safe against concurrent custom-data writes. The batch reads each channel before writing the merged object, so another request that updates custom data after that read can have its changes overwritten. Avoid concurrent custom-data writes while the batch is running. When you need a concurrency-safe merge, use the single-channel partial-update endpoint for each channel instead.
Rules:
updateDataonly, and mutually exclusive withdata.custom. A request carrying both is rejected with a 400, because "replace everything" and "change these keys" have no combined meaning.- Keys are dot-paths:
a.baddresses keybinside objecta. The parent object must already exist on the channel or be written by the samecustom_set. Keys are applied in alphabetical order, so a parent such asexpirationis written beforeexpiration.version. This operation never creates intermediate objects on its own. Acustom_setkey whose parent object is missing on a channel is an error, and so is any key whose parent exists but is not an object (ais a string and you addressa.b), forcustom_unsettoo. A merge that pushes the channel's custom data past the 5 KB limit is an error as well. - A failing channel fails the chunk it was batched into, not just itself. The
batch is split into chunks of up to 25 channels, and each chunk is applied as a
unit. If one channel in a chunk fails any of the checks above, every CID in that
chunk is reported under
failed_channelswith the same reason and none of them is modified, including the channels that would have merged cleanly. Other chunks in the same batch still apply. Plan for this when patching a large set: one channel missing a parent object can cost you 25 channels' worth of the update, so make sure the parent exists everywhere you are targeting, set the parent in the samecustom_set, or narrow the filter. - Deleting a key that does not exist is a no-op, not an error.
- The same key cannot appear in both
custom_setandcustom_unset(400). Neither can a parent path in one and its child in the other, such ascustom_unset: ["expiration"]withcustom_set: {"expiration.version": 2}(400). - Keys always go into custom data, even when they match a built-in channel
property.
custom_set: {"team": "x"}stores a custom key namedteam. It does not change the channel's team. This differs from the single-channel partial update, whereseton a built-in property changes that property or is rejected. Usedatato change built-in properties such asteamorfrozen.
{
"operation": "updateData",
"filter": { "types": { "$in": ["messaging"] } },
"custom_set": { "campaign": "summer-2026", "expiration.version": 2 },
"custom_unset": ["legacy_owner"]
}Given a channel whose custom is
{"campaign": "spring-2026", "expiration": {"version": 1}, "legacy_owner": "u1", "color": "blue"},
the request above leaves
{"campaign": "summer-2026", "expiration": {"version": 2}, "color": "blue"};
color is untouched, which a custom replace would have removed.
Config overrides
The config_overrides object allows you to override the default channel type configuration for specific channels:
| Property | Type | Description |
|---|---|---|
typing_events |
boolean | Enable/disable typing indicators |
reactions |
boolean | Enable/disable message reactions |
replies |
boolean | Enable/disable message replies (threads) |
quotes |
boolean | Enable/disable message quotes |
uploads |
boolean | Enable/disable file uploads |
url_enrichment |
boolean | Enable/disable URL preview enrichment |
max_message_length |
integer | Maximum message length (1-20000) |
blocklist |
string | Name of the blocklist to apply |
blocklist_behavior |
string | Blocklist behavior: flag or block |
grants |
object | Permission grants modifiers |
commands |
array | List of enabled command names |
Most of the operations require additional parameters to be specified, such as the members to add or remove, or the channelData to update.
We've prepared convenience methods for all operations, some examples are shown below:
// Add members
const updater = client.channelBatchUpdater();
const filter = {
types: {
$in: ["messaging"],
},
};
const members = ["user-123"];
const resp = await updater.addMembers(filter, members);
// Update channel data
const updater = client.channelBatchUpdater();
const filter = {
types: {
$in: ["messaging", "team"],
},
};
const data = {
frozen: true,
custom: {
color: "blue",
},
};
const resp = await updater.updateData(filter, data);Webhooks
When an asynchronous batch update is started, a webhook event channel_batch_update.started will be triggered.
Additionally, for each channel that is updated, the corresponding webhook events will be triggered as well.
For example, if members are added to channels, the member.added event will be triggered for each channel that is updated.
When the asynchronous batch update operation is completed, a webhook event channel_batch_update.completed will be triggered. This event will contain the task ID and the status of the operation (success or partial failure). If some channels failed to update, they are listed in failed_channels, grouped by reason - see the partial failure example.
For the format of the status please see next section.
Status
You can check the status of a batch update operation by using the task ID returned when the operation was started.
To get the status of the task, you use the Get Task endpoint with the task ID.
const taskResponse = await client.getTask({ id: response.task_id });The response will contain information about the task, including its status, result, and any errors that occurred during the operation.
{
"task_id": "23685bb3-d1c7-492b-a02a-3dbaa12855e1",
"status": "completed",
"created_at": "2025-12-13T10:25:15.856428Z",
"updated_at": "2025-12-13T10:35:00.512601Z",
"result": {
"operation": "show",
"status": "completed",
"success_channels_count": 1080,
"task_id": "23685bb3-d1c7-492b-a02a-3dbaa12855e1",
"batch_created_at": "2025-12-13T10:25:16Z",
"failed_channels": [
{
"reason": "cannot invite members to the distinct channel",
"cids": ["messaging:550e8400-e29b-41d4-a716-446655440000"]
},
{
"reason": "user not found: user_id 'user_12345' does not exist",
"cids": ["team:7c9e6679-7425-40de-944b-e07fc1f90ae7"]
}
],
"finished_at": "2025-12-13T10:35:00.512579053Z"
},
"duration": "36.23ms"
}Poll the inner result.status, not the top-level status. The top-level
field describes the task that fans the work out, and it flips to completed as
soon as that fan-out returns while the channels are still being updated. The
batch itself is finished only once result.status is completed and
result.finished_at is set; until then success_channels_count and
failed_channels are not final. Reading a channel back on the top-level field
alone will often show you its old data.
You can try to re apply the operation on the failed channels again by creating a new task with the same operation and just the CIDs of the failed channels.