> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kaisupport.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Import many contacts

> Load thousands of contacts with the batch endpoint.

`POST /contacts/batch` runs up to 1000 operations in one request. Use it for a
first load, and for a nightly sync.

## The four operations

| `op`     | What it does                                                       |
| -------- | ------------------------------------------------------------------ |
| `upsert` | Creates the contact, or updates the one that matches. The default. |
| `update` | Updates the contact that matches. Never creates one.               |
| `delete` | Deletes the profile.                                               |
| `assign` | Sets or clears the account owner.                                  |

Name the contact with `id`, or with an identity such as `external_id`.

## A batch

```bash theme={null}
curl -X POST https://kaisupport.com/api/v1/contacts/batch \
  -H "Authorization: Bearer $KAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "operations": [
      { "op": "upsert", "data": {
          "external_id": "DK-1042",
          "name": "Ayşe Yılmaz",
          "email": "ayse@example.com",
          "attributes": { "package": "Tam Destek" }
      }},
      { "op": "assign", "external_id": "DK-1043", "owner": "baran@example.com" },
      { "op": "delete", "external_id": "DK-0007" }
    ]
  }'
```

## Read the response

```json theme={null}
{
  "summary": { "total": 3, "created": 1, "updated": 0, "deleted": 1, "assigned": 1, "errors": 0 },
  "results": [
    { "index": 0, "op": "upsert", "status": "created", "id": "4f1d6f2a-..." },
    { "index": 1, "op": "assign", "status": "assigned", "id": "7d2b8e11-..." },
    { "index": 2, "op": "delete", "status": "deleted", "id": "8b2f1c40-..." }
  ]
}
```

One bad operation does not stop the others. Kai runs every operation and
reports each one on its own.

The status code describes the request, not the rows. A request that Kai
understands returns 200, even when every operation in it failed. Always read
`summary.errors` and the `results` array.

```json theme={null}
{
  "index": 4,
  "op": "upsert",
  "status": "error",
  "error": {
    "type": "invalid_attributes",
    "message": "Some attributes could not be stored.",
    "details": [{ "key": "exam_year", "message": "expected a number" }]
  }
}
```

## Import 40,000 contacts

<Steps>
  <Step title="Create the attributes first">
    Send `POST /attributes` for each attribute that your rows carry. Kai
    refuses an attribute key that the workspace does not define.
  </Step>

  <Step title="Split the rows into batches of 1000">
    A larger request would run for minutes and can stop at a proxy timeout
    with no report of what it did.
  </Step>

  <Step title="Send the batches one after the other">
    Two batches that run at the same time can hold the same person and make
    two contacts. Send them in sequence.
  </Step>

  <Step title="Log the failed rows">
    Record `index` and `error.type` for each failed operation. Correct those
    rows and send them again.
  </Step>
</Steps>

```python theme={null}
import itertools, requests

API = "https://kaisupport.com/api/v1"
HEADERS = {"Authorization": f"Bearer {KAI_API_KEY}"}

def batches(rows, size=1000):
    it = iter(rows)
    while chunk := list(itertools.islice(it, size)):
        yield chunk

failures = []
for chunk in batches(contacts):
    body = {"operations": [{"op": "upsert", "data": row} for row in chunk]}
    response = requests.post(f"{API}/contacts/batch", json=body, headers=HEADERS)
    response.raise_for_status()
    result = response.json()
    failures += [r for r in result["results"] if r["status"] == "error"]
    print(result["summary"])

print(f"{len(failures)} rows failed")
```

## Repeat a batch safely

An `upsert` matches on identity, so the same batch sent twice updates the same
contacts. It does not make copies.

Give every row an `external_id` if your system has one. It is the strongest
match, because you control it. See [Identity](/concepts/identity).
