> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.askelephant.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.askelephant.ai/_mcp/server.

# Pagination and filtering

The live collection endpoints use cursor pagination with a hybrid query model:

* `GET /v2/contacts`
* `GET /v2/companies`
* `GET /v2/users`
* `GET /v2/tags`
* `GET /v2/engagements`
* Query params: `limit`, `cursor`, `search` when supported, structured `filter[...]`, and `order_by`

## Query parameters

| Parameter                | Meaning                                                                  |
| ------------------------ | ------------------------------------------------------------------------ |
| `limit`                  | Number of records to return. Defaults to `25`, maximum `100`.            |
| `cursor`                 | Opaque cursor from a previous list response.                             |
| `search`                 | Optional top-level free-text search on resources that support it.        |
| `filter[updated_at][gt]` | Incremental sync filter for records updated strictly after a checkpoint. |
| `order_by`               | Sort order, currently `updated_at:asc` or `updated_at:desc`.             |

## Response fields

| Field         | Meaning                                       |
| ------------- | --------------------------------------------- |
| `data`        | Current page of contact objects               |
| `has_more`    | `true` if another page is available           |
| `next_cursor` | Cursor to use for the next request, or `null` |

## Example

```bash
curl --request GET \
  --url 'https://app.askelephant.ai/api/v2/contacts?limit=2&search=ada&filter[updated_at][gt]=2026-03-01T00:00:00.000Z&order_by=updated_at:desc' \
  --header 'Authorization: sk-apik_<id>.<secret>'
```

```json
{
  "object": "list",
  "data": [
    {
      "object": "contact",
      "id": "cnt_01HQY3JMS2QAXJGX6X7CH7CM6X",
      "first_name": "Ada",
      "last_name": "Lovelace",
      "created_at": "2026-03-01T12:00:00Z",
      "updated_at": "2026-03-04T18:25:00Z"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJpZCI6ImNudF8wMUhRWTNKTVMyUUFYSkdYNlg3Q0g3Q002WCJ9"
}
```

To request the next page:

```bash
curl --request GET \
  --url 'https://app.askelephant.ai/api/v2/contacts?limit=2&cursor=eyJpZCI6ImNudF8wMUhRWTNKTVMyUUFYSkdYNlg3Q0g3Q002WCJ9' \
  --header 'Authorization: sk-apik_<id>.<secret>'
```

## Engagement filters

`GET /v2/engagements` supports these engagement-specific filters on top of the shared list query contract:

| Parameter                              | Meaning                                                                                                                       |
| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `filter[engagement_type][eq\|in]`      | Filter by engagement type.                                                                                                    |
| `filter[processing_status][eq\|in]`    | Filter by processing status.                                                                                                  |
| `filter[company_ids][eq\|in]`          | Filter by AskElephant company IDs.                                                                                            |
| `filter[contact_ids][eq\|in]`          | Filter by AskElephant contact IDs.                                                                                            |
| `filter[crm_associations][eq]`         | Filter by CRM associations using indexed bracket syntax or a JSON-encoded array, for example `company`, `contact`, or `deal`. |
| `filter[start_at][gt\|gte\|lt\|lte]`   | Filter by engagement start time.                                                                                              |
| `filter[updated_at][gt\|gte\|lt\|lte]` | Filter by engagement update time.                                                                                             |
| `expand`                               | Include additional engagement fields such as `companies`, `contacts`, `owner`, `signals`, or `transcript`.                    |

Engagement filtering follows MCP-style bucket semantics:

* `company_ids` and CRM associations that resolve to companies share one company bucket.
* `contact_ids` and CRM associations that resolve to contacts share one participant bucket.
* CRM associations that resolve to deals/projects populate a project bucket.

Values within the same bucket use OR semantics. Populated buckets combine with AND semantics.

For readability, docs examples use indexed bracket syntax. One CRM company association looks like:

```text
filter[crm_associations][eq][0][id]=12345
filter[crm_associations][eq][0][object_type]=company
```

When using raw `filter[...]` query params with `curl`, add `--globoff` so `curl` does not interpret `[]` as URL globbing syntax.

That corresponds to this object:

```json
{ "id": "12345", "object_type": "company" }
```

For `GET /v2/engagements`, the common CRM object filters are:

* CRM company ID: `{ "id": "12345", "object_type": "company" }`
* CRM contact ID: `{ "id": "67890", "object_type": "contact" }`
* CRM deal ID: `{ "id": "deal_abc123", "object_type": "deal" }`

Equivalent JSON-array form:

```text
filter[crm_associations][eq]=[{"id":"123","object_type":"deal"},{"id":"456","object_type":"contact"}]
```

`filter[company_ids][in]` and `filter[contact_ids][in]` support up to 25 values each. `filter[crm_associations][eq]` supports up to 20 objects.

CRM source is inferred from the workspace's connected CRM state. The API resolves local CRM links first, and unresolved CRM associations do not erase otherwise valid direct `company_ids` or `contact_ids` filters in the same bucket.

For a full walkthrough, see [Filter engagements by CRM object IDs](/filtering-engagements-by-crm-object-ids).

## Engagement example

```bash
curl --request GET \
  --globoff \
  --url 'https://app.askelephant.ai/api/v2/engagements?filter[company_ids][in]=cmp_01HQXVB4Y9Y0Q6P0QH5GX2CT7T,cmp_01HQXVB4Y9Y0Q6P0QH5GX2CT7U&filter[contact_ids][eq]=cnt_01HQY3JMS2QAXJGX6X7CH7CM6X&filter[crm_associations][eq][0][id]=deal_123&filter[crm_associations][eq][0][object_type]=deal&filter[updated_at][gt]=2026-03-01T00:00:00.000Z&order_by=updated_at:desc' \
  --header 'Authorization: sk-apik_<id>.<secret>'
```

## Why cursor pagination

Cursor pagination is the live pattern because it is more stable than offset pagination for large, frequently changing datasets.

For incremental sync, keep the last seen `updated_at` checkpoint and continue walking pages with `next_cursor` until `has_more` is `false`.