> ## 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.

# Export contacts

> Read every contact, or only the contacts that changed.

`GET /contacts` is the export. There is no separate endpoint, and no job to
start: you read pages until the cursor is `null`.

## Read every contact

Set `limit` to 500, then follow `next_cursor`:

```bash theme={null}
curl "https://kaisupport.com/api/v1/contacts?limit=500" \
  -H "Authorization: Bearer $KAI_API_KEY"
```

```json theme={null}
{
  "data": [ { "id": "4f1d6f2a-...", "external_id": "DK-1042" } ],
  "next_cursor": "MjAyNi0wOC0wNVQxODoyMDoxMS4wMDBafDRmMWQ2ZjJh",
  "has_more": true
}
```

Send `next_cursor` as `cursor` on the next request. Stop when `has_more` is
`false`.

```python theme={null}
import requests

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

cursor, rows = None, []
while True:
    params = {"limit": 500}
    if cursor:
        params["cursor"] = cursor
    page = requests.get(f"{API}/contacts", params=params, headers=HEADERS).json()
    rows += page["data"]
    cursor = page["next_cursor"]
    if not cursor:
        break

print(len(rows))
```

## Read only what changed

Send `updated_since` with the timestamp of your last successful run:

```bash theme={null}
curl "https://kaisupport.com/api/v1/contacts?limit=500&updated_since=2026-08-05T00:00:00Z" \
  -H "Authorization: Bearer $KAI_API_KEY"
```

Store the `updated_at` of the last contact that you read, and use it as the
`updated_since` of your next run. Take a small overlap, for example one
minute, so a contact that changed during the run cannot fall between two runs.

## Why the order is what it is

Kai returns contacts oldest change first, by `updated_at` and then by `id`.

An export of 40,000 contacts takes many requests, and your workspace keeps
editing contacts while it runs. An edited contact moves to the end of this
order, never backward, so the export can see it twice but cannot miss it.

A newest-first order has the opposite property, and an export would silently
drop rows.

## Filters

| Parameter       | What it does                                             |
| --------------- | -------------------------------------------------------- |
| `search`        | Matches a part of the name, email, phone or external id. |
| `owner`         | A member id, for the contacts of one account owner.      |
| `owner=none`    | The contacts that have no owner.                         |
| `updated_since` | Contacts changed at or after an ISO 8601 timestamp.      |

The filters work together. This request lists the contacts of one teammate
that changed today:

```
GET /contacts?owner=2c9a0d51-...&updated_since=2026-08-06T00:00:00Z&limit=500
```
