> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://contentful.com/developers/docs/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://contentful.com/_mcp/server. # Bulk entry content operations > Learn how to efficiently create, update, delete, or export entries at scale using bulk entry content operations, reducing API calls and improving performance for large datasets. Bulk entry content operations provide an efficient way to create, update, delete, or export multiple entries simultaneously, reducing the number of API calls required for bulk content management. This feature is particularly useful when working with large datasets, as it significantly reduces the time and complexity required for handling a larger number of entries. The operation is asynchronous and returns immediately with a job ID that you can use to track the progress of the bulk operation. ## Why bulk entry content operations? Traditional entry management requires individual API calls for each entry, which can be time-consuming and inefficient when dealing with hundreds or thousands of entries, leading to several challenges: * **API rate limits**: Creating entries one-by-one can quickly exhaust rate limits, especially for large datasets * **Network overhead**: Each API call introduces latency, making bulk operations slow * **Complexity**: Managing individual requests, error handling, and retry logic adds significant complexity to client code * **Resource consumption**: Multiple HTTP connections consume more resources on both client and server ## Key benefits The bulk entry content operations feature uses an asynchronous processing model to handle large volumes of entries efficiently, providing the following benefits: * **Performance**: Create, update, delete, or export hundreds of entries in seconds instead of minutes * **Efficiency**: Significantly reduce API calls for a large number of operations * **Non-blocking operations**: Your application can continue working while entries are being processed in the background * **Progress tracking**: Real-time visibility into how many entries have been processed * **Reliability**: Built-in error handling with detailed reporting for individual entry failures * **Graceful error handling**: Individual entry failures don't halt the entire operation * **Scalability**: The system can handle multiple concurrent bulk operations across different spaces ## How it works ### 1. Prepare your data Structure your entries as a JSON array in a file (e.g., `entries.json`). For **create**, `sys.id` is optional. For **update**, include `sys.id` and `sys.version` (required; `sys.version` must match the current entry version). * **Create**: Each entry must include `sys.contentType` and `fields`. Omit `sys.id` to auto-generate an entry ID, or include `sys.id` to use a specific ID. If an entry with that ID already exists, the item fails with `EntryExistsError`. * **Update**: Each entry must include `sys.id`, `sys.version` (must match the current version), and `fields`. * **Delete**: Each entry must include only `sys.id`. **Example: Preparing entries for bulk creation** ```json [ { "sys": { "contentType": {"sys": {"id": "article"}} }, "fields": { "title": { "en-US": "Getting Started with Contentful" }, "body": { "en-US": "This is a comprehensive guide to getting started with Contentful." } } }, { "sys": { "id": "my-custom-entry-id", "contentType": {"sys": {"id": "article"}} }, "fields": { "title": { "en-US": "Advanced Content Modeling Techniques" } } } ] ``` The first entry omits `sys.id` (auto-generated). The second supplies `sys.id` to create an entry with that ID. **Example: Preparing entries for bulk deletion** ```json [ {"sys": {"id": "existing-entry-id-1"}}, {"sys": {"id": "existing-entry-id-2"}} ] ``` ### 2. Upload entries file Upload your entries file to `https://upload.contentful.com/spaces/{space_id}/environments/{environment_id}/uploads` **Upload using cURL:** ```bash curl -X POST "https://upload.contentful.com/spaces//environments//uploads" \ -H "Authorization: Bearer " \ -H "Content-Type: application/octet-stream" \ --data-binary @entries.json ``` **Response:** The API returns an `Upload` resource containing the upload ID: ```json { "sys": { "id": "6gbwC4qRtakgsdqUcCPihF", "type": "Upload", "createdAt": "2025-11-27T13:47:57.000Z", "expiresAt": "2025-11-29T00:00:00.000Z", "space": { "sys": { "type": "Link", "linkType": "Space", "id": "ncbxgqx5v0xe" } }, "environment": { "sys": { "type": "Link", "linkType": "Environment", "id": "master" } } } } ``` ### 3. Receive upload ID The API returns an `Upload` resource with an ID. ### 4. Trigger bulk operation Send a POST request to the factory endpoint for the desired action: * **Create**: `POST https://api.contentful.com/spaces/{space_id}/environments/{environment_id}/bulk_operations/entries/create` * **Update**: `POST https://api.contentful.com/spaces/{space_id}/environments/{environment_id}/bulk_operations/entries/update` * **Delete**: `POST https://api.contentful.com/spaces/{space_id}/environments/{environment_id}/bulk_operations/entries/delete` * **Export**: `POST https://api.contentful.com/spaces/{space_id}/environments/{environment_id}/bulk_operations/entries/export` (optional `query` in the body; no upload step) **Trigger a bulk creation using cURL:** ```bash curl -X POST "https://api.contentful.com/spaces//environments//bulk_operations/entries/create" \ -H "Authorization: Bearer " \ -H "Content-Type: application/vnd.contentful.management.v1+json" \ -d '{"upload":{"sys":{"type":"Upload","id":"6gbwC4qRtakgsdqUcCPihF"}}}' ``` **Response:** The API returns a `BulkContentOperation` resource with `sys.status` set to `in_progress`: ```json { "sys": { "id": "bulkOperationId123", "type": "BulkContentOperation", "bulkType": "EntriesCreate", "status": "in_progress", "space": { "sys": { "type": "Link", "linkType": "Space", "id": "ncbxgqx5v0xe" } }, "environment": { "sys": { "type": "Link", "linkType": "Environment", "id": "master" } }, "createdBy": { "sys": { "type": "Link", "linkType": "User", "id": "userId" } }, "createdAt": "2025-11-27T13:50:00.000Z", "updatedAt": "2025-11-27T13:50:00.000Z" }, "payload": { "upload": { "sys": { "type": "Upload", "id": "6gbwC4qRtakgsdqUcCPihF" } } } } ``` ### 5. Receive operation ID The API returns a `BulkContentOperation` resource. Use `sys.id` to poll for status. ### 6. Track progress Poll `https://api.contentful.com/spaces/{space_id}/environments/{environment_id}/bulk_operations/{operation_id}` until `sys.status` is `completed` or `failed`. **Check status using cURL:** ```bash curl -X GET "https://api.contentful.com/spaces//environments//bulk_operations/" \ -H "Authorization: Bearer " ``` **Response when completed:** ```json { "sys": { "id": "bulkOperationId123", "type": "BulkContentOperation", "bulkType": "EntriesCreate", "status": "completed", "space": { "sys": { "type": "Link", "linkType": "Space", "id": "ncbxgqx5v0xe" } }, "environment": { "sys": { "type": "Link", "linkType": "Environment", "id": "master" } }, "createdBy": { "sys": { "type": "Link", "linkType": "User", "id": "userId" } }, "createdAt": "2025-11-27T13:50:00.000Z", "updatedAt": "2025-11-27T13:55:00.000Z" }, "payload": { "upload": { "sys": { "type": "Upload", "id": "6gbwC4qRtakgsdqUcCPihF" } } }, "result": { "items": [ { "status": "succeeded", "entity": { "sys": { "type": "Entry", "id": "newEntryId1" } } }, { "status": "succeeded", "entity": { "sys": { "type": "Entry", "id": "newEntryId2" } } }, { "status": "failed", "error": { "sys": { "type": "Error", "id": "InvalidEntry" }, "message": "Validation error" } } ] } } ``` ### 7. Retrieve results For **create**, **update**, and **delete** jobs, once `sys.status` is `completed`, check `result.items` for per-entry outcomes. Each item has a `status` of `"succeeded"` or `"failed"`. Failed items include an `error` object with a machine-readable `sys.id` and a human-readable `message`. For **export** jobs, use `result.files` instead — see [Bulk export](#bulk-export). ## Error handling It's important to understand the difference between the status of the bulk operation itself and the status of individual entries within that operation. The bulk operation status is available at `sys.status` and reflects the job lifecycle: * `"in_progress"`: The operation is queued or currently being processed * `"completed"`: The operation finished processing (even if some individual entries failed) * `"failed"`: The entire bulk operation failed due to a system error (e.g., invalid upload file, timeout, or server error) When a bulk operation fails at the job level (`sys.status` is `"failed"`), the response includes a top-level `error` field: ```json { "sys": { "id": "bulkOperationId123", "type": "BulkContentOperation", "bulkType": "EntriesCreate", "status": "failed", "space": { "sys": { "type": "Link", "linkType": "Space", "id": "ncbxgqx5v0xe" } }, "environment": { "sys": { "type": "Link", "linkType": "Environment", "id": "master" } }, "createdBy": { "sys": { "type": "Link", "linkType": "User", "id": "userId" } }, "createdAt": "2025-11-27T13:50:00.000Z", "updatedAt": "2025-11-27T13:50:05.000Z" }, "payload": { "upload": { "sys": { "type": "Upload", "id": "6gbwC4qRtakgsdqUcCPihF" } } }, "error": { "sys": { "type": "Error", "id": "NotFound" }, "message": "Upload not found" } } ``` When a **create**, **update**, or **delete** job completes (`sys.status` is `"completed"`), check `result.items` to see which individual entries succeeded and which failed. Each item has: * `status`: `"succeeded"` or `"failed"` * `entity` (on success): minimal `sys` object identifying the created/updated/deleted entry * `error` (on failure): `sys.type`, `sys.id` (error code), and `message` (minimal error shape; no `details`) Individual entry failures typically occur due to validation errors (e.g., missing required fields, invalid field values, or content type mismatches). On bulk **create**, supplying an `sys.id` that already exists returns `EntryExistsError` for that item. ## Updating existing entries To update existing entries, include `sys.id`, `sys.version`, and `fields` in your entries array. `sys.version` is required and must match the current entry version. ```json [ { "sys": { "id": "existing-entry-id-1", "version": 5 }, "fields": { "title": { "en-US": "Updated Article Title" }, "body": { "en-US": "Updated content for the article." } } } ] ``` The workflow for updates is the same as for creates, but use the update endpoint when triggering the bulk operation: ```bash curl -X POST "https://api.contentful.com/spaces//environments//bulk_operations/entries/update" \ -H "Authorization: Bearer " \ -H "Content-Type: application/vnd.contentful.management.v1+json" \ -d '{"upload":{"sys":{"type":"Upload","id":""}}}' ``` > **Info** > > Updates work like PUT requests and completely overwrite entry fields. Fields omitted from the request are removed from the entry. ## Deleting entries in bulk To delete multiple entries at once, prepare a file containing the IDs of the entries to delete: ```json [ {"sys": {"id": "entry-id-1"}}, {"sys": {"id": "entry-id-2"}}, {"sys": {"id": "entry-id-3"}} ] ``` Upload the file the same way as for create/update, then trigger the delete operation: ```bash curl -X POST "https://api.contentful.com/spaces//environments//bulk_operations/entries/delete" \ -H "Authorization: Bearer " \ -H "Content-Type: application/vnd.contentful.management.v1+json" \ -d '{"upload":{"sys":{"type":"Upload","id":""}}}' ``` When completed, check `result.items`. Each successfully deleted entry has `status: "succeeded"` and an `entity` with `sys.type` `DeletedEntry` and the entry `sys.id`: ```json { "sys": { "type": "BulkContentOperation", "id": "2PUlJ3FH0kZac8ZJuQmWkJ", "bulkType": "EntriesDelete", "status": "completed", "createdAt": "2026-06-10T12:33:05.938Z", "updatedAt": "2026-06-10T12:33:18.122Z", "space": { "sys": { "type": "Link", "linkType": "Space", "id": "odkdf1zpemc0" } }, "environment": { "sys": { "type": "Link", "linkType": "Environment", "id": "master" } }, "createdBy": { "sys": { "type": "Link", "linkType": "User", "id": "6Ynia2PFrjsO15piM8eljZ" } } }, "payload": { "upload": { "sys": { "type": "Upload", "id": "6gbwC4qRtakgsdqUcCPihF" } } }, "result": { "items": [ { "status": "succeeded", "entity": { "sys": { "type": "DeletedEntry", "id": "2AXDFG0gkiGup8jp4EBvDN" } } } ] } } ``` ## List bulk operations `GET https://api.contentful.com/spaces/{space_id}/environments/{environment_id}/bulk_operations` returns a cursor-paginated list of bulk operations. Optional query parameters: * `sys.status[in]` — filter by status (`in_progress`, `completed`, `failed`) * `sys.bulkType[in]` — filter by type (`EntriesCreate`, `EntriesUpdate`, `EntriesDelete`, `EntriesExport`) * `sys.createdAt[gte]` / `sys.createdAt[lte]` — filter by creation date * `sys.updatedAt[gte]` / `sys.updatedAt[lte]` — filter by update date * `order` — sort by `sys.createdAt`, `-sys.createdAt`, `sys.updatedAt`, or `-sys.updatedAt` (default: `-sys.createdAt`) * `limit` — page size (1–100) * `pageNext` / `pagePrev` — cursor pagination (mutually exclusive) **Example:** ```bash curl -G "https://api.contentful.com/spaces//environments//bulk_operations" \ -H "Authorization: Bearer " \ --data-urlencode "limit=25" ``` The response includes `items`, `limit`, and optional `pages.next` / `pages.prev` cursors. See the [API reference](/developers/docs/references/content-management-api/bulk-entry-content-operations) for the full response shape. ## Bulk export Export uses `POST .../bulk_operations/entries/export` with an optional `query` in the body to filter the desired entries. No upload step is required. ### 1. Define your query Send an optional `query` object to filter which entries to export. Omit `query` or send `{}` to export all entries in the environment, capped at 10,000 entries. #### Allowed export query parameters Only the following parameters are allowed; all other entry query parameters are rejected. Filters apply to `sys.*` properties and `content_type` only — filtering on `fields.*` or `metadata.*` is not supported. `order` is not supported. | Parameter / operator | Example in `query` | | --------------------------------------------- | ---------------------------------------------- | | `select` | `"select": "sys.id,fields.title"` | | `content_type` | `"content_type": "product"` | | Equality | `"sys.id": "abc123"` | | `[exists]=true` | `"sys.archivedAt[exists]": "true"` | | `[in]` | `"sys.contentType.sys.id[in]": "foo,bar"` | | Date range (`[lt]`, `[lte]`, `[gt]`, `[gte]`) | `"sys.updatedAt[gte]": "2025-01-01T00:00:00Z"` | **Export all entries of a content type** ```json { "query": { "content_type": "product" } } ``` **Export entries of multiple content types** ```json { "query": { "sys.contentType.sys.id[in]": "article,page" } } ``` **Export archived entries** ```json { "query": { "sys.archivedAt[exists]": "true" } } ``` **Export articles updated since a date** ```json { "query": { "content_type": "article", "sys.updatedAt[gte]": "2026-01-01T00:00:00Z", "select": "sys.id,fields.title" } } ``` ### 2. Create the export job ```bash curl -X POST "https://api.contentful.com/spaces//environments//bulk_operations/entries/export" \ -H "Authorization: Bearer " \ -H "Content-Type: application/vnd.contentful.management.v1+json" \ -d '{ "query": { "content_type": "article", "sys.updatedAt[gte]": "2026-01-01T00:00:00Z", "select": "sys.id,fields.title" } }' ``` The API returns a `BulkContentOperation` with `bulkType` `EntriesExport` and `sys.status` `in_progress`. ### 3. Poll until complete Poll `GET .../bulk_operations/{bulk_operation_id}` until `sys.status` is `completed` or `failed` (same endpoint as create, update, and delete jobs). ### 4. Download the export archives When `sys.status` is `completed`, `result.files` contains signed download URLs for one or more ZIP archives. Each item has `url.href` and `url.expiresAt` (URLs expire after 1 hour). A new URL is signed on every `GET`, so download before it expires. **Response when completed:** ```json { "sys": { "type": "BulkContentOperation", "id": "abc123", "bulkType": "EntriesExport", "status": "completed", "createdAt": "2026-06-16T09:00:00.000Z", "updatedAt": "2026-06-16T09:01:30.000Z", "createdBy": { "sys": { "type": "Link", "linkType": "User", "id": "userId" } }, "environment": { "sys": { "type": "Link", "linkType": "Environment", "id": "master" } }, "space": { "sys": { "type": "Link", "linkType": "Space", "id": "spaceId" } } }, "payload": { "query": { "content_type": "blogPost", "sys.createdAt[gte]": "2026-01-01T00:00:00Z" } }, "result": { "files": [ { "url": { "href": "https://results.contentful.com/spaceId_master_abc123/export_archive.zip?Policy=...&Signature=...&Key-Pair-Id=...", "expiresAt": "2026-06-16T21:00:00.000Z" } } ] } } ``` ### Archive contents The export produces a ZIP file. Inside it, everything is placed under a single directory. Directory and data file names are dynamic and unique for each export — use `metadata.json` to discover them and to get the names of the data files to read. ``` {bulkOperationId}/ {bulkOperationId}_0.json ← first data file {bulkOperationId}_1.json ← second data file (if entities span multiple files) ... metadata.json ``` Each `{bulkOperationId}_N.json` file is a JSON array of exported entries, split by a max file size limit. Large exports can have multiple numbered data files. #### metadata.json Describes the full export. Use it to see how many entities were exported, how to interpret the data files, and to verify completeness before processing. ```json { "spaceId": "ncbxgqx5v0xe", "environmentId": "master", "bulkOperationId": "2uArEj87W38CoqXxf4LwiC", "entityType": "entries", "total": 8500, "limit": 10000, "size": 42500000, "files": [ { "name": "2uArEj87W38CoqXxf4LwiC_0.json", "total": 8500, "size": 42500000 } ] } ``` | Field | Description | | ------------ | --------------------------------------------------------------------------- | | `total` | Total number of exported entities across all data files | | `limit` | Max entities the export was configured to return | | `size` | Total uncompressed size of all data files in bytes | | `entityType` | Type of exported entities (`entries`) | | `files` | One entry per data file — `name`, item count (`total`), and `size` in bytes | Read `metadata.json` first to know how many data files to expect, then process each file listed in `files` in order. ## Limitations * Maximum number of entries per request is subject to [technical limits](/developers/docs/platform/technical-limits) (currently 10,000 entries per file) * Create and update: entries must reference valid content types. Upload files follow standard upload size limits * Export: results are capped at 10,000 entries per job and delivered as JSON files inside downloadable ZIP archives * Export: signed download URLs in `result.files[].url` expire after 1 hour * At most one bulk operation can be in flight per space at a time (create, update, delete, or export) For detailed API reference documentation, see the [Bulk entry content operations API reference](/developers/docs/references/content-management-api/bulk-entry-content-operations). > Learn how to efficiently create, update, delete, or export entries at scale using bulk entry content operations, reducing API calls and improving performance for large datasets.