
# Batch calls

A single HTTP request combines up to 50 operations across different entities. Each call is identified by its own `id` and processed independently: an error in one call does not cancel the others.

`POST /v1/batch`

**Scope:** checked individually per entity (`crm`, `task`, `im`, `disk`, etc.) | **Base URL:** `https://vibecode.bitrix24.com/v1` | **Authorization:** `X-Api-Key` (APP key)

## Request fields (body)

| Field | Type | Req. | Description |
|------|-----|:---:|---------|
| `calls` | array | ★ | Array of calls (1 to 50). Each element is an object; its format is described in the table below. |

## Fields of a single call

| Field | Type | Req. | Description |
|------|-----|:---:|---------|
| `id` | string | no | Call identifier in the response. If omitted, a sequential index is assigned (`"0"`, `"1"`, ...). Up to 64 characters. |
| `entity` | string | ★ | Plural entity name: `deals`, `contacts`, `companies`, `tasks`, `users`, `files`, `folders`, and others. The full list of entities available to your key is returned by `GET /v1/me` — see [Keys and authorization](/docs/keys-auth). |
| `action` | string | ★ | Operation: `list`, `get`, `create`, `update`, `delete`, `fields`, `search`. |
| `entityId` | number / string | for `get` / `update` / `delete` | Record identifier. |
| `params` | object | no | Operation parameters in the unified entity format (field names in `camelCase`, filters in [filtering syntax](/docs/filtering)). |

The parameters inside `params` match the parameters of a single Entity API endpoint:

- `list` and `search` — `filter`, `select`, `order`, `limit`
- `get` — the `include` field, if the entity supports it
- `create` and `update` — entity fields (names in `camelCase`)
- `delete` and `fields` — no parameters needed
- smart processes (`entity: "items"`) — `entityTypeId` is required inside `params`

The record identifier for `get` / `update` / `delete` is passed in the `entityId` field at the call level, **not** in `params.id`. The call `{ "entity": "users", "action": "get", "params": { "id": 1 } }` without `entityId` is rejected as `MISSING_ENTITY_ID`, and the whole batch returns `400` with a validation error. Correct: `{ "entity": "users", "action": "get", "entityId": 1 }`. The `params` field for `get` is used only for `include`. `update` and `delete` are built the same way — the identifier goes in `entityId`, and the fields to change for `update` go in `params`: `{ "entity": "deals", "action": "update", "entityId": 575, "params": { "title": "New title" } }` and `{ "entity": "contacts", "action": "delete", "entityId": 42 }`. The success of `update` and `delete` is determined by `data.summary.succeeded` and the absence of the `id` in `data.errors`.

## Examples

### curl — personal key

```bash
curl -X POST https://vibecode.bitrix24.com/v1/batch \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "calls": [
      {
        "id": "deals",
        "entity": "deals",
        "action": "list",
        "params": {
          "filter": { "stageId": "NEW" },
          "select": ["id", "title", "amount"],
          "limit": 50
        }
      },
      {
        "id": "contacts",
        "entity": "contacts",
        "action": "list",
        "params": {
          "select": ["id", "name", "lastName"],
          "limit": 20
        }
      },
      {
        "id": "user1",
        "entity": "users",
        "action": "get",
        "entityId": 1
      }
    ]
  }'
```

### curl — OAuth application

```bash
curl -X POST https://vibecode.bitrix24.com/v1/batch \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "calls": [
      {
        "id": "deals",
        "entity": "deals",
        "action": "list",
        "params": {
          "filter": { "stageId": "NEW" },
          "select": ["id", "title", "amount"],
          "limit": 50
        }
      },
      {
        "id": "contacts",
        "entity": "contacts",
        "action": "list",
        "params": {
          "select": ["id", "name", "lastName"],
          "limit": 20
        }
      },
      {
        "id": "user1",
        "entity": "users",
        "action": "get",
        "entityId": 1
      }
    ]
  }'
```

### JavaScript — personal key

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/batch', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    calls: [
      { id: 'deals', entity: 'deals', action: 'list', params: { filter: { stageId: 'NEW' }, select: ['id', 'title', 'amount'], limit: 50 } },
      { id: 'contacts', entity: 'contacts', action: 'list', params: { select: ['id', 'name', 'lastName'], limit: 20 } },
      { id: 'user1', entity: 'users', action: 'get', entityId: 1 }
    ]
  })
})

const { data } = await res.json()
console.log('Deals:', data.results.deals)
console.log('Contacts:', data.results.contacts)
console.log('Summary:', data.summary)
```

### JavaScript — OAuth application

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/batch', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    calls: [
      { id: 'deals', entity: 'deals', action: 'list', params: { filter: { stageId: 'NEW' }, select: ['id', 'title', 'amount'], limit: 50 } },
      { id: 'contacts', entity: 'contacts', action: 'list', params: { select: ['id', 'name', 'lastName'], limit: 20 } },
      { id: 'user1', entity: 'users', action: 'get', entityId: 1 }
    ]
  })
})

const { data } = await res.json()
console.log('Deals:', data.results.deals)
```

## Response fields

| Field | Type | Description |
|------|-----|---------|
| `success` | boolean | `true` if the request is accepted. Partial errors inside `data.errors` do not flip it to `false`. |
| `data.results` | object | Results keyed by the `id` of each call. The value is whatever the corresponding Entity API endpoint would return: an array of records for `list` / `search`, an object for `get` / `create`, a normalized record for `update`, an object `{ id, deleted: true }` for `delete`, a field schema for `fields`. |
| `data.totals` | object | Total record count matching the filter for `list` / `search` calls. Keys are the `id` values of the corresponding calls. |
| `data.errors` | object | Errors keyed by the `id` of failed calls. Each value is `{ "code": "...", "message": "..." }`. |
| `data.summary.total` | number | Total number of calls in the request. |
| `data.summary.succeeded` | number | Number of successful calls. |
| `data.summary.failed` | number | Number of failed calls. |
| `data.meta` | object | Additional details keyed by the `id` of calls with `action: "list"` or `action: "search"`: `total`, `returned`, `hasMore`, `truncated`, and — when a page of results is lost — `pageErrorSample`. |

## Response example

```json
{
  "success": true,
  "data": {
    "results": {
      "deals": [
        { "id": 575, "title": "Currency test", "amount": 0 },
        { "id": 741, "title": "Equipment delivery", "amount": 250000 }
      ],
      "contacts": [
        { "id": 1, "name": "John", "lastName": "Brown" }
      ],
      "user1": [
        { "ID": "1", "NAME": "Mary", "ACTIVE": true }
      ]
    },
    "totals": {
      "deals": 1798,
      "contacts": 305
    },
    "errors": {},
    "summary": {
      "total": 3,
      "succeeded": 3,
      "failed": 0
    },
    "meta": {
      "deals": { "total": 1798, "returned": 2, "hasMore": true, "truncated": false },
      "contacts": { "total": 305, "returned": 1, "hasMore": true, "truncated": false }
    }
  }
}
```

## Partial errors

If some calls fail validation or return an error on the Bitrix24 side, successful results stay in `data.results`, failed ones go to `data.errors` under the same `id`:

```json
{
  "success": true,
  "data": {
    "results": {
      "deals": [
        { "id": 575, "title": "Currency test" }
      ]
    },
    "totals": {
      "deals": 1798
    },
    "errors": {
      "unknown": {
        "code": "UNKNOWN_ENTITY",
        "message": "Unknown entity \"foobar\". Check GET /v1/guide for available entities."
      }
    },
    "summary": {
      "total": 2,
      "succeeded": 1,
      "failed": 1
    },
    "meta": {
      "deals": { "total": 1798, "returned": 1, "hasMore": true, "truncated": false }
    }
  }
}
```

## Error response example

If all calls fail validation, `400 INVALID_REQUEST` is returned with a per-`id` breakdown in `data.errors`:

```json
{
  "success": false,
  "error": {
    "code": "INVALID_REQUEST",
    "message": "All calls in the batch failed validation"
  },
  "data": {
    "errors": {
      "x": {
        "code": "MISSING_ENTITY_ID",
        "message": "Action \"get\" requires entityId."
      }
    }
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|----------|
| 400 | `INVALID_REQUEST` | The request body does not match the schema, or all calls failed validation. |
| 400 | `UNKNOWN_ENTITY` | One of the calls passed an unknown entity name. |
| 400 | `ACTION_NOT_SUPPORTED` | The entity does not support the specified action (e.g. `delete` for a reference entity that does not support deletion). |
| 400 | `MISSING_ENTITY_ID` | Actions `get`, `update`, `delete` require `entityId`. |
| 400 | `EMPTY_CREATE_BODY` | A `create` call with no body field. Returned for the specific call inside `data.errors`. |
| 400 | `EMPTY_UPDATE_BODY` | An `update` call with no body field. Returned for the specific call inside `data.errors`. |
| 400 | `MISSING_REQUIRED_PARAMS` | A `list` call for `calendar-events` did not pass the mandatory `type` and `ownerId` in `params`. |
| 400 | `UNSUPPORTED_FILTER` | Fields the entity's method does not accept as a filter were passed in the call's `params.filter` — for example, `calendar-events`. |
| 400 | `MISSING_DYNAMIC_PARAM` | For smart processes (`entity: "items"`), `entityTypeId` was not passed inside `params`. |
| 400 | `INVALID_DYNAMIC_PARAM` | `entityTypeId` for smart processes is set incorrectly (not a positive integer). |
| 400 | `USE_DEDICATED_ENTITY` | A dedicated entity exists for the passed `entityTypeId` — use it instead of `items`. |
| 400 | `ENTITY_CUSTOM_ROUTES` | The entity works only through specialized routes (e.g. `task-comments` — via `/v1/tasks/:taskId/comments`). |
| 400 | `INVALID_CALL` | The call object is missing the required `entity` and `action` fields. |
| 401 | `TOKEN_MISSING` | The key has no configured OAuth tokens, or for an OAuth application no `Authorization: Bearer ...` was passed. |
| 401 | `TOKEN_REFRESH_FAILED` | Failed to refresh the portal OAuth token. |
| 403 | `MANAGEMENT_KEY_NO_ENTITY_ACCESS` | The request came from a management key — batch calls are available only to APP keys. |
| 403 | `SCOPE_NOT_ALLOWED` | All calls requested scopes the key does not have. If at least one call passes, this code is returned inside `data.errors` for the specific calls, and the request itself succeeds. |
| 422 | `BITRIX_ERROR` | Bitrix24 rejected the call. The response body contains `bitrixError.error` and `bitrixError.error_description`. |
| 429 | `RATE_LIMITED` | The limit of 30 requests per minute per portal was exceeded — the limit is shared across all of the portal's API keys. The response contains a `Retry-After` header. |
| 429 | `QUEUE_OVERFLOW` | Too many concurrent Bitrix24 calls have accumulated on the portal (by default more than 100 pending). The response is returned instantly with an HTTP header `Retry-After: N` (seconds) — the client must wait for it and retry with exponential backoff + jitter. Body: `error.code = QUEUE_OVERFLOW`, `error.retryAfter` duplicates the header. |
| 429 | `QUEUE_TIMEOUT` | The request waited in the portal queue for more than 30 seconds. The request was NOT sent to Bitrix24 — safe to retry (`Retry-After`). The response contains `userMessage` and `hint`. |
| 502 | `BITRIX_UNAVAILABLE` | Bitrix24 responded with a 5xx error. |
| 503 | `BITRIX_TIMEOUT` | Bitrix24 accepted the request but did not respond within 15 seconds — the outcome is unknown. For write calls inside the batch: re-read the entity first, the change may have applied. |
| 500 | `INTERNAL_ERROR` | Internal proxy error. |

Full list of common API errors — [Error codes](/docs/errors).

## Known specifics

**`search` and `list` with `limit > 50` bypass the native Bitrix24 batch call.** These calls run as separate sequential requests with paging up to 5000 records — each consumes its own Bitrix24 rate-limit quota independently. The remaining calls — `list` with `limit ≤ 50`, `get`, `create`, `update`, `delete`, `fields` — are combined into a single batch call on the Bitrix24 side and cost one rate-limit unit in total.

**Cost of `list` in Bitrix24 rate-limit units.** Every 50 records = 1 unit. With `limit > 50` the auto-paginator makes several calls:

| `limit` | Bitrix24 units |
|---------|-----------------|
| 1–50 | 1 (native batch) |
| 51–2 550 | 2 |
| 2 551–5 000 | 3 |

**If an action in one call fails, the rest keep executing.** Errors land in `data.errors` under the same `id`, successful results — in `data.results`. The `success` field stays `true`; check `data.summary.failed` or the presence of the expected `id` in `data.results`.

**`users.get` returns an array, not an object.** This is Entity API-specific behavior: the `get` response for users is `[ { ID, NAME, ... } ]`. The first element is the requested record.

**A page of results is lost when a sub-request fails.** If, during auto-pagination of a `list` / `search` call, one of the page's sub-requests fails, the result is trimmed to a contiguous prefix and `data.meta[<id>].pageErrorSample` carries `{ code, message }` of the first failure. The `hasMore` field stays `true` in that case — the remaining records can be re-fetched.

**`calendar-events` in a batch call.** The mandatory `type` and `ownerId` go into the call's `params`. Filtering is not supported for this entity — leftover fields in `params.filter` return `UNSUPPORTED_FILTER`, and a missing `type` or `ownerId` returns `MISSING_REQUIRED_PARAMS`.

**Batch operations for a single entity.** The specialized endpoint `POST /v1/{entity}/batch` works with one entity and supports the actions `create`, `update`, `delete`, `list`, `get`, `fields`. For `delete` you pass an `ids` array, for `create` and `update` — `items`, and for the reads `list` / `get` / `fields` — a `calls` array. Writes run in internal batches of 50 records, up to 500 per request, and the response is an array of results with a `success` flag for each element. For smart processes the path carries the type segment: `POST /v1/items/{entityTypeId}/batch`. In a `list` call the `filter` field goes through the filter translator — field aliases and the operators `$gt`, `$contains`, `$in` work. An invalid filter rejects the whole batch with `400` and names the call index in the message.

## See also

- [Limits and optimization](/docs/optimization) — auto-pagination, batch calls, aggregation
- [Filtering syntax](/docs/filtering) — how to describe `params.filter`
- [Entity API](/docs/entity-api) — entity reference and single endpoints
- [Keys and authorization](/docs/keys-auth) — key types, scopes, self-description via `GET /v1/me`
- [Error codes](/docs/errors) — general reference
