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

# Contacts

> Create, read, update, delete, and bulk upsert contacts

Contacts represent the people in your CRM — leads, customers, prospects, and anyone else captured by your agents or imported from another system. Every contact belongs to a specific agent (`bot_id`) and inherits the workspace from your API key.

## The Contact object

<ResponseField name="id" type="string">
  Unique contact identifier.
</ResponseField>

<ResponseField name="workspace_id" type="string">
  The workspace this contact belongs to.
</ResponseField>

<ResponseField name="bot_id" type="string">
  The agent that owns this contact.
</ResponseField>

<ResponseField name="first_name" type="string | null" />

<ResponseField name="last_name" type="string | null" />

<ResponseField name="emails" type="array">
  Array of `{ email, is_primary }` objects. The first item is treated as primary if no `is_primary` flag is set.
</ResponseField>

<ResponseField name="phone_numbers" type="array">
  Array of `{ phone_number, is_primary }` objects.
</ResponseField>

<ResponseField name="company" type="string | null">
  Free-text company name. Use `company_id` to link to a structured Company record.
</ResponseField>

<ResponseField name="company_id" type="string | null">
  Foreign key to a [Company](/api-reference/companies).
</ResponseField>

<ResponseField name="job_title" type="string | null" />

<ResponseField name="lead_source" type="string | null" />

<ResponseField name="tags" type="string[]" />

<ResponseField name="deal_value" type="number | null">
  Estimated deal value as a number (currency assumed to match workspace settings).
</ResponseField>

<ResponseField name="pipeline_stage_id" type="string | null">
  Current stage in a sales pipeline.
</ResponseField>

<ResponseField name="contact_type" type="string | null">
  Free-form classification (e.g. `"lead"`, `"customer"`).
</ResponseField>

<ResponseField name="address_line1" type="string | null" />

<ResponseField name="address_line2" type="string | null" />

<ResponseField name="city" type="string | null" />

<ResponseField name="state" type="string | null" />

<ResponseField name="postal_code" type="string | null" />

<ResponseField name="country" type="string | null" />

<ResponseField name="linkedin_url" type="string | null" />

<ResponseField name="notes" type="string | null" />

<ResponseField name="custom_fields" type="object">
  Workspace-defined custom fields, stored as a free-form JSON object.
</ResponseField>

<ResponseField name="last_contacted_at" type="string | null">
  ISO 8601 timestamp of the most recent activity.
</ResponseField>

<ResponseField name="created_at" type="string">
  ISO 8601 timestamp.
</ResponseField>

<ResponseField name="updated_at" type="string">
  ISO 8601 timestamp.
</ResponseField>

***

## List contacts

<ParamField path="method" type="GET">
  `GET /api/v1/contacts`
</ParamField>

Returns a paginated list of contacts in the workspace.

### Query parameters

| Param               | Type              | Description                                     |
| ------------------- | ----------------- | ----------------------------------------------- |
| `search`            | string            | Match against name, email, phone, or company    |
| `tags`              | string            | Comma-separated list of tags to filter by       |
| `lead_source`       | string            | Filter by lead source                           |
| `pipeline_stage_id` | string            | Filter to contacts in a specific pipeline stage |
| `bot_id`            | string            | Filter to contacts owned by a specific agent    |
| `created_after`     | string (ISO 8601) | Created at or after this timestamp              |
| `created_before`    | string (ISO 8601) | Created at or before this timestamp             |
| `updated_after`     | string (ISO 8601) | Updated at or after this timestamp              |
| `page`              | integer           | See [Pagination](/api-reference/pagination)     |
| `page_size`         | integer           | See [Pagination](/api-reference/pagination)     |

### Example

```bash theme={null}
curl "https://clarky.ai/api/v1/contacts?search=ada&page_size=10" \
  -H "Authorization: Bearer ck_live_example"
```

```json theme={null}
{
  "data": [
    {
      "id": "c_01HXYZ...",
      "workspace_id": "w_01HABC...",
      "bot_id": "b_01HDEF...",
      "first_name": "Ada",
      "last_name": "Lovelace",
      "emails": [{ "email": "ada@example.com", "is_primary": true }],
      "phone_numbers": [],
      "company": "Analytical Engines Inc.",
      "company_id": null,
      "job_title": "Founder",
      "lead_source": "website",
      "tags": ["vip"],
      "deal_value": 25000,
      "pipeline_stage_id": "ps_01HGHI...",
      "contact_type": "lead",
      "address_line1": null,
      "address_line2": null,
      "city": "London",
      "state": null,
      "postal_code": null,
      "country": "UK",
      "linkedin_url": null,
      "notes": null,
      "custom_fields": {},
      "last_contacted_at": "2026-04-29T14:00:00.000Z",
      "created_at": "2026-04-12T09:30:00.000Z",
      "updated_at": "2026-04-29T14:00:00.000Z"
    }
  ],
  "pagination": { "page": 1, "page_size": 10, "total": 1, "total_pages": 1 }
}
```

***

## Get contact by email

<ParamField path="method" type="GET">
  `GET /api/v1/contacts/by-email`
</ParamField>

Looks up a single contact by primary email address. Returns `404 not_found` if no contact matches.

### Query parameters

| Param   | Type   | Required | Description                       |
| ------- | ------ | -------- | --------------------------------- |
| `email` | string | Yes      | Email to match (case-insensitive) |

### Example

```bash theme={null}
curl "https://clarky.ai/api/v1/contacts/by-email?email=ada@example.com" \
  -H "Authorization: Bearer ck_live_example"
```

```json theme={null}
{
  "data": {
    "id": "c_01HXYZ...",
    "first_name": "Ada",
    "last_name": "Lovelace",
    "emails": [{ "email": "ada@example.com", "is_primary": true }],
    "phone_numbers": [],
    "company": "Analytical Engines Inc.",
    "tags": ["vip"],
    "created_at": "2026-04-12T09:30:00.000Z",
    "updated_at": "2026-04-29T14:00:00.000Z"
  }
}
```

***

## Get a contact

<ParamField path="method" type="GET">
  `GET /api/v1/contacts/{id}`
</ParamField>

### Example

```bash theme={null}
curl https://clarky.ai/api/v1/contacts/c_01HXYZ \
  -H "Authorization: Bearer ck_live_example"
```

```json theme={null}
{
  "data": {
    "id": "c_01HXYZ...",
    "first_name": "Ada",
    "last_name": "Lovelace",
    "emails": [{ "email": "ada@example.com", "is_primary": true }],
    "phone_numbers": [],
    "tags": ["vip"],
    "created_at": "2026-04-12T09:30:00.000Z",
    "updated_at": "2026-04-29T14:00:00.000Z"
  }
}
```

***

## Create a contact

<ParamField path="method" type="POST">
  `POST /api/v1/contacts`
</ParamField>

Requires a key with the `write` scope.

### Body fields

| Field               | Type      | Required | Description                             |
| ------------------- | --------- | -------- | --------------------------------------- |
| `bot_id`            | string    | Yes      | Agent that should own this contact      |
| `first_name`        | string    | No       |                                         |
| `last_name`         | string    | No       |                                         |
| `emails`            | array     | No       | Array of `{ email, is_primary }`        |
| `phone_numbers`     | array     | No       | Array of `{ phone_number, is_primary }` |
| `company`           | string    | No       | Free-text company name                  |
| `company_id`        | string    | No       | Link to an existing Company             |
| `job_title`         | string    | No       |                                         |
| `lead_source`       | string    | No       |                                         |
| `tags`              | string\[] | No       |                                         |
| `deal_value`        | number    | No       |                                         |
| `pipeline_stage_id` | string    | No       |                                         |
| `contact_type`      | string    | No       |                                         |
| `address_line1`     | string    | No       |                                         |
| `address_line2`     | string    | No       |                                         |
| `city`              | string    | No       |                                         |
| `state`             | string    | No       |                                         |
| `postal_code`       | string    | No       |                                         |
| `country`           | string    | No       |                                         |
| `linkedin_url`      | string    | No       |                                         |
| `notes`             | string    | No       |                                         |
| `custom_fields`     | object    | No       |                                         |

### Example

```bash theme={null}
curl https://clarky.ai/api/v1/contacts \
  -H "Authorization: Bearer ck_live_example" \
  -H "Content-Type: application/json" \
  -d '{
    "bot_id": "b_01HDEF",
    "first_name": "Ada",
    "last_name": "Lovelace",
    "emails": [{ "email": "ada@example.com", "is_primary": true }],
    "company": "Analytical Engines Inc.",
    "tags": ["vip"]
  }'
```

```json theme={null}
{
  "data": {
    "id": "c_01HXYZ...",
    "first_name": "Ada",
    "last_name": "Lovelace",
    "emails": [{ "email": "ada@example.com", "is_primary": true }],
    "company": "Analytical Engines Inc.",
    "tags": ["vip"],
    "created_at": "2026-04-29T18:42:00.000Z",
    "updated_at": "2026-04-29T18:42:00.000Z"
  }
}
```

***

## Update a contact

<ParamField path="method" type="PATCH">
  `PATCH /api/v1/contacts/{id}`
</ParamField>

Updates the listed fields and leaves everything else untouched. Same fields as [Create a contact](#create-a-contact), **except** `bot_id` cannot be changed.

### Example

```bash theme={null}
curl -X PATCH https://clarky.ai/api/v1/contacts/c_01HXYZ \
  -H "Authorization: Bearer ck_live_example" \
  -H "Content-Type: application/json" \
  -d '{
    "tags": ["vip", "newsletter"],
    "deal_value": 30000
  }'
```

```json theme={null}
{
  "data": {
    "id": "c_01HXYZ...",
    "tags": ["vip", "newsletter"],
    "deal_value": 30000,
    "updated_at": "2026-04-29T18:50:00.000Z"
  }
}
```

***

## Delete a contact

<ParamField path="method" type="DELETE">
  `DELETE /api/v1/contacts/{id}`
</ParamField>

Returns `204 No Content` with an empty body on success.

### Example

```bash theme={null}
curl -X DELETE https://clarky.ai/api/v1/contacts/c_01HXYZ \
  -H "Authorization: Bearer ck_live_example"
```

<Warning>
  Deletion is permanent — there's no undo.
</Warning>

***

## Bulk upsert contacts

<ParamField path="method" type="POST">
  `POST /api/v1/contacts/upsert`
</ParamField>

Insert or update up to **200 contacts** in a single request. Each input contact is matched against existing contacts in this order:

1. Primary email (if provided)
2. Primary phone number (if no email match)

If a match is found, the existing contact is updated. If not, a new contact is created.

### Body fields

| Field      | Type   | Required | Description                                                                     |
| ---------- | ------ | -------- | ------------------------------------------------------------------------------- |
| `bot_id`   | string | Yes      | Agent to own newly created contacts                                             |
| `contacts` | array  | Yes      | Array of contact objects (same fields as [Create](#create-a-contact)). Max 200. |

### Example

```bash theme={null}
curl https://clarky.ai/api/v1/contacts/upsert \
  -H "Authorization: Bearer ck_live_example" \
  -H "Content-Type: application/json" \
  -d '{
    "bot_id": "b_01HDEF",
    "contacts": [
      {
        "first_name": "Ada",
        "emails": [{ "email": "ada@example.com", "is_primary": true }]
      },
      {
        "first_name": "Grace",
        "emails": [{ "email": "grace@example.com", "is_primary": true }]
      }
    ]
  }'
```

### Response

```json theme={null}
{
  "data": {
    "created": [
      {
        "id": "c_01HNEW1...",
        "first_name": "Grace",
        "emails": [{ "email": "grace@example.com", "is_primary": true }]
      }
    ],
    "updated": [
      {
        "id": "c_01HXYZ...",
        "first_name": "Ada",
        "emails": [{ "email": "ada@example.com", "is_primary": true }]
      }
    ],
    "errors": []
  }
}
```

<ResponseField name="created" type="Contact[]">
  Contacts that were newly inserted.
</ResponseField>

<ResponseField name="updated" type="Contact[]">
  Contacts that matched an existing record and were updated.
</ResponseField>

<ResponseField name="errors" type="array">
  Per-row failures as `{ index, message }`. The numeric `index` is the position of the failing item in the input `contacts` array. The rest of the batch still succeeds.
</ResponseField>

<Tip>
  Upsert counts as a single write against your [rate limit](/api-reference/rate-limits) regardless of how many contacts are in the batch — much more efficient than a loop of `POST /contacts` calls.
</Tip>
