# Contacts API

> Create, upsert, list, update, and delete contacts programmatically — plus custom attribute definitions — keyed by your own external_id.

The Contacts API manages the [contact spine](/docs/crm/contacts) from your backend. Contacts key on **`external_id`** — your stable identifier — so repeat writes update the same record instead of duplicating. Authenticate with a workspace `bk_` key; the agent ID in the path scopes the directory.

## Endpoints

| Method & path | Does |
| --- | --- |
| `GET /api/v1/chatbots/:agentId/contacts` | List (paginated: `?page=1&per_page=50`). |
| `POST /api/v1/chatbots/:agentId/contacts` | Create or **upsert** by `external_id`. |
| `GET /api/v1/chatbots/:agentId/contacts/:id` | Fetch one. |
| `PATCH /api/v1/chatbots/:agentId/contacts/:id` | Update. |
| `DELETE /api/v1/chatbots/:agentId/contacts/:id` | Delete. |
| `GET/POST /api/v1/chatbots/:agentId/custom-attributes` | List / define typed custom attributes. |

## Create / upsert

```bash
curl -X POST https://app.openagent.work/api/v1/chatbots/123/contacts \
  -H "Authorization: Bearer bk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "user-123",
    "name": "Jane Doe",
    "email": "jane@example.com",
    "phonenumber": "+1 555 0100",
    "custom_attributes": { "plan": "growth", "ltv": 1290 }
  }'
```

## How the pieces connect

- Use the **same value** for `external_id` here and for `user_id` in [identity verification](/docs/developers/identity-verification) — verified chats then update the same record.
- Custom attributes defined here become filterable in [audiences](/docs/campaigns/audiences) and usable in tool context (`{{contact.attr.plan}}`).
- For continuous sync from a sheet or another CRM, prefer a [contact source](/docs/crm/contacts) — it refills automatically without you writing the loop.
