# Bulk operations

> Create, update and delete up to 100 records per request with Kustomer's asynchronous bulk API, then poll for completion and match per-item results with _clientRef.

Source: https://developer.kustomer.com/docs/guides/bulk-operations

Bulk endpoints accept up to **100 records per request** and process them asynchronously. One bulk request uses one unit of your [rate limit](https://developer.kustomer.com/docs/concepts/rate-limits) instead of 100, which makes bulk the right tool for imports, nightly syncs and backfills.

```mermaid
sequenceDiagram
  autonumber
  participant You
  participant API as Kustomer API
  You->>API: POST /v1/customers/bulk [ …up to 100 items ]
  API-->>You: 201 { data: { type: "bulk", id, status: "queued" } }
  loop until status = complete
    You->>API: GET /v1/bulk/{id}
    API-->>You: { status: queued | running | complete, batches: { complete, total } }
  end
  You->>API: GET /v1/bulk/{id}/batches
  API-->>You: [ { successCount, failureCount, objectIds, results[] } ]
```

## Supported resources

| Resource      | Create                                                                                                                  | Update                                                                                                                       | Delete                                                                                                                    |
| ------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Customers     | [`POST /v1/customers/bulk`](https://developer.kustomer.com/api/core-resources/bulk-create-customers)                    | [`PUT /v1/customers/bulk`](https://developer.kustomer.com/api/core-resources/bulk-batch-update-customers)                    | [`DELETE /v1/customers/bulk`](https://developer.kustomer.com/api/core-resources/bulk-delete-customers)                    |
| Companies     | [`POST /v1/companies/bulk`](https://developer.kustomer.com/api/core-resources/bulk-create-companies)                    | [`PUT /v1/companies/bulk`](https://developer.kustomer.com/api/core-resources/bulk-batch-update-companies)                    |                                                                                                                           |
| Conversations | [`POST /v1/conversations/bulk`](https://developer.kustomer.com/api/core-resources/bulk-create-conversations)            | [`PUT /v1/conversations/bulk`](https://developer.kustomer.com/api/core-resources/bulk-batch-update-conversations)            | [`DELETE /v1/conversations/bulk`](https://developer.kustomer.com/api/core-resources/bulk-delete-conversations)            |
| Messages      | [`POST /v1/messages/bulk`](https://developer.kustomer.com/api/core-resources/bulk-create-messages)                      | [`PUT /v1/messages/bulk`](https://developer.kustomer.com/api/core-resources/bulk-batch-update-messages)                      |                                                                                                                           |
| Notes         | [`POST /v1/notes/bulk`](https://developer.kustomer.com/api/core-resources/bulk-create-notes)                            | [`PUT /v1/notes/bulk`](https://developer.kustomer.com/api/core-resources/bulk-batch-update-notes)                            |                                                                                                                           |
| KObjects      | [`POST /v1/klasses/{name}/bulk`](https://developer.kustomer.com/api/core-resources/bulk-create-kobjects-custom-objects) | [`PUT /v1/klasses/{name}/bulk`](https://developer.kustomer.com/api/core-resources/bulk-batch-update-kobjects-custom-objects) | [`DELETE /v1/klasses/{name}/bulk`](https://developer.kustomer.com/api/core-resources/bulk-delete-kobjects-custom-objects) |

Bulk deletes take `?ids=` with a comma-separated list.

## 1. Submit a job

Send an array of the same objects you'd send to the single-record endpoint. Add `_clientRef` to each item so you can match results back to your source rows:

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

The response is a bulk operation. Store its `id`:

```json title="201 Created"
{
  "data": {
    "type": "bulk",
    "id": "5b3bddb67f8469003df370a4",
    "attributes": { "status": "queued" }
  }
}
```

### `_clientRef` rules

- Optional, but **all or nothing**: if any item has `_clientRef`, every item must. A mix returns `400`.
- A non-empty string of up to 255 characters, unique within the request.
- Supported on creates and on array-shaped updates. Not supported on `?ids=` updates or on deletes.

## 2. Poll for completion

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

| Status     | Meaning                                                            |
| ---------- | ------------------------------------------------------------------ |
| `queued`   | Accepted and waiting. You can submit many jobs and let them queue. |
| `running`  | A worker is processing the job.                                    |
| `complete` | Finished. Read the batches for per-item results.                   |

`batches.complete` and `batches.total` show progress, and `batchErrors` is `true` if any batch had failures. Poll every few seconds with backoff. Each poll uses your rate limit.

## 3. Read the results

`GET /v1/bulk/{id}/batches` ([reference](https://developer.kustomer.com/api/core-resources/get-bulk-operation-batches))

```json title="200 OK"
{
  "data": [
    {
      "type": "bulk_batch",
      "id": "507f1f77bcf86cd799439021",
      "attributes": {
        "status": "complete",
        "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": []
      }
    }
  ]
}
```

Each `results` entry has `status` (`created`, `updated` or `error`), the `objectId` on success, and an `error` reason on failure. `results` only appears when you sent `_clientRef`. Without it, use `objectIds`, or look records up by `externalId` once the job completes.

## Updates and deletes

**Different changes per record**

Send an array; each item needs its `id`:

```json title="PUT /v1/customers/bulk"
[
  { "_clientRef": "crm-001", "id": "507f1f77bcf86cd799439031", "custom": { "loyaltyTierStr": "gold" } },
  { "_clientRef": "crm-002", "id": "507f1f77bcf86cd799439032", "custom": { "loyaltyTierStr": "silver" } }
]
```

**Same change to many records**

Pass the IDs in the query string and one object in the body:

```bash
curl -X PUT "https://acme.api.kustomerapp.com/v1/customers/bulk?ids=507f1f77bcf86cd799439031,507f1f77bcf86cd799439032" \
  -H "Authorization: Bearer $KUSTOMER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "tags": ["migrated"] }'
```

**Delete**

```bash
curl -X DELETE "https://acme.api.kustomerapp.com/v1/customers/bulk?ids=507f1f77bcf86cd799439031,507f1f77bcf86cd799439032" \
  -H "Authorization: Bearer $KUSTOMER_API_KEY"
```

Updates merge by default. Add `?replace=true` to replace each record entirely.

## A complete import script

```ts title="Node.js"
const base = `https://${process.env.KUSTOMER_ORG}.api.kustomerapp.com`
const headers = { Authorization: `Bearer ${process.env.KUSTOMER_API_KEY}`, 'Content-Type': 'application/json' }
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))

type Row = { id: string; name: string; email: string }

async function importCustomers(rows: Row[]) {
  const failures: { ref: string; error: string }[] = []
  for (let i = 0; i < rows.length; i += 100) {
    const chunk = rows.slice(i, i + 100)
    const body = chunk.map((r) => ({ _clientRef: r.id, externalId: r.id, name: r.name, emails: [{ type: 'home', email: r.email }] }))
    const job = await fetch(`${base}/v1/customers/bulk`, { method: 'POST', headers, body: JSON.stringify(body) }).then((r) => r.json())

    let status = job.data.attributes.status
    for (let delay = 1000; status !== 'complete'; delay = Math.min(delay * 2, 15000)) {
      await sleep(delay)
      status = (await fetch(`${base}/v1/bulk/${job.data.id}`, { headers }).then((r) => r.json())).data.attributes.status
    }

    const batches = await fetch(`${base}/v1/bulk/${job.data.id}/batches`, { headers }).then((r) => r.json())
    for (const b of batches.data) for (const r of b.attributes.results ?? []) if (r.status === 'error') failures.push({ ref: r._clientRef, error: r.error })
  }
  return failures
}
```

## Tips

- **Set `externalId` as well as `_clientRef`.** `_clientRef` matches results within one job; `externalId` lets you find the record forever.
- **Use `importedAt` for historical data.** It exempts conversations, messages and KObjects from the per-customer create limit.
- **Order matters across jobs.** Create customers before their conversations, and conversations before their messages. Wait for each job to complete before you submit dependent records.
- **Don't send duplicates in one job.** Two items with the same `externalId` in a single request can race.
