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.

Bulk endpoints accept up to 100 records per request and process them asynchronously. One bulk request uses one unit of your rate limit instead of 100, which makes bulk the right tool for imports, nightly syncs and backfills.

Kustomer APIYouKustomer APIYouloop[until status = complete]POST /v1/customers/bulk [ …up to 100 items ]1201 { data: { type: "bulk", id, status: "queued" } }2GET /v1/bulk/{id}3{ status: queued | running | complete, batches: { complete, total } }4GET /v1/bulk/{id}/batches5[ { successCount, failureCount, objectIds, results[] } ]6
Diagram source
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

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:

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:

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}
StatusMeaning
queuedAccepted and waiting. You can submit many jobs and let them queue.
runningA worker is processing the job.
completeFinished. 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
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

Send an array; each item needs its id:

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

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

A complete import script

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.