---
title: "Create Bulk Actions"
method: POST
path: "/bulk"
tags: ["Bulk Actions"]
---

# Create Bulk Actions

`POST /bulk`

## Request

- Base URL: `https://api.wistia.com/modern`
- URL: `https://api.wistia.com/modern/bulk`
- Auth: HTTP bearer

Submits a batch of up to 1000 create, update, delete, and move actions to be
processed asynchronously. Returns a background job status whose Show
endpoint reports aggregate progress and per-action results, including the
hashed IDs of created records.

Supported resource types are `media`, `folder`, `subfolder`, `channel`,
`channel_episode`, `captions`, and the ten `customization_*` concerns. A
`folder` is a top-level folder (previously called a project); a `subfolder`
is nested inside one and requires `folder_id` and `name` when created. A
`captions` action operates on one caption track -- one media in one
language.

Because caption actions carry SRT contents inline, they are the resource
type most likely to reach the request body limit before the action cap.
Purchasing captions is not available here -- it has its own endpoint.

A `move` action targets one media and accepts a destination `folder_id` and
optional `subfolder_id`. Bulk moves can use different destinations and are
not subject to the Move Media endpoint's 100-item limit or separate throttle.

Player customizations are addressed one concern at a time
(`customization_appearance`, `customization_playback`, and so on), matching
the Update Customizations endpoints; each accepts `update` only, takes the
media's hashed ID as its `id`, and takes the same payload as its
corresponding endpoint. There is no batch equivalent of the broad customize
endpoint, so a batch always states which slice of the player it is changing.

A `media` update payload can also carry a `custom_metadata` object mapping
field keys to the values to set (`null` clears a field; omitted fields are
left untouched). Values are validated against each field's type exactly as
the Set Custom Metadata Field Value endpoint validates them, and each write
is recorded with its actor and source. Requires the custom metadata feature
on the account; without it actions carrying `custom_metadata` fail
individually.

Deleting a folder or subfolder also soft-deletes its media. An account owner
or manager can restore that media from the trash until it purges. To keep the
media when deleting a subfolder, use the Delete Subfolder endpoint; it moves
the media to the folder's root level instead.

Each action in the batch is authorized and processed independently:
failures (including authorization failures) are reported per action and do
not prevent other actions from completing. Media creation is not supported
-- uploads and URL imports have their own endpoints.


## Requires api token with one of the following permissions
```
Read, update & delete anything
```

Tokens with the "Act with a team member's permissions" permission
(`all:delegate_to_contact_permissions` scope) can also be used. Requests
made with such a token are authorized using the permissions of the
contact assigned to the token.

[Expiring access tokens](https://docs.wistia.com/reference/post_expiring-token)
with authorizations cannot use this endpoint: each action is authorized
later, in a background job, as the persisted contact that submitted it,
and a token's authorizations are not carried into that job. Such requests
are forbidden.

## Headers

- `X-Wistia-API-Version` string, required

## Request body

- object: A bulk request, in one of two shapes. Send `actions` when each record needs its own payload, and `job` when the same change applies to every record under a parent. Provide exactly one of them -- a request carrying both is rejected, and so is a request carrying neither. To make a mixed change, send two requests.
  - `actions` object[]: An array of actions to process, one per record. Maximum 1000 actions per request, and the request body must stay under 2 MB -- whichever limit is reached first. An oversized body is rejected with a `413` and no action in it runs. Each action specifies an operation (create, update, delete, or move), a resource type, and the relevant payload or record ID. Use `job` instead when every record takes the same payload.
    - `operation` 'create' | 'update' | 'delete' | 'move', required: The operation to perform. Media creation is not supported here -- uploads and URL imports have their own endpoints. `delete` also soft-deletes media inside a `folder` or `subfolder`. An account owner or manager can restore it from the trash until it purges. `move` applies to `media` only, one action per media. Each action carries its own destination, so a single request can move media into many different folders.
    - `resource_type` 'media' | 'folder' | 'subfolder' | 'channel' | 'channel_episode' | 'captions' | 'customization_access' | 'customization_accessibility' | 'customization_appearance' | 'customization_chapters' | 'customization_engagement' | 'customization_lead_capture' | 'customization_playback' | 'customization_related_media' | 'customization_sharing' | 'customization_thumbnail', required: The type of resource to operate on. `folder` means a top-level folder (previously called a project); use `subfolder` for a folder nested inside one. `captions` operates on a single caption track -- one media in one language. The `customization_*` types each write one concern of a media's player customizations and accept `update` only. Their `id` is the media's hashed ID, and their payload matches the corresponding Update Customizations endpoint (for example, `customization_appearance` takes the same fields as Update Appearance Customizations). Sending a field another concern owns fails that action rather than writing it, so a batch can never quietly overwrite unrelated player settings.
    - `id` string: The hashed ID of the resource. Required for update, delete, and move operations. For `captions` this is the caption track's own ID (the `id` field returned by List Captions), not the media's -- a media can have a track per language.
    - `payload` object: The data for the operation. Required for create, update, and move operations. The accepted fields depend on the resource type and match the corresponding create or update endpoint's request body (for example, a channel_episode create takes the same fields as the Create Channel Episode endpoint, including channel_id). Creating a subfolder requires `folder_id` (the parent folder's hashed ID) and `name`. Creating captions requires `media_id` and `caption_file` (the SRT contents as a string; the multipart file upload the Create Captions endpoint accepts is not available here) and takes an optional `language`, detected from the file when omitted. Updating captions takes `caption_file`; the track's language is fixed by the record. Creating captions for a language that already has a track replaces it, matching the Create Captions endpoint. Moving a media requires `folder_id` (the destination folder's hashed ID) and accepts an optional `subfolder_id`, which must belong to that folder. Omit `subfolder_id` to move the media to the folder's root level. A `customization_*` payload is a partial update of that concern only: just the fields you send are changed, and a field naming another concern's setting fails the action. A `media` update payload can also carry a `custom_metadata` object mapping field keys to the values to set, in the same shapes the Set Custom Metadata Field Value endpoint accepts for each field's type. A `null` value clears that field; fields the object omits are left untouched. Requires the custom metadata feature on the account, and each write is recorded with its actor and source.
  - `job` object: One change applied to many records, named by a parent (`scope`) or listed explicitly (`ids`). The server resolves the target and runs one action per record, so a folder of 400 media takes one job rather than 400 actions. A `scope` resolves to exactly what the matching list endpoint returns for that parent, including its defaults -- so a `folder` scope on `media` reaches media in that folder's subfolders, and includes **archived** media. A job resolves to at most 5000 records. Beyond that it is rejected rather than truncated, so a job never silently acts on part of the set you named -- narrow the scope, or send the records as an actions array. Cannot be used with `create`, which has no record to address, and is not available to external contacts.
    - `operation` 'update' | 'delete' | 'move', required: The operation to apply to every matching record. `create` is not accepted here.
    - `resource_type` 'media' | 'folder' | 'subfolder' | 'channel' | 'channel_episode' | 'captions' | 'customization_access' | 'customization_accessibility' | 'customization_appearance' | 'customization_chapters' | 'customization_engagement' | 'customization_lead_capture' | 'customization_playback' | 'customization_related_media' | 'customization_sharing' | 'customization_thumbnail', required: The type of record to operate on, using the same vocabulary as a single action. Which parents are valid depends on it -- see `scope`.
    - `scope` object: The parent whose records the job applies to. Which parent types are valid depends on the job's `resource_type` and `operation`: - `media` and the `customization_*` types: `account`, `folder`, `subfolder`, `channel`. - `captions` with `update` or `delete`, which address a caption track: `account`, `media`, `folder`, `channel`. - `channel_episode`: `account`, `channel`, `media`. - `subfolder`: `account`, `folder`. - `folder` and `channel`: `account`. An invalid combination is rejected with the valid parents listed.
      - `type` 'account' | 'folder' | 'subfolder' | 'channel' | 'media', required: The kind of parent `id` names. Required, because a hashed ID does not say what it belongs to -- the same value could name a folder or a channel. Use `account` to mean every record the job could reach, with no `id`.
      - `id` string: The parent's hashed ID. Required for every scope type except `account`.
    - `ids` string[]: The records to apply the change to, named explicitly. Use this instead of `scope` when the records do not share a parent -- it is still far cheaper than one action each, since only the ids repeat and the payload is stated once. Give either `scope` or `ids`, never both.
    - `payload` object: The data applied to every matching record, in the same shape a single action's payload takes for this resource type. Required for `update` and `move`.

## Response `202`

Bulk request accepted and queued for processing

- object
  - `message` string, required: A confirmation message that the background job has been queued.
  - `background_job_status` object, required: A background job keeps track of the progress of an asynchronous task, e.g bulk archiving media, translating media, etc.
    - `id` integer, required: The ID of the background job that's been queued for the request.
    - `hashed_id` string, required: The unguessable hashed ID of the background job. Prefer this over the numeric ID when polling for status.
    - `status` 'queued' | 'started' | 'finished' | 'failed', required: The status of the background job that's been queued for the request.

## Other responses

- `400`: Bad request
- `401`: Unauthorized, invalid or missing token
- `403`: Forbidden, token is valid but account does not have access to feature
- `500`: Internal server error

## Changes

- **2026-11-03** `b86cb2a7b420`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/wistia/apis/data-api/changes/bulk/post.md)

---

[API](https://skmtc.dev/wistia/apis/data-api.md) - [All operations](https://skmtc.dev/wistia/apis/data-api/llms.txt) - [OpenAPI document](https://skmtc.dev/wistia/apis/data-api/revisions/b86cb2a7b420?raw)
