---
title: Batch operations
description: Create, update, delete or restore many records in one call. By default one invalid item fails the whole batch; with rollback-method=1 a batch can succeed in part.
canonical: https://developers.coevera.com/concepts/batch-operations/
section: concepts
verification: behaviour
lastVerified: 2026-10-15
publisher: Coevera
---

# Batch operations

**Answer:** By default, one invalid item fails the whole batch with HTTP 422 and nothing is written; the error does not say which item failed. With rollback-method=1 the valid items are written, the batch answers HTTP 200 with success: false, and data holds one entry per item in input order: the record's ID or an error.

A batch writes many records in one request. It is faster than one request per record, and it
decides what happens to the valid items when one of them is invalid: read that part before you
send your first batch.

## Batch endpoints

| Endpoint                                | Body                | Does                                                          |
| --------------------------------------- | ------------------- | ------------------------------------------------------------- |
| `POST /entities/{Entity}/batch-modify`  | An array of records | Creates the items without an `id`, updates the items with one |
| `POST /entities/{Entity}/batch-delete`  | An array of IDs     | Soft-deletes the records                                      |
| `POST /entities/{Entity}/batch-restore` | An array of IDs     | Restores soft-deleted records                                 |

In GraphQL, `bulkUpdate<Entity>` mutations such as `bulkUpdateContact` create and update in
one call. Every entity's batch operations are on its reference page, such as
[`POST /entities/Contacts/batch-modify`](/reference/rest/contacts/#batch-modify) and
[`bulkUpdateContact`](/reference/graphql/contact/#m-bulkUpdateContact).

The API sets no batch size limit. Send **up to 100 records per request** for a quick response:
a batch takes about 40 ms per record, so 500 records take about 20 s. A larger batch is not
refused, but it answers more slowly, and by default one invalid item throws away more work.

Deleting a record leaves its relation records active. Deleting contacts or accounts leaves their
`ContactAccountRelations` in place, still pointing at the deleted records, and deleting a
`CloudObjectRelation` leaves its `CloudObject`, the file. Delete those on their own.

## By default, all or nothing

The sample sends three contacts; the third has no owner. Sent without `rollback-method`, the
batch fails as a whole:

**REST · cURL**

```bash
# No --fail-with-body: the 422 is the expected answer here.
curl -sS -w '\nHTTP %{http_code}\n' "$COEVERA_BASE/entities/Contacts/batch-modify" \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
  -H "Content-Type: application/json" \
  -d "$CONTACTS"
```

**REST · TypeScript**

```ts
const strict = await call("/entities/Contacts/batch-modify", contacts);
console.log(`Default rollback: HTTP ${strict.status}`, JSON.stringify(strict.body, null, 2));
```

**REST · Python**

```python
strict = session.post(f"{BASE}/entities/Contacts/batch-modify", json=contacts, timeout=60)
print(f"Default rollback: HTTP {strict.status_code}", json.dumps(strict.json(), indent=2))
```

**GraphQL · cURL**

```bash
batch ROLLBACK_ALL | gql
echo
```

**GraphQL · TypeScript**

```ts
const strict = await gql(ADD_CONTACTS, { input: contacts, rollbackMethod: "ROLLBACK_ALL" });
console.log("ROLLBACK_ALL:", JSON.stringify(strict, null, 2));
```

**GraphQL · Python**

```python
strict = gql(ADD_CONTACTS, {"input": contacts, "rollbackMethod": "ROLLBACK_ALL"})
print("ROLLBACK_ALL:", json.dumps(strict, indent=2))
```

The API answers HTTP 422 with code 40000 `ERROR_ENTITY_VALIDATION` and writes nothing, not even
the two valid contacts. The error carries `entity_index: null` and an `entity_id` the server made
up for the new record, so it does not tell you which input item failed:

**Recorded response · REST · 422 Unprocessable Entity**

```json
{
  "code": 40000,
  "name": "ERROR_ENTITY_VALIDATION",
  "message": "\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].owner' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].unit' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].owner_id' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].unit_id' is null or empty.",
  "entity_id": "0c000000-0000-4000-a000-000000000001",
  "entity_name": "Contact",
  "entity_errors": [],
  "field_errors": [
    {
      "field_id": "10000000-0000-4000-a000-000000000001",
      "field_name": "unit_id",
      "name": "ERROR_FIELD_VALIDATION",
      "code": 40002,
      "errors": [
        {
          "code": 119,
          "name": "ERROR_PARAMETER_MISSING",
          "message": "Non-null field 'Contact'[0c000000-0000-4000-a000-000000000001].unit' is null or empty.",
          "field_id": "10000000-0000-4000-a000-000000000001",
          "field_name": "unit_id"
        },
        {
          "code": 119,
          "name": "ERROR_PARAMETER_MISSING",
          "message": "Non-null field 'Contact'[0c000000-0000-4000-a000-000000000001].unit_id' is null or empty.",
          "field_id": "10000000-0000-4000-a000-000000000001",
          "field_name": "unit_id"
        }
      ]
    },
    {
      "field_id": "10000000-0000-4000-a000-000000000002",
      "field_name": "owner_id",
      "name": "ERROR_FIELD_VALIDATION",
      "code": 40002,
      "errors": [
        {
          "code": 119,
          "name": "ERROR_PARAMETER_MISSING",
          "message": "Non-null field 'Contact'[0c000000-0000-4000-a000-000000000001].owner' is null or empty.",
          "field_id": "10000000-0000-4000-a000-000000000002",
          "field_name": "owner_id"
        },
        {
          "code": 119,
          "name": "ERROR_PARAMETER_MISSING",
          "message": "Non-null field 'Contact'[0c000000-0000-4000-a000-000000000001].owner_id' is null or empty.",
          "field_id": "10000000-0000-4000-a000-000000000002",
          "field_name": "owner_id"
        }
      ]
    }
  ],
  "step_checklist_errors": [],
  "entity_index": null,
  "http_status": 422,
  "success": false
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "errors": [
    {
      "message": "\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].owner' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].unit' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].owner_id' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].unit_id' is null or empty.",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "bulkUpdateContact"
      ],
      "api_error": {
        "code": 40000,
        "name": "ERROR_ENTITY_VALIDATION",
        "message": "\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].owner' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].unit' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].owner_id' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].unit_id' is null or empty.",
        "entity_id": "0c000000-0000-4000-a000-000000000001",
        "entity_name": "Contact",
        "entity_errors": [],
        "field_errors": [
          {
            "field_id": "10000000-0000-4000-a000-000000000001",
            "field_name": "unit_id",
            "name": "ERROR_FIELD_VALIDATION",
            "code": 40002,
            "errors": [
              {
                "code": 119,
                "name": "ERROR_PARAMETER_MISSING",
                "message": "Non-null field 'Contact'[0c000000-0000-4000-a000-000000000001].unit' is null or empty.",
                "field_id": "10000000-0000-4000-a000-000000000001",
                "field_name": "unit_id"
              },
              {
                "code": 119,
                "name": "ERROR_PARAMETER_MISSING",
                "message": "Non-null field 'Contact'[0c000000-0000-4000-a000-000000000001].unit_id' is null or empty.",
                "field_id": "10000000-0000-4000-a000-000000000001",
                "field_name": "unit_id"
              }
            ]
          },
          {
            "field_id": "10000000-0000-4000-a000-000000000002",
            "field_name": "owner_id",
            "name": "ERROR_FIELD_VALIDATION",
            "code": 40002,
            "errors": [
              {
                "code": 119,
                "name": "ERROR_PARAMETER_MISSING",
                "message": "Non-null field 'Contact'[0c000000-0000-4000-a000-000000000001].owner' is null or empty.",
                "field_id": "10000000-0000-4000-a000-000000000002",
                "field_name": "owner_id"
              },
              {
                "code": 119,
                "name": "ERROR_PARAMETER_MISSING",
                "message": "Non-null field 'Contact'[0c000000-0000-4000-a000-000000000001].owner_id' is null or empty.",
                "field_id": "10000000-0000-4000-a000-000000000002",
                "field_name": "owner_id"
              }
            ]
          }
        ],
        "step_checklist_errors": [],
        "entity_index": null
      }
    }
  ],
  "data": {
    "bulkUpdateContact": null
  }
}
```

_Recorded on 2026-10-15 against API 6.5.2 in the docs sandbox, de-identified._

GraphQL's default, `rollbackMethod: ROLLBACK_ALL`, behaves the same way, with HTTP 200, the error
in `errors` and no result. A `batch-delete` with an ID that does not exist fails the same way: HTTP
404, code 143 `ERROR_NO_ENTITY`, and nothing is deleted.

## Partial failures

The same batch sent with `rollback-method=1` writes the two valid contacts and reports the third.
The [create an account with contacts](/recipes/create-account-with-contacts/) recipe adds its
contacts this way:

**REST · cURL**

```bash
partial=$(curl -sS --fail-with-body "$COEVERA_BASE/entities/Contacts/batch-modify?rollback-method=1" \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
  -H "Content-Type: application/json" \
  -d "$CONTACTS")
echo "$partial"
```

**REST · TypeScript**

```ts
const partial = await call("/entities/Contacts/batch-modify?rollback-method=1", contacts);
console.log(`rollback-method=1: HTTP ${partial.status}`, JSON.stringify(partial.body, null, 2));
if (partial.status !== 200) throw new Error(`Expected HTTP 200, got ${partial.status}`);
```

**REST · Python**

```python
partial = session.post(
    f"{BASE}/entities/Contacts/batch-modify",
    params={"rollback-method": 1},
    json=contacts,
    timeout=60,
)
print(f"rollback-method=1: HTTP {partial.status_code}", json.dumps(partial.json(), indent=2))
if partial.status_code != 200:
    sys.exit(f"Expected HTTP 200, got {partial.status_code}")
```

**GraphQL · cURL**

```bash
partial=$(batch ROLLBACK_FAILED | gql)
echo "$partial"
if jq -e '.errors' <<<"$partial" >/dev/null; then
  echo "Unexpected GraphQL errors" >&2
  exit 1
fi
```

**GraphQL · TypeScript**

```ts
const partial = await gql(ADD_CONTACTS, { input: contacts, rollbackMethod: "ROLLBACK_FAILED" });
console.log("ROLLBACK_FAILED:", JSON.stringify(partial, null, 2));
if (partial.errors) throw new Error(`Unexpected GraphQL errors: ${JSON.stringify(partial.errors)}`);
```

**GraphQL · Python**

```python
partial = gql(ADD_CONTACTS, {"input": contacts, "rollbackMethod": "ROLLBACK_FAILED"})
print("ROLLBACK_FAILED:", json.dumps(partial, indent=2))
if partial.get("errors"):
    sys.exit(f"Unexpected GraphQL errors: {json.dumps(partial['errors'])}")
```

**Recorded response · REST · 200 OK**

```json
{
  "success": false,
  "data": [
    "0c000000-0000-4000-a000-000000000002",
    "0c000000-0000-4000-a000-000000000003",
    {
      "code": 40000,
      "name": "ERROR_ENTITY_VALIDATION",
      "message": "\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000004].owner' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000004].unit' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000004].owner_id' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000004].unit_id' is null or empty."
    }
  ]
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "bulkUpdateContact": {
      "result": {
        "created": [
          {
            "index": 0,
            "id": "0c000000-0000-4000-a000-000000000002"
          },
          {
            "index": 1,
            "id": "0c000000-0000-4000-a000-000000000003"
          }
        ],
        "errors": [
          {
            "index": 2,
            "code": 40000,
            "message": "\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000004].owner' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000004].unit' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000004].owner_id' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000004].unit_id' is null or empty."
          }
        ]
      }
    }
  }
}
```

_Recorded on 2026-10-15 against API 6.5.2 in the docs sandbox, de-identified._

### Read per-item results

`data` holds one entry per input item, in input order:

| Item | Entry in `data`                                                        | Meaning                                   |
| ---- | ---------------------------------------------------------------------- | ----------------------------------------- |
| 0    | `"0c000000-…-000000000002"`                                            | Created; the record's ID                  |
| 1    | `"0c000000-…-000000000003"`                                            | Created                                   |
| 2    | `{ "code": 40000, "name": "ERROR_ENTITY_VALIDATION", "message": "…" }` | Failed; nothing was written for this item |

An error entry has only `code`, `name` and `message`, without `field_errors`. In GraphQL,
`rollbackMethod: ROLLBACK_FAILED` returns `result.created` and `result.errors`, each entry with
the `index` of its input item.

> **Checking only the HTTP status loses data:** With `rollback-method=1`, a batch with failed items still answers HTTP 200. An integration that
> treats every 200 as success drops the failed items without noticing. Always walk `data`, and
> treat `success: false` as "some items failed".

## rollback-method

| Value         | GraphQL           | Effect                                                                                                                                                        |
| ------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `0` (default) | `ROLLBACK_ALL`    | One invalid item fails the whole batch: HTTP 422 (404 for an unknown ID in `batch-delete`). Nothing is written, and the error does not say which item failed. |
| `1`           | `ROLLBACK_FAILED` | HTTP 200 with `success: false` when an item failed. The valid items are written; each failed item has its error at its position in `data`.                    |

`rollback-method` applies to `batch-modify`, `batch-delete` and `batch-restore`.
`validation-level` applies to `batch-modify` only: on `batch-delete` and `batch-restore` it is
refused with HTTP 400, code 118 `ERROR_PARAMETER_UNKNOWN`. See [validation](/concepts/validation/).

## Check every item in code

Walk `data` after every batch sent with `rollback-method=1`, and report or retry the failed
items:

**REST · cURL**

```bash
# One line per item, in input order: the new ID, or the error in its place.
jq -r '.data | to_entries[]
  | if (.value | type) == "string" then "\(.key) created \(.value)"
    else "\(.key) FAILED \(.value.code) \(.value.name)" end' <<<"$partial"
```

**REST · TypeScript**

```ts
type ItemError = { code: number; name: string; message: string };
const results = partial.body.data.map((item: string | ItemError, index: number) =>
  typeof item === "string"
    ? { index, ok: true, id: item }
    : { index, ok: false, code: item.code, message: item.message },
);
const failed = results.filter((r: { ok: boolean }) => !r.ok);
if (failed.length) console.warn(`${failed.length} of ${results.length} items failed`, failed);
```

**REST · Python**

```python
results = [
    {"index": index, "ok": True, "id": item}
    if isinstance(item, str)
    else {"index": index, "ok": False, "code": item["code"], "message": item["message"]}
    for index, item in enumerate(partial.json()["data"])
]
failed = [result for result in results if not result["ok"]]
if failed:
    print(f"{len(failed)} of {len(results)} items failed", failed, file=sys.stderr)
```

**GraphQL · cURL**

```bash
jq -r '.data.bulkUpdateContact.result
  | (.created[] | "\(.index) created \(.id)"),
    (.errors[] | "\(.index) FAILED \(.code)")' <<<"$partial"
```

**GraphQL · TypeScript**

```ts
const { created, errors } = partial.data.bulkUpdateContact.result;
for (const item of created) console.log(`${item.index} created ${item.id}`);
for (const item of errors) console.warn(`${item.index} FAILED ${item.code}: ${item.message.trim()}`);
```

**GraphQL · Python**

```python
result = partial["data"]["bulkUpdateContact"]["result"]
for item in result["created"]:
    print(f"{item['index']} created {item['id']}")
for item in result["errors"]:
    print(f"{item['index']} FAILED {item['code']}: {item['message'].strip()}", file=sys.stderr)
```

## Limits

| Limit              | Value                          | Details                                                                                     |
| ------------------ | ------------------------------ | ------------------------------------------------------------------------------------------- |
| Batch size         | None set                       | Up to 100 per request recommended; 500 take about 20 s                                      |
| Which item failed  | Not reported by default        | Use `rollback-method=1` to get per-item results                                             |
| `validation-level` | `batch-modify` only            | Refused on `batch-delete` and `batch-restore` with HTTP 400, code 118                       |
| Relation records   | Not deleted with their records | Delete `ContactAccountRelations` and the `CloudObject` of a removed attachment on their own |
| Request body size  | None documented                | A batch of 500 records (42 kB) is accepted                                                  |
