# Get bulk operation batches

> GET /v1/bulk/{id}/batches · Core resources API

Source: https://developer.kustomer.com/api/core-resources/get-bulk-operation-batches

```http
GET https://{orgname}.api.kustomerapp.com/v1/bulk/{id}/batches
Authorization: Bearer <API key>
```

## Authorization

Requires any one of these roles: `org.permission.bulk.read`; or legacy `org.user.bulk.read`. The API key or user must hold at least one of them. Legacy roles predate permission sets and still work.

Retrieves detailed batch information for a specific bulk operation, including individual object IDs created or processed during the operation. This endpoint provides comprehensive details about each batch within a bulk operation.

### Use Cases
* Retrieve object IDs of newly created entities from bulk create operations
* Get detailed error information for failed batch items
* Monitor individual batch processing status and completion times
* Access success/failure counts at the batch level

### Batch Status Values
* `queued` - Batch is waiting to be processed
* `scheduled` - Batch has been scheduled for processing
* `complete` - Batch processing has finished

### Response Details
Each batch includes:
* `objectIds` - Array of IDs for successfully created/updated objects
* `successCount` - Number of items processed successfully in this batch
* `failureCount` - Number of items that failed in this batch
* `completedAt` - Timestamp when batch processing finished
* `errors` - Detailed error information for failed items
* `results` - Per-item correlation array (only present when `_clientRef` was supplied
  on the original request). Each entry contains:
  * `_clientRef` — the token you supplied on that request item
  * `status` — `"created"`, `"updated"`, or `"error"`
  * `objectId` — ID of the created/updated object; **omitted on error entries**
  * `error` — failure reason; present only when `status` is `"error"`

### Sorting and Filtering
* **Sorting**: Supports sorting by `createdAt` field
  - `sort=createdAt` - Sort by creation time (ascending, oldest first)
  - `sort=-createdAt` - Sort by creation time (descending, newest first)
  - **Default**: `createdAt` ascending (oldest first)
* **Status Filtering**: Filter by batch status (`queued`, `scheduled`, `complete`)
* **Completion Filtering**: Filter by completion state (`true`/`false`)

### Pagination
Results are paginated. Use standard pagination parameters (`page`, `limit`) to navigate through large batch lists.

## About Bulk operations

A bulk operation is the background job Kustomer creates when you create, update or delete many records in one bulk request. You poll it to see when the job finishes, then read its batches to find out which records succeeded and which failed.

Bulk endpoints such as `POST /v1/customers/bulk`, `PUT /v1/conversations/bulk` and `DELETE /v1/klasses/{name}/bulk` accept up to 100 records per request, but they don't do the work while you wait. They validate the request, queue the work, and respond right away with a bulk operation (type `bulk`) whose `id` you keep. Kustomer then processes the records in the background. The two endpoints here are how you follow that job: one for its overall status, one for per-record results. The records themselves are only created, updated or deleted once the job has run, so don't read them back straight after the bulk request returns.

#### Job status

| `status` | Meaning |
| --- | --- |
| `queued` | Accepted and waiting. You can submit many jobs and leave them queued. |
| `running` | Being processed. |
| `complete` | Finished. `completed` is `true` and `completedAt` is set. |

A job moves through these on its own; one job sitting in `queued` doesn't stop you submitting or checking others. While it runs, `batches.complete` out of `batches.total` shows progress. `complete` means processing has ended, not that every record succeeded: `batchErrors` is `true` if any batch had failures, and `lastBatchError` holds the most recent one.

#### Batches and per-record results

Kustomer splits each job into batches of up to 100 items, each with its own `status` (`queued`, `scheduled` or `complete`), `successCount` and `failureCount`. A batch has three ways to tell you what happened to individual records:

- `objectIds` lists the IDs of records that were created or updated successfully. This is how you get the IDs of new records from a bulk create.
- `errors` gives details for items that failed.
- `results` appears only if you sent `_clientRef` on every item of the original request. Each entry pairs your `_clientRef` with a `status` of `created`, `updated` or `error`, plus the `objectId` on success or an `error` reason on failure. This is the most reliable way to match outcomes back to your source rows.

If you also set `externalId` on the records you create, you can look them up by that ID once the job is complete, long after the batch results stop being convenient.

#### Which endpoint to use

| To | Call |
| --- | --- |
| Check whether a bulk job has finished | [`GET /v1/bulk/{id}`](https://developer.kustomer.com/api/core-resources/get-bulk-operation-by-id) |
| Get per-record results, new record IDs and errors | [`GET /v1/bulk/{id}/batches`](https://developer.kustomer.com/api/core-resources/get-bulk-operation-batches), with `status` or `completed` to filter and `sort=-createdAt` for newest first |

Poll `GET /v1/bulk/{id}` every 5 to 10 seconds with backoff until `status` is `complete`, then read the batches. Each poll counts against your rate limit, while a whole bulk request counts as one call, so polling sensibly keeps bulk far cheaper than single-record calls. When records depend on each other, such as customers and then their conversations, wait for one job to complete before you submit the next. The batch list is paginated with `page` and `limit`.

Related: [Bulk operations guide](https://developer.kustomer.com/docs/guides/bulk-operations), [Bulk customers](https://developer.kustomer.com/api/core-resources#customers-bulk), [Rate limits](https://developer.kustomer.com/docs/concepts/rate-limits)

### Recipes

#### 1. Import customers and collect their new IDs

Load up to 100 customers from another system in one request, wait for the job, then map each source row to the Kustomer ID it became.

**Step 1.** [`POST /v1/customers/bulk`](https://developer.kustomer.com/api/core-resources/bulk-create-customers.md). Send an array of up to 100 customers. Give every item a `_clientRef`, such as your own row ID, so you can match results back later; if any item has one, they all must, and each must be unique in the request. An `externalId` lets you look the customers up afterwards too. Keep the bulk operation's `id` from the response.

```bash title="cURL"
curl --request POST "https://api.kustomerapp.com/v1/customers/bulk" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data '[
    {"_clientRef": "crm-001", "externalId": "crm-001", "name": "Alice Smith", "emails": [{"type": "home", "email": "alice@example.com"}]},
    {"_clientRef": "crm-002", "externalId": "crm-002", "name": "Bob Jones", "emails": [{"type": "home", "email": "bob@example.com"}]}
  ]'
```

```bash title="kusty"
BULK_ID=$(kusty api POST /v1/customers/bulk --data @customers.json | jq -r '.data.id')
```

**Step 2.** [`GET /v1/bulk/{id}`](https://developer.kustomer.com/api/core-resources/get-bulk-operation-by-id.md). Poll every 5 to 10 seconds until `status` is `complete`. The customers don't exist until the job has run, so don't read them back before then. Each poll counts against your rate limit.

```bash title="cURL"
curl "https://api.kustomerapp.com/v1/bulk/$BULK_ID" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```

```bash title="kusty"
until [ "$(kusty api GET /v1/bulk/$BULK_ID | jq -r '.data.attributes.status')" = complete ]; do sleep 10; done
```

**Step 3.** [`GET /v1/bulk/{id}/batches`](https://developer.kustomer.com/api/core-resources/get-bulk-operation-batches.md). Read the batches. Each `results` entry pairs your `_clientRef` with a `status` of `created` or `error`, plus the new `objectId` or an `error` reason. Without `_clientRef`, you only get `objectIds` and `errors`, which are harder to match to rows.

```bash title="cURL"
curl "https://api.kustomerapp.com/v1/bulk/$BULK_ID/batches" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```

```bash title="kusty"
kusty api GET /v1/bulk/$BULK_ID/batches --all \
  | jq -r '(.data.attributes.results // [])[] | [._clientRef, .status, (.objectId // .error)] | @tsv' > results.tsv
```

> `complete` means processing ended, not that every row succeeded: check `batchErrors` on the job, or the `error` rows in the results. The [bulk operations guide](https://developer.kustomer.com/docs/guides/bulk-operations) lists every bulk endpoint.

#### 2. Resubmit only the rows that failed

After a bulk job completes with errors, fix and resend just the failed rows instead of the whole file.

**Step 1.** [`GET /v1/bulk/{id}/batches`](https://developer.kustomer.com/api/core-resources/get-bulk-operation-batches.md). List the finished batches and collect the `_clientRef` and `error` of every result with `status` `error`.

```bash title="cURL"
curl "https://api.kustomerapp.com/v1/bulk/$BULK_ID/batches?completed=true" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```

```bash title="kusty"
kusty api GET /v1/bulk/$BULK_ID/batches --query completed=true --all \
  | jq -c '(.data.attributes.results // [])[] | select(.status == "error") | {_clientRef, error}' > failed.jsonl
```

**Step 2.** Pick the failed rows out of the file you sent, using `_clientRef`. Read each `error` first: a row that failed as a duplicate won't succeed by sending it again, so look that record up by `externalId` instead.

```bash title="kusty"
jq --slurpfile failed failed.jsonl \
  '[.[] | select(._clientRef as $ref | any($failed[]; ._clientRef == $ref))]' customers.json > retry.json
```

**Step 3.** [`POST /v1/customers/bulk`](https://developer.kustomer.com/api/core-resources/bulk-create-customers.md). Submit the corrected rows as a new job, keeping their `_clientRef`s, and follow it like the first one.

```bash title="cURL"
curl --request POST "https://api.kustomerapp.com/v1/customers/bulk" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data @retry.json
```

```bash title="kusty"
kusty api POST /v1/customers/bulk --data @retry.json --fields id
```

#### 3. Create conversations once their customers exist

Import records that depend on each other, such as customers and then their conversation history, by waiting for the first job before starting the second.

**Step 1.** [`GET /v1/bulk/{id}`](https://developer.kustomer.com/api/core-resources/get-bulk-operation-by-id.md). Wait for the customer job to reach `complete`. A conversation needs its customer's Kustomer ID, and that only exists once the customer job has run.

```bash title="cURL"
curl "https://api.kustomerapp.com/v1/bulk/$CUSTOMER_BULK_ID" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```

```bash title="kusty"
until [ "$(kusty api GET /v1/bulk/$CUSTOMER_BULK_ID | jq -r '.data.attributes.status')" = complete ]; do sleep 10; done
```

**Step 2.** [`GET /v1/bulk/{id}/batches`](https://developer.kustomer.com/api/core-resources/get-bulk-operation-batches.md). Map each `_clientRef` to the customer's new `objectId`, skipping rows that failed.

```bash title="cURL"
curl "https://api.kustomerapp.com/v1/bulk/$CUSTOMER_BULK_ID/batches" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```

```bash title="kusty"
kusty api GET /v1/bulk/$CUSTOMER_BULK_ID/batches --all \
  | jq -s '[.[] | (.data.attributes.results // [])[] | select(.status == "created") | {key: ._clientRef, value: .objectId}] | from_entries' > customer-ids.json
```

**Step 3.** Build the conversations from your source data, setting `customer` to the mapped ID; `customer` is required on every item. In this example each source row has its own `ref`, a `subject`, and `customerRef`, the `_clientRef` its customer was imported with. Rows whose customer failed are left out.

```bash title="kusty"
jq --slurpfile ids customer-ids.json \
  '[.[] | select($ids[0][.customerRef]) | {_clientRef: .ref, externalId: .ref, name: .subject, customer: $ids[0][.customerRef]}]' \
  source-conversations.json > conversations.json
```

**Step 4.** [`POST /v1/conversations/bulk`](https://developer.kustomer.com/api/core-resources/bulk-create-conversations.md). Submit the conversations as a second job and follow it the same way.

```bash title="cURL"
curl --request POST "https://api.kustomerapp.com/v1/conversations/bulk" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data @conversations.json
```

```bash title="kusty"
kusty api POST /v1/conversations/bulk --data @conversations.json --fields id
```

### Example object

A real record from [`GET /v1/bulk/{id}`](https://developer.kustomer.com/api/core-resources/get-bulk-operation-by-id.md):

```json
{
  "type": "bulk",
  "id": "507f1f77bcf86cd799439011",
  "attributes": {
    "context": "api.sobjects.customers.bulk-create",
    "status": "complete",
    "completed": true,
    "completedAt": "2023-12-01T10:05:00.000Z",
    "batchErrors": false,
    "v2": true,
    "createdAt": "2023-12-01T10:00:00.000Z",
    "updatedAt": "2023-12-01T10:05:00.000Z",
    "batches": {
      "complete": 1,
      "total": 1,
      "idsTotal": 3
    },
    "payload": {
      "api": {
        "method": "POST",
        "service": "api",
        "path": "/v1/customers",
        "requestId": "507f1f77bcf86cd799439012",
        "user": {
          "id": "507f1f77bcf86cd799439013",
          "userType": "user",
          "roles": [
            "org"
          ],
          "orgName": "example-org",
          "subject": "507f1f77bcf86cd799439013",
          "org": "507f1f77bcf86cd799439014"
        },
        "data": [
          {
            "name": "John Smith",
            "externalId": "customer_001"
          },
          {
            "name": "Sarah Johnson",
            "externalId": "customer_002"
          },
          {
            "name": "Michael Brown",
            "externalId": "customer_003"
          }
        ]
      }
    }
  },
  "relationships": {
    "org": {
      "links": {
        "self": "/v1/orgs/507f1f77bcf86cd799439014"
      },
      "data": {
        "type": "org",
        "id": "507f1f77bcf86cd799439014"
      }
    },
    "createdBy": {
      "links": {
        "self": "/v1/users/507f1f77bcf86cd799439013"
      },
      "data": {
        "type": "user",
        "id": "507f1f77bcf86cd799439013"
      }
    }
  },
  "links": {
    "self": "/v1/bulk/507f1f77bcf86cd799439011"
  }
}
```

Fields:

- `id` (string): The ID the bulk request returned. Keep it to poll `GET /v1/bulk/{id}` and read the job's batches.
- `attributes.context` (string): Which kind of bulk request started the job, such as a bulk customer create.
- `attributes.status` (string): `queued`, `running` or `complete`. A job moves through them on its own; poll until it's `complete`.
- `attributes.completed` (boolean): `true` once processing has ended. It doesn't mean every record succeeded; check `batchErrors`.
- `attributes.completedAt` (string (date-time)): When processing ended. Not set while the job is queued or running.
- `attributes.batchErrors` (boolean): `true` if any batch had a failure. The [batches](https://developer.kustomer.com/api/core-resources/get-bulk-operation-batches) say which records failed and why.
- `attributes.batches` (object): Progress: `complete` out of `total` batches have finished. Batches hold up to 100 items, so a single bulk request is usually one batch.
- `attributes.payload.api` (object): The request that created the job: its `method`, `path` and the records you sent in `data`.
- `attributes.payload.api.user` (object): The user or API key that submitted the request, with its roles at the time.
- `attributes.payload.api.data` (array): The records exactly as you sent them. For the IDs of created records, read the batches' `objectIds` or `results`.

### Permissions

Fewest roles for a read-only integration: `org.permission.bulk.read`. For every endpoint: `org.permission.bulk.read`.

## Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Resource ID. |

## Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `page` | integer | no | Page number for pagination |
| `sort` | string | no | Sort field and direction. Only supports createdAt. Use "createdAt" for ascending or "-createdAt" for descending. Defaults to createdAt ascending. |
| `status` | string | no | Filter batches by status |
| `completed` | string | no | Filter batches by completion status |
| `limit` | integer | no | Number of results per page |
| `pageSize` | integer | no | Number of results to return per page. |

## Responses

### 200

OK

- `data` (array<object>)
  - `type` (string)
  - `id` (string)
  - `attributes` (object)
    - `status` (string): One of: `queued`, `scheduled`, `complete`.
    - `completed` (boolean)
    - `completedAt` (string<date-time>, nullable)
    - `successCount` (integer): (≥ 0)
    - `failureCount` (integer): (≥ 0)
    - `objectIds` (array<string>): Array of IDs for successfully processed objects
    - `errors` (array<object>): Error details for failed items
    - `results` (array<object>): Per-item correlation results. Only present when `_clientRef` was supplied on the original bulk request. Each entry echoes the caller-supplied token alongside the outcome for that item.
    - `v2` (boolean)
    - `createdAt` (string<date-time>)
    - `updatedAt` (string<date-time>)
    - `payload` (object): Original batch payload and metadata
  - `relationships` (object)
    - `org` (object)
    - `bulk` (object)
    - `createdBy` (object)
  - `links` (object)
    - `self` (string)
- `meta` (object)
  - `page` (integer)
  - `pages` (integer)
  - `count` (integer)
  - `limit` (integer)

```json
{
  "data": [
    {
      "type": "bulk_batch",
      "id": "507f1f77bcf86cd799439021",
      "attributes": {
        "status": "complete",
        "completed": true,
        "completedAt": "2023-12-01T10:03:15.000Z",
        "successCount": 2,
        "failureCount": 1,
        "objectIds": [
          "507f1f77bcf86cd799439031",
          "507f1f77bcf86cd799439032"
        ],
        "results": [
          {
            "_clientRef": "crm-001",
            "objectId": "507f1f77bcf86cd799439031",
            "status": "created"
          },
          {
            "_clientRef": "crm-002",
            "objectId": "507f1f77bcf86cd799439032",
            "status": "created"
          },
          {
            "_clientRef": "crm-003",
            "status": "error",
            "error": "duplicate detected"
          }
        ],
        "errors": [],
        "v2": true,
        "createdAt": "2023-12-01T10:00:30.000Z",
        "updatedAt": "2023-12-01T10:03:15.000Z",
        "payload": {
          "api": {
            "method": "POST",
            "service": "api",
            "path": "/v1/customers"
          }
        }
      },
      "relationships": {
        "org": {
          "links": {
            "self": "/v1/orgs/507f1f77bcf86cd799439014"
          },
          "data": {
            "type": "org",
            "id": "507f1f77bcf86cd799439014"
          }
        },
        "bulk": {
          "links": {
            "self": "/v1/bulk/507f1f77bcf86cd799439011"
          },
          "data": {
            "type": "bulk",
            "id": "507f1f77bcf86cd799439011"
          }
        },
        "createdBy": {
          "links": {
            "self": "/v1/users/507f1f77bcf86cd799439013"
          },
          "data": {
            "type": "user",
            "id": "507f1f77bcf86cd799439013"
          }
        }
      },
      "links": {
        "self": "/v1/bulk/batches/507f1f77bcf86cd799439021"
      }
    }
  ],
  "meta": {
    "page": 1,
    "pages": 1,
    "count": 1,
    "limit": 25
  }
}
```

### 400

Bad Request

### 401

Unauthorized

### 403

Forbidden

### 404

Not Found

## Example request

```bash
curl --request GET \
  --url 'https://api.kustomerapp.com/v1/bulk/62799c44a96e7f160834199c/batches' \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```
