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

# GET /contacts

> Required. The people behind your leads. Incremental on updated_at, ordered ascending; contacts carry no deletion signal at all.

<Info>
  **Required.** `/contacts` returns your contacts — the people behind your leads.
  It is [incremental](/concepts/how-sync-works#incremental-sync): it receives
  `updated_since` and **must** be returned ordered by `updated_at` **ascending**.
</Info>

## Purpose

A contact is one person in your CRM. HunterAI reads `/contacts` to resolve who is
behind each lead — a lead's first `contact_ids` entry points here — and to carry
contact-level context (name, phone, email, and your custom fields) for analytics
and coaching. Contacts share the same incremental contract as
[`/leads`](/endpoints/leads): the same `updated_since` filter, the same required
ascending order, and the same watermark rules.

## Request

```http theme={null}
GET /api/hunterai/v1/contacts?limit=100&page=1&updated_since=2025-07-01T00:00:00+05:00 HTTP/1.1
Host: crm.example.uz
Authorization: Bearer <token>
Accept: application/json
```

| Query param     | Value       | Notes                                                                                                                                          |
| --------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `limit`         | `100`       | Always sent. Return up to this many contacts per page.                                                                                         |
| `page`          | `1`, `2`, … | 1-based. Page until `has_more` is `false`.                                                                                                     |
| `updated_since` | timestamp   | Return contacts with `updated_at >= updated_since` (inclusive). On a full sync this is the [start date](/concepts/how-sync-works#time-window). |

You **must** return the contacts ordered by `updated_at` **ascending**. HunterAI
advances its watermark page by page and relies on that order; an out-of-order
page can cause older contacts on a later page to be skipped.

## Response envelope

```json 200 OK theme={null}
{
  "data": [
    {
      "id": "c-5001",
      "name": "Jasur Toshmatov",
      "phone": "+998901234567",
      "email": "jasur@example.uz",
      "responsible_user_id": "u-1001",
      "custom_fields": { "telegram": "@jasur", "region": "Toshkent" },
      "created_at": "2025-06-15T08:00:00+05:00",
      "updated_at": "2025-07-02T14:30:00+05:00"
    },
    {
      "id": "c-5002",
      "name": "Malika Rahimova",
      "phone": "+998935556677",
      "email": "malika@example.uz",
      "responsible_user_id": "u-1002",
      "custom_fields": { "region": "Samarqand" },
      "created_at": "2025-06-20T09:00:00+05:00",
      "updated_at": "2025-07-03T10:15:00+05:00"
    }
  ],
  "has_more": false,
  "page": 1,
  "limit": 100
}
```

The envelope (`data`, `has_more`, `page`, `limit`) is the same on every endpoint
— see [How sync works → The envelope](/concepts/how-sync-works#the-envelope).
Only `data` and `has_more` are read.

## Fields

| Field                 | Type   | Required | Nullable | Format / Constraint                                                                 | Example                                          | Notes                                                                                                                                                |
| --------------------- | ------ | -------- | -------- | ----------------------------------------------------------------------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                  | string | Yes      | No       | max 255 chars; stable and unique per connection; never changes for the same contact | `"c-5001"`                                       | The upsert key together with the connection. An absent or over-length `id` fails its page.                                                           |
| `name`                | string | No       | Yes      | max 255 chars                                                                       | `"Jasur Toshmatov"`                              | Contact name. Silently truncated to 255 chars.                                                                                                       |
| `phone`               | string | No       | Yes      | max 50 chars                                                                        | `"+998901234567"`                                | Silently truncated to 50 chars.                                                                                                                      |
| `email`               | string | No       | Yes      | max 255 chars                                                                       | `"jasur@example.uz"`                             | Silently truncated to 255 chars.                                                                                                                     |
| `responsible_user_id` | string | No       | Yes      | must match an `id` from [`/users`](/endpoints/users)                                | `"u-1001"`                                       | The owning manager; drives per-manager attribution for the contact.                                                                                  |
| `custom_fields`       | object | No       | Yes      | flat JSON object — a map of string keys to values                                   | `{ "telegram": "@jasur", "region": "Toshkent" }` | Stored as name/value pairs; keys are coerced to strings and each value is kept as its original JSON type. See the note below for a non-object value. |
| `created_at`          | string | No       | Yes      | timestamp (see [Timestamp format](#timestamp-format))                               | `"2025-06-15T08:00:00+05:00"`                    | Contact creation time. An unparseable value becomes `null`.                                                                                          |
| `updated_at`          | string | Yes      | No       | timestamp (see [Timestamp format](#timestamp-format))                               | `"2025-07-02T14:30:00+05:00"`                    | Drives `updated_since` and the watermark. Must be present, parseable, and must advance on every modification.                                        |

<Note>
  **`custom_fields` must be a JSON object.** Each value inside it may be any JSON
  type — string, number, boolean, object, array, or `null` — and is stored
  unchanged. If `custom_fields` itself is **not** an object — an array, a string,
  a number, a boolean, or an empty `{}` — it is silently stored as `null` (the
  whole field is dropped); **no error is raised** and the contact is still
  ingested.
</Note>

### Timestamp format

`created_at` and `updated_at` accept a calendar date and time in these concrete
forms:

* With an explicit offset — `2025-07-02T14:30:00+05:00`.
* With a trailing `Z` for UTC — `2025-07-02T14:30:00Z`.
* With no zone at all — `2025-07-02T14:30:00`, which is read as UTC.
* The date and time may be separated by a `T` or by a single space
  (`2025-07-02 14:30:00`).
* A bare date — `2025-07-02` — is accepted; the time becomes `00:00` UTC.

Prefer the explicit-offset form. **An unparseable value becomes `null` for that
field, and no error is raised** — the contact is still ingested.

<Warning>
  **Consequence when this hits `updated_at`:** a `null` `updated_at` carries no
  modification time, so the contact cannot advance the
  [watermark](/concepts/how-sync-works#ordering-and-the-watermark) and is not
  selected by any later `updated_since` filter. In effect the contact — and every
  future change to it — drops out of incremental sync. Keep `updated_at` a valid
  timestamp that advances on every change.
</Warning>

## Fields the adapter ignores

Sending any of these has **no effect** — do not spend effort populating them:

* `is_deleted` or any other deletion flag — contacts have no tombstone; see
  [Deletion behaviour](#deletion-behaviour).
* Any other key not listed in the [Fields](#fields) table — contacts retain no
  raw copy, so anything unlisted is dropped entirely. See
  [Limitations → Fields dropped entirely](/limitations#fields-dropped-entirely).

## Identity & cross-references

* **Identity:** `id` is the stable primary key. HunterAI upserts by
  `(connection, id)`, so a contact's `id` **must never change** — a changed `id`
  is ingested as a brand-new contact.
* `responsible_user_id` → resolves against [`/users`](/endpoints/users).
* **Referenced by leads:** a lead's first `contact_ids` entry resolves to a
  contact `id` returned here — HunterAI keeps only that first element. See
  [`/leads`](/endpoints/leads).

## Incremental sync

`/contacts` is incremental, on the same contract as [`/leads`](/endpoints/leads).
HunterAI sends `updated_since`; return only contacts whose `updated_at` **is
greater than or equal to** it (inclusive — re-returning the boundary contact is
harmless, since the upsert is idempotent).

* `updated_at` drives the per-entity **watermark** — the largest `updated_at`
  HunterAI has committed. The next run resumes from there.
* You **must** return contacts ordered by `updated_at` **ascending**, so the
  watermark can advance page by page without skipping an older contact on a later
  page.
* `updated_at` **must change on every modification** of a contact — any field
  edit. A change that does not advance `updated_at` falls at or below the
  watermark and is never pulled again.

See [How sync works](/concepts/how-sync-works#incremental-sync) for the shared
model.

## Deletion behaviour

**Contacts carry no deletion signal at all.** Unlike leads, there is no
`is_deleted` on a contact and no other tombstone — an `is_deleted: true` you send
is ignored, not honoured. Because the upsert only inserts and updates, a contact
you delete in your CRM, or simply stop returning, is **not** removed from
HunterAI; it stays ingested as an active contact and its historical row persists.
Only leads support deletion, via `is_deleted` — see
[Limitations → Tombstones](/limitations#tombstones-apply-to-leads-only).

## Failure modes

A page of contacts (up to 100 records) is validated and written **as one
transaction**. The consequences:

* **A single malformed contact fails the whole page.** If any contact on a page
  cannot be processed — a missing `id`, or an over-length `id` — none of the
  contacts on that page are saved. This is page-level, not per-record.
* **A failed page fails the sync.** Because `/contacts` is required, a page that
  fails takes the contacts entity down with it, and the overall sync is reported
  *failed*. Pages that already committed stay committed, and the next run resumes
  from the watermark.
* **An unparseable timestamp does not fail anything** — the field becomes `null`
  and the contact is still ingested. See the consequence for `updated_at` above.
* **A non-object `custom_fields` does not fail anything** — it is stored as
  `null` and the contact is still ingested.
* **`429`, `5xx`, and request timeouts** are retried with exponential backoff up
  to 5 times, then the entity fails; a timeout is retried the same way as a
  `5xx`. HunterAI applies its request rate limit **per connection**, so two
  connections to the same base URL are throttled independently.
* **A repeated or unbounded page** — a page whose IDs match the previous page, or
  a run past the page ceiling — fails the entity. See
  [How sync works → Paging](/concepts/how-sync-works#paging).

## If this endpoint is missing

<Warning>
  `/contacts` is **required**. A `404` fails the contacts entity and the sync is
  reported failed. Leads still ingest, but the people behind them are unresolved,
  so contact-level context and per-contact attribution are empty.
</Warning>
