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

> Optional. Lead history — stage moves, notes, and other timeline events per lead. Incremental; return 404 if you don't implement it and the sync still succeeds.

<Info>
  **Optional.** `/events` returns lead-history events — stage changes, notes, and
  other per-lead timeline entries. 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

An event is one entry in a lead's history — a stage change, a note, or another
timeline moment. HunterAI reads `/events` to reconstruct per-lead activity
timelines and stage-change history, so a lead's progress can be replayed step by
step. Every event **must** point at a lead through `lead_id`; there is no
lead-independent event.

## Request

```http theme={null}
GET /api/hunterai/v1/events?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 events per page.                                                                               |
| `page`          | `1`, `2`, … | 1-based. Page until `has_more` is `false`.                                                                                         |
| `updated_since` | timestamp   | Return events whose watermark timestamp — `updated_at` if you send it, otherwise `created_at` — is `>= updated_since` (inclusive). |

You **must** return events ordered **ascending** by the same timestamp that drives
the watermark (`updated_at` if you send it, otherwise `created_at`). HunterAI
advances its watermark page by page and relies on that order; an out-of-order page
can cause older events on a later page to be skipped. Events are typically
immutable, so most sources sort by `created_at`.

## Response envelope

```json 200 OK theme={null}
{
  "data": [
    {
      "id": "ev-7001",
      "lead_id": "L-9001",
      "type": "stage_changed",
      "value_before": "stg-nego",
      "value_after": "stg-won",
      "created_at": "2025-07-02T16:45:00+05:00",
      "updated_at": "2025-07-02T16:45:00+05:00"
    },
    {
      "id": "ev-7002",
      "lead_id": "L-9002",
      "type": "note_added",
      "value_before": { "stage": "stg-nego" },
      "value_after": { "stage": "stg-lost" },
      "created_at": "2025-07-03T11:20:00+05:00",
      "updated_at": "2025-07-03T11:20: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 event                                  | `"ev-7001"`                   | The upsert key together with the connection. An absent or over-length `id` fails its page.                                                                       |
| `lead_id`      | string | Yes      | No       | max 255 chars; must match an `id` from [`/leads`](/endpoints/leads)                                                | `"L-9001"`                    | The lead this event belongs to. A missing `lead_id` fails the page — see [Failure modes](#failure-modes).                                                        |
| `type`         | string | No       | No       | max 100 chars; silently truncated to 100                                                                           | `"stage_changed"`             | Event type label, e.g. `stage_changed`, `note_added`. **Never stored as null** — an absent value becomes an empty string.                                        |
| `value_before` | object | No       | No       | a scalar, object, or array; a scalar is wrapped as `{ "value": <scalar> }`, an object or array is stored unchanged | `{ "stage": "stg-nego" }`     | State before the change (see [note](#value-before-after)). Omit when there is no prior state.                                                                    |
| `value_after`  | object | No       | No       | same as `value_before`                                                                                             | `{ "stage": "stg-won" }`      | State after the change (see [note](#value-before-after)). Omit when there is no resulting state.                                                                 |
| `created_at`   | string | No       | Yes      | timestamp (see [Timestamp format](#timestamp-format))                                                              | `"2025-07-02T16:45:00+05:00"` | When the event occurred, and the watermark fallback. An unparseable value becomes `null`.                                                                        |
| `updated_at`   | string | No       | Yes      | timestamp (see [Timestamp format](#timestamp-format))                                                              | `"2025-07-02T16:45:00+05:00"` | Read only to advance the [watermark](#incremental-sync) — **not stored**. If present it drives incremental selection and ordering; if absent, `created_at` does. |

<Note id="value-before-after">
  **`value_before` / `value_after` accept a scalar, an object, or an array.** A
  scalar such as `"stg-new"` is stored wrapped as `{ "value": "stg-new" }`; an
  object or an array is stored unchanged. There is no fixed schema — send whatever
  best describes the change. HunterAI also keeps the full raw event, so nothing is
  lost. These two fields are not timestamps and never drive the watermark.
</Note>

### Timestamp format

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

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

<Warning>
  **Consequence for the watermark:** an event's watermark timestamp is `updated_at`
  if you send it, otherwise `created_at`. If that timestamp is `null` or absent —
  for example an unparseable `created_at` with no `updated_at` — the event carries
  no position and cannot advance the
  [watermark](/concepts/how-sync-works#ordering-and-the-watermark), so it drops out
  of incremental sync. Keep at least one of the two a valid, advancing timestamp.
</Warning>

## Fields the adapter ignores

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

* `created_by` — dropped from the mapped columns; there is no actor column on an
  event. Attribute activity through the lead and its owner instead. See
  [Limitations → Fields dropped entirely](/limitations#fields-dropped-entirely).
* Any other key not listed in the [Fields](#fields) table — events keep your full
  raw payload, so an unlisted key survives in the raw copy for future
  reprocessing, but it is not mapped and has no effect on v1.0 analytics.

## Identity & cross-references

* **Identity:** `id` is the stable primary key. HunterAI upserts by
  `(connection, id)`, so an event's `id` **must never change** — a changed `id` is
  ingested as a brand-new event.
* `lead_id` → resolves against [`/leads`](/endpoints/leads). Every event **must**
  carry one; there is no lead-independent event.
* Events point at no other entity.

## Incremental sync

`/events` is incremental. HunterAI sends `updated_since`; return only events whose
**watermark timestamp is greater than or equal to** it (inclusive — re-returning
the boundary event is harmless, since the upsert is idempotent).

* The **watermark prefers `updated_at` and falls back to `created_at`.** If you
  send `updated_at`, it drives incremental selection, ordering, and the watermark;
  if you do not, `created_at` does. `updated_at` is read for the watermark only and
  is **not stored** — send it as a requirement, not as data you expect back.
* You **must** return events ordered by the watermark timestamp **ascending**, so
  the watermark can advance page by page without skipping an older event on a later
  page.
* Whichever timestamp drives the watermark **must advance** whenever the event
  changes. An event that never changes keeps a fixed position, which is expected —
  events are typically immutable.

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

## Deletion behaviour

Events have **no tombstone** — there is no `is_deleted` on an event. Because the
upsert only inserts and updates, an event 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 events (up to 100 records) is validated and written **as one
transaction**. The consequences:

* **A single malformed event fails the whole page.** If any event on a page cannot
  be processed — a missing `id`, an over-length `id`, or a **missing `lead_id`** —
  none of the events on that page are saved. This is page-level, not per-record.
* **A failed page fails the events entity, not the whole sync.** Because `/events`
  is **optional**, a page that fails takes the events 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 missing `lead_id` is the most common cause.** `lead_id` is hard-required; an
  event without one fails its page and thus the events entity. Always attach every
  event to a lead.
* **`type` never fails.** An absent `type` becomes an empty string; an over-length
  one is truncated to 100 chars.
* **An unparseable timestamp does not fail anything** — the field becomes `null`
  and the event is still ingested. See the watermark consequence 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>
  `/events` is **optional**. Returning `404` skips it cleanly — the entity reports
  success with zero records and the overall sync still succeeds.
</Check>

Without `/events`, HunterAI cannot reconstruct per-lead activity timelines or
stage-change history. Everything else — leads, their current stage, contacts,
tasks, and calls — is unaffected; only the historical replay of how each lead got
to its current state is lost.
