> ## 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 /calls

> Optional. Call records — call volume and connect rate per manager. Incremental on updated_at; return 404 if you don't implement it and the sync still succeeds.

<Info>
  **Optional.** `/calls` returns call records — the telephony activity behind your
  leads and contacts. If you do not implement it, return `404` — HunterAI skips it
  and the sync still succeeds. It is
  [incremental](/concepts/how-sync-works#incremental-sync).
</Info>

## Purpose

A call is one telephony record in your CRM — an inbound or outbound call a manager
made or received. HunterAI reads `/calls` to compute call volume per manager and
**connect rate** — the share of calls with `duration_seconds > 0`. Each call is
linked to the lead or contact it concerns, so those metrics attribute per manager
and per record.

## Request

```http theme={null}
GET /api/hunterai/v1/calls?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 calls per page.          |
| `page`          | `1`, `2`, … | 1-based. Page until `has_more` is `false`.                   |
| `updated_since` | timestamp   | Return calls with `updated_at >= updated_since` (inclusive). |

You **must** return calls 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 calls on a later page to be skipped.

## Response envelope

```json 200 OK theme={null}
{
  "data": [
    {
      "id": "call-2001",
      "lead_id": "L-9001",
      "contact_id": null,
      "responsible_user_id": "u-1001",
      "direction": "out",
      "duration_seconds": 214,
      "phone": "+998901234567",
      "created_at": "2025-07-02T15:10:00+05:00",
      "updated_at": "2025-07-02T15:10:00+05:00"
    },
    {
      "id": "call-2002",
      "lead_id": null,
      "contact_id": "c-5003",
      "responsible_user_id": "u-1002",
      "direction": "in",
      "duration_seconds": 0,
      "phone": "+998935556677",
      "created_at": "2025-07-03T09:05:00+05:00",
      "updated_at": "2025-07-03T09:05: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 call                                                        | `"call-2001"`                 | The upsert key together with the connection. An absent or over-length `id` fails its page.                                   |
| `lead_id`             | string  | No       | Yes      | max 255 chars; must match an `id` from [`/leads`](/endpoints/leads); **at least one of `lead_id` / `contact_id` must be present**       | `"L-9001"`                    | Links the call to a lead. **Wins when both are present** — see [Association](#association).                                  |
| `contact_id`          | string  | No       | Yes      | max 255 chars; must match an `id` from [`/contacts`](/endpoints/contacts); **at least one of `lead_id` / `contact_id` must be present** | `"c-5003"`                    | Links the call to a contact. Used **only when `lead_id` is absent** — see [Association](#association).                       |
| `responsible_user_id` | string  | No       | Yes      | must match an `id` from [`/users`](/endpoints/users)                                                                                    | `"u-1001"`                    | The owning manager; drives per-manager call volume and connect rate.                                                         |
| `direction`           | string  | No       | Yes      | one of `in` or `out`; any other value, `null`, or an omitted value silently becomes `out`                                               | `"out"`                       | Call direction. An unrecognised value is **not** rejected — it is silently stored as `out`.                                  |
| `duration_seconds`    | integer | No       | No       | integer; a non-numeric value fails the page                                                                                             | `214`                         | Call length in seconds. Defaults to `0` when omitted; `> 0` counts as a connected call. See [Failure modes](#failure-modes). |
| `phone`               | string  | No       | Yes      | max 32 chars; silently truncated to 32                                                                                                  | `"+998901234567"`             | The other party's number.                                                                                                    |
| `created_at`          | string  | No       | Yes      | timestamp (see [Timestamp format](#timestamp-format))                                                                                   | `"2025-07-02T15:10:00+05:00"` | When the call happened. An unparseable value becomes `null`.                                                                 |
| `updated_at`          | string  | Yes      | No       | timestamp (see [Timestamp format](#timestamp-format))                                                                                   | `"2025-07-02T15:10:00+05:00"` | Drives `updated_since` and the watermark. Must be present, parseable, and must advance on every modification.                |

### Association

Every call **must** reference a lead or a contact. The link is resolved in this
exact order:

1. **If `lead_id` is present, the call is linked to that lead.** `lead_id` wins even
   when `contact_id` is also present — `contact_id` is then ignored.
2. **If `lead_id` is absent and `contact_id` is present, the call is linked to that
   contact.**
3. **If neither is present, the call is rejected** and fails its page — see
   [Failure modes](#failure-modes).

<Warning>
  **A call with neither `lead_id` nor `contact_id` fails the page**, and because a
  failed page fails the calls entity, the whole call feed for that run is lost.

  **This differs from [`/tasks`](/endpoints/tasks#association):** a task with
  neither is *accepted* (just linked to nothing), whereas a call with neither is
  *rejected*. Two similar endpoints, opposite behaviour — do not assume one from the
  other. Always send a `lead_id` (preferred) or a `contact_id` on every call.
</Warning>

### Timestamp format

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

* With an explicit offset — `2025-07-02T15:10:00+05:00`.
* With a trailing `Z` for UTC — `2025-07-02T15:10:00Z`.
* With no zone at all — `2025-07-02T15:10:00`, which is read as UTC.
* The date and time may be separated by a `T` or by a single space
  (`2025-07-02 15:10: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 call is still ingested.

<Warning>
  **Consequence when this hits `updated_at`:** a `null` `updated_at` carries no
  modification time, so the call 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 call — 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 on analytics** — do not spend effort
populating them:

* `call_status`, `result`, `recording_link`, `source` (provider), `created_by` —
  telephony extras that are not part of this spec; dropped from the mapped columns.
  Calls keep your full raw payload, so these survive in the raw copy for future
  reprocessing, but they are not surfaced in v1.0 analytics. See
  [Limitations → Fields dropped entirely](/limitations#fields-dropped-entirely).
* Any other key not listed in the [Fields](#fields) table — kept in the raw copy
  only, with no effect on analytics.

## Identity & cross-references

* **Identity:** `id` is the stable primary key. HunterAI upserts by
  `(connection, id)`, so a call's `id` **must never change** — a changed `id` is
  ingested as a brand-new call.
* `lead_id` → resolves against [`/leads`](/endpoints/leads).
* `contact_id` → resolves against [`/contacts`](/endpoints/contacts).
* `responsible_user_id` → resolves against [`/users`](/endpoints/users).
* Association between the call and a lead or contact follows the
  [Association](#association) rule above.

## Incremental sync

`/calls` is incremental. HunterAI sends `updated_since`; return only calls whose
`updated_at` **is greater than or equal to** it (inclusive — re-returning the
boundary call 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 calls ordered by `updated_at` **ascending**, so the watermark
  can advance page by page without skipping an older call on a later page.
* `updated_at` **must change on every modification** of a call. A change that does
  not advance `updated_at` falls at or below the watermark and is never pulled
  again. Calls are usually immutable once logged, so in practice `updated_at`
  equals the log time.

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

## Deletion behaviour

Calls have **no tombstone** — there is no `is_deleted` on a call. Because the
upsert only inserts and updates, a call you delete in your CRM, or simply stop
returning, is **not** removed from HunterAI; 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 calls (up to 100 records) is validated and written **as one
transaction**. The consequences:

* **A single malformed call fails the whole page.** If any call on a page cannot be
  processed — a missing `id`, an over-length `id`, a call with **neither `lead_id`
  nor `contact_id`**, or a **non-numeric `duration_seconds`** — none of the calls on
  that page are saved. This is page-level, not per-record.
* **A failed page fails the calls entity, not the whole sync.** Because `/calls` is
  **optional**, a page that fails takes the calls entity down and reports it failed,
  but the overall sync still succeeds — unlike a required entity, whose failure
  fails the sync. Pages that already committed stay committed, and the next run
  resumes from the watermark.
* **A call with neither `lead_id` nor `contact_id` is rejected** and fails its page.
  This is the opposite of [`/tasks`](/endpoints/tasks#failure-modes), where an
  unassociated task is accepted and merely orphaned.
* **A non-numeric `duration_seconds` fails the page.** `duration_seconds` must be an
  integer; a non-numeric value is rejected outright. This is **stricter than
  `price`** on [`/leads`](/endpoints/leads), which tolerates a numeric string — a
  known inconsistency between the two numeric fields. Send `duration_seconds` as a
  plain integer.
* **`direction` never fails.** An unrecognised, `null`, or omitted `direction` is
  silently stored as `out`, so an inbound call mislabelled this way is counted as
  outbound without any error.
* **An unparseable timestamp does not fail anything** — the field becomes `null` and
  the call is still ingested. See the consequence for `updated_at` above.
* **`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

<Check>
  `/calls` is **optional**. Returning `404` skips it cleanly — the entity reports
  success with zero records and the overall sync still succeeds.
</Check>

Without `/calls`, HunterAI has no call volume per manager and no connect rate.
Leads, contacts, tasks, and attribution are unaffected.
