# 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:

```json
{
  "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`:

```json
{
  "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](https://getstream.io/docs/platform/query-syntax-operators/), 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

```python label="Python"
# 1) Filter by type
filters = {
    "types": {"$in": ["messaging"]},
}

# 2) Filter by specific CIDs
filters_by_cids = {
    "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](https://getstream.io/chat/docs/node/channel-members/#hide-history-before-a-specific-date).

| 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. |

```json
{
  "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 `addMembers` to 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_before` with any other operation, including
  `addMembers`, 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, or comma-separated list of codes, 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:

- `updateData` only, and mutually exclusive with `data.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.b` addresses key `b` inside object `a`. The parent
  object must already exist on the channel or be written by the same
  `custom_set`. Keys are applied in alphabetical order, so a parent such as
  `expiration` is written before `expiration.version`. This operation never
  creates intermediate objects on its own. A `custom_set` key whose parent object
  is missing on a channel is an error, and so is any key whose parent exists but is not an object
  (`a` is a string and you address `a.b`), for `custom_unset` too. 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_channels` with 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 same `custom_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_set` and `custom_unset` (400).
  Neither can a parent path in one and its child in the other, such as
  `custom_unset: ["expiration"]` with `custom_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 named `team`. It
  does not change the channel's team. This differs from the single-channel
  partial update, where `set` on a built-in property changes that property or is
  rejected. Use `data` to change built-in properties such as `team` or `frozen`.

```json label="REST"
{
  "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:

```python label="Python"
# Note: Batch channel updates are not yet available in the getstream Python SDK.
# Use individual channel.update() calls or the REST API directly.
```

## Webhooks

When an asynchronous batch update is started, a webhook event [`channel_batch_update.started`](https://getstream.io/chat/docs/node/webhook-events/#channel_batch_updatestarted) 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`](https://getstream.io/chat/docs/node/webhook-events/#memberadded) 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`](https://getstream.io/chat/docs/node/webhook-events/#channel_batch_updatecompleted) 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](https://getstream.io/chat/docs/node/webhook-events/#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](https://getstream.io/docs/platform/async-operations/) with the task ID.

```python label="Python"
# Note: Batch channel updates are not yet available in the getstream Python SDK.
# Use individual channel.update() calls or the REST API directly.
```

The response will contain information about the task, including its status, result, and any errors that occurred during the operation.

```json
{
  "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.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/python/batch-updates/](https://getstream.io/chat/docs/python/batch-updates/).