Bulk entry content operations

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

    1[
    2 {
    3 "sys": {
    4 "contentType": {"sys": {"id": "article"}}
    5 },
    6 "fields": {
    7 "title": {
    8 "en-US": "Getting Started with Contentful"
    9 },
    10 "body": {
    11 "en-US": "This is a comprehensive guide to getting started with Contentful."
    12 }
    13 }
    14 },
    15 {
    16 "sys": {
    17 "id": "my-custom-entry-id",
    18 "contentType": {"sys": {"id": "article"}}
    19 },
    20 "fields": {
    21 "title": {
    22 "en-US": "Advanced Content Modeling Techniques"
    23 }
    24 }
    25 }
    26]

    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

    1[
    2 {"sys": {"id": "existing-entry-id-1"}},
    3 {"sys": {"id": "existing-entry-id-2"}}
    4]

2. Upload entries file

Upload your entries file to https://upload.contentful.com/spaces/{space_id}/environments/{environment_id}/uploads

Upload using cURL:

$curl -X POST "https://upload.contentful.com/spaces/<space_id>/environments/<environment_id>/uploads" \
> -H "Authorization: Bearer <cma_token>" \
> -H "Content-Type: application/octet-stream" \
> --data-binary @entries.json

Response:

The API returns an Upload resource containing the upload ID:

1{
2 "sys": {
3 "id": "6gbwC4qRtakgsdqUcCPihF",
4 "type": "Upload",
5 "createdAt": "2025-11-27T13:47:57.000Z",
6 "expiresAt": "2025-11-29T00:00:00.000Z",
7 "space": {
8 "sys": {
9 "type": "Link",
10 "linkType": "Space",
11 "id": "ncbxgqx5v0xe"
12 }
13 },
14 "environment": {
15 "sys": {
16 "type": "Link",
17 "linkType": "Environment",
18 "id": "master"
19 }
20 }
21 }
22}

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:

    $curl -X POST "https://api.contentful.com/spaces/<space_id>/environments/<environment_id>/bulk_operations/entries/create" \
    > -H "Authorization: Bearer <cma_token>" \
    > -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:

    1{
    2 "sys": {
    3 "id": "bulkOperationId123",
    4 "type": "BulkContentOperation",
    5 "bulkType": "EntriesCreate",
    6 "status": "in_progress",
    7 "space": {
    8 "sys": {
    9 "type": "Link",
    10 "linkType": "Space",
    11 "id": "ncbxgqx5v0xe"
    12 }
    13 },
    14 "environment": {
    15 "sys": {
    16 "type": "Link",
    17 "linkType": "Environment",
    18 "id": "master"
    19 }
    20 },
    21 "createdBy": {
    22 "sys": {
    23 "type": "Link",
    24 "linkType": "User",
    25 "id": "userId"
    26 }
    27 },
    28 "createdAt": "2025-11-27T13:50:00.000Z",
    29 "updatedAt": "2025-11-27T13:50:00.000Z"
    30 },
    31 "payload": {
    32 "upload": {
    33 "sys": {
    34 "type": "Upload",
    35 "id": "6gbwC4qRtakgsdqUcCPihF"
    36 }
    37 }
    38 }
    39}

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:

$curl -X GET "https://api.contentful.com/spaces/<space_id>/environments/<environment_id>/bulk_operations/<bulk_operation_id>" \
> -H "Authorization: Bearer <cma_token>"

Response when completed:

1{
2 "sys": {
3 "id": "bulkOperationId123",
4 "type": "BulkContentOperation",
5 "bulkType": "EntriesCreate",
6 "status": "completed",
7 "space": {
8 "sys": {
9 "type": "Link",
10 "linkType": "Space",
11 "id": "ncbxgqx5v0xe"
12 }
13 },
14 "environment": {
15 "sys": {
16 "type": "Link",
17 "linkType": "Environment",
18 "id": "master"
19 }
20 },
21 "createdBy": {
22 "sys": {
23 "type": "Link",
24 "linkType": "User",
25 "id": "userId"
26 }
27 },
28 "createdAt": "2025-11-27T13:50:00.000Z",
29 "updatedAt": "2025-11-27T13:55:00.000Z"
30 },
31 "payload": {
32 "upload": {
33 "sys": {
34 "type": "Upload",
35 "id": "6gbwC4qRtakgsdqUcCPihF"
36 }
37 }
38 },
39 "result": {
40 "items": [
41 {
42 "status": "succeeded",
43 "entity": {
44 "sys": {
45 "type": "Entry",
46 "id": "newEntryId1"
47 }
48 }
49 },
50 {
51 "status": "succeeded",
52 "entity": {
53 "sys": {
54 "type": "Entry",
55 "id": "newEntryId2"
56 }
57 }
58 },
59 {
60 "status": "failed",
61 "error": {
62 "sys": {
63 "type": "Error",
64 "id": "InvalidEntry"
65 },
66 "message": "Validation error"
67 }
68 }
69 ]
70 }
71}

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.

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:

1{
2 "sys": {
3 "id": "bulkOperationId123",
4 "type": "BulkContentOperation",
5 "bulkType": "EntriesCreate",
6 "status": "failed",
7 "space": {
8 "sys": {
9 "type": "Link",
10 "linkType": "Space",
11 "id": "ncbxgqx5v0xe"
12 }
13 },
14 "environment": {
15 "sys": {
16 "type": "Link",
17 "linkType": "Environment",
18 "id": "master"
19 }
20 },
21 "createdBy": {
22 "sys": {
23 "type": "Link",
24 "linkType": "User",
25 "id": "userId"
26 }
27 },
28 "createdAt": "2025-11-27T13:50:00.000Z",
29 "updatedAt": "2025-11-27T13:50:05.000Z"
30 },
31 "payload": {
32 "upload": {
33 "sys": {
34 "type": "Upload",
35 "id": "6gbwC4qRtakgsdqUcCPihF"
36 }
37 }
38 },
39 "error": {
40 "sys": {
41 "type": "Error",
42 "id": "NotFound"
43 },
44 "message": "Upload not found"
45 }
46}

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.

1[
2 {
3 "sys": {
4 "id": "existing-entry-id-1",
5 "version": 5
6 },
7 "fields": {
8 "title": {
9 "en-US": "Updated Article Title"
10 },
11 "body": {
12 "en-US": "Updated content for the article."
13 }
14 }
15 }
16]

The workflow for updates is the same as for creates, but use the update endpoint when triggering the bulk operation:

$curl -X POST "https://api.contentful.com/spaces/<space_id>/environments/<environment_id>/bulk_operations/entries/update" \
> -H "Authorization: Bearer <cma_token>" \
> -H "Content-Type: application/vnd.contentful.management.v1+json" \
> -d '{"upload":{"sys":{"type":"Upload","id":"<upload_id>"}}}'
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:

1[
2 {"sys": {"id": "entry-id-1"}},
3 {"sys": {"id": "entry-id-2"}},
4 {"sys": {"id": "entry-id-3"}}
5]

Upload the file the same way as for create/update, then trigger the delete operation:

$curl -X POST "https://api.contentful.com/spaces/<space_id>/environments/<environment_id>/bulk_operations/entries/delete" \
> -H "Authorization: Bearer <cma_token>" \
> -H "Content-Type: application/vnd.contentful.management.v1+json" \
> -d '{"upload":{"sys":{"type":"Upload","id":"<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:

1{
2 "sys": {
3 "type": "BulkContentOperation",
4 "id": "2PUlJ3FH0kZac8ZJuQmWkJ",
5 "bulkType": "EntriesDelete",
6 "status": "completed",
7 "createdAt": "2026-06-10T12:33:05.938Z",
8 "updatedAt": "2026-06-10T12:33:18.122Z",
9 "space": {
10 "sys": {
11 "type": "Link",
12 "linkType": "Space",
13 "id": "odkdf1zpemc0"
14 }
15 },
16 "environment": {
17 "sys": {
18 "type": "Link",
19 "linkType": "Environment",
20 "id": "master"
21 }
22 },
23 "createdBy": {
24 "sys": {
25 "type": "Link",
26 "linkType": "User",
27 "id": "6Ynia2PFrjsO15piM8eljZ"
28 }
29 }
30 },
31 "payload": {
32 "upload": {
33 "sys": {
34 "type": "Upload",
35 "id": "6gbwC4qRtakgsdqUcCPihF"
36 }
37 }
38 },
39 "result": {
40 "items": [
41 {
42 "status": "succeeded",
43 "entity": {
44 "sys": {
45 "type": "DeletedEntry",
46 "id": "2AXDFG0gkiGup8jp4EBvDN"
47 }
48 }
49 }
50 ]
51 }
52}

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:

    $curl -G "https://api.contentful.com/spaces/<space_id>/environments/<environment_id>/bulk_operations" \
    > -H "Authorization: Bearer <cma_token>" \
    > --data-urlencode "limit=25"

The response includes items, limit, and optional pages.next / pages.prev cursors. See the API reference 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 / operatorExample 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

1{
2 "query": {
3 "content_type": "product"
4 }
5}

Export entries of multiple content types

1{
2 "query": {
3 "sys.contentType.sys.id[in]": "article,page"
4 }
5}

Export archived entries

1{
2 "query": {
3 "sys.archivedAt[exists]": "true"
4 }
5}

Export articles updated since a date

1{
2 "query": {
3 "content_type": "article",
4 "sys.updatedAt[gte]": "2026-01-01T00:00:00Z",
5 "select": "sys.id,fields.title"
6 }
7}

2. Create the export job

$curl -X POST "https://api.contentful.com/spaces/<space_id>/environments/<environment_id>/bulk_operations/entries/export" \
> -H "Authorization: Bearer <cma_token>" \
> -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:

1{
2 "sys": {
3 "type": "BulkContentOperation",
4 "id": "abc123",
5 "bulkType": "EntriesExport",
6 "status": "completed",
7 "createdAt": "2026-06-16T09:00:00.000Z",
8 "updatedAt": "2026-06-16T09:01:30.000Z",
9 "createdBy": {
10 "sys": {
11 "type": "Link",
12 "linkType": "User",
13 "id": "userId"
14 }
15 },
16 "environment": {
17 "sys": {
18 "type": "Link",
19 "linkType": "Environment",
20 "id": "master"
21 }
22 },
23 "space": {
24 "sys": {
25 "type": "Link",
26 "linkType": "Space",
27 "id": "spaceId"
28 }
29 }
30 },
31 "payload": {
32 "query": {
33 "content_type": "blogPost",
34 "sys.createdAt[gte]": "2026-01-01T00:00:00Z"
35 }
36 },
37 "result": {
38 "files": [
39 {
40 "url": {
41 "href": "https://results.contentful.com/spaceId_master_abc123/export_archive.zip?Policy=...&Signature=...&Key-Pair-Id=...",
42 "expiresAt": "2026-06-16T21:00:00.000Z"
43 }
44 }
45 ]
46 }
47}

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.

1{
2 "spaceId": "ncbxgqx5v0xe",
3 "environmentId": "master",
4 "bulkOperationId": "2uArEj87W38CoqXxf4LwiC",
5 "entityType": "entries",
6 "total": 8500,
7 "limit": 10000,
8 "size": 42500000,
9 "files": [
10 { "name": "2uArEj87W38CoqXxf4LwiC_0.json", "total": 8500, "size": 42500000 }
11 ]
12}
FieldDescription
totalTotal number of exported entities across all data files
limitMax entities the export was configured to return
sizeTotal uncompressed size of all data files in bytes
entityTypeType of exported entities (entries)
filesOne 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 (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.