> 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.

# Bulk exports and incremental syncs

Use the top-level list endpoints with `filter[updated_at][...]`, `order_by`, and cursor pagination to build both one-time exports and recurring sync jobs:

* `GET /v2/contacts`
* `GET /v2/companies`
* `GET /v2/users`
* `GET /v2/tags`
* `GET /v2/engagements`

The shared contract is:

* `limit` defaults to `25` and is capped at `100`
* `order_by` supports `updated_at:asc` and `updated_at:desc`
* `filter[updated_at][gt]`, `filter[updated_at][gte]`, `filter[updated_at][lt]`, and `filter[updated_at][lte]` accept ISO-8601 UTC timestamps
* `next_cursor` continues the current traversal until `has_more` becomes `false`

## Which pattern to use

| Goal                 | Recommended request shape                                                | Why                                                                                    |
| -------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- |
| Initial bulk export  | `order_by=updated_at:asc&limit=100`                                      | Walks oldest to newest so your checkpoint moves forward predictably.                   |
| Incremental sync     | `filter[updated_at][gte]=<checkpoint>&order_by=updated_at:asc&limit=100` | Re-reads the boundary timestamp so your sync can deduplicate instead of risking a gap. |
| Latest activity view | `order_by=updated_at:desc&limit=25`                                      | Returns newest updates first for dashboards or ad hoc inspection.                      |

## Initial bulk export

For a first-time export, start from the beginning and walk forward in ascending `updated_at` order.

1. Request the resource without an `updated_at` filter.
2. Set `order_by=updated_at:asc`.
3. Set `limit=100` unless you need a smaller page size.
4. Follow `next_cursor` until `has_more` is `false`.
5. Upsert each record into your destination system.
6. Record the largest `updated_at` value you processed, but only promote it to your saved checkpoint after the full run finishes successfully.

Example:

```bash
curl --request GET \
  --url 'https://app.askelephant.ai/api/v2/contacts?limit=100&order_by=updated_at:asc' \
  --header 'Authorization: sk-apik_<id>.<secret>' \
  --header 'Accept: application/json'
```

When the response contains a `next_cursor`, continue the same traversal:

```bash
curl --request GET \
  --url 'https://app.askelephant.ai/api/v2/contacts?limit=100&order_by=updated_at:asc&cursor=<next_cursor>' \
  --header 'Authorization: sk-apik_<id>.<secret>' \
  --header 'Accept: application/json'
```

## Incremental sync

For recurring syncs, use your saved checkpoint as the lower bound and keep walking forward.

1. Load the last completed checkpoint for the resource.
2. Request `filter[updated_at][gte]=<checkpoint>` with `order_by=updated_at:asc`.
3. Follow `next_cursor` until the run is complete.
4. Upsert records by `id`.
5. Deduplicate records you have already processed at the checkpoint boundary.
6. After the final page succeeds, save the highest `updated_at` seen during the run as the next checkpoint.

Example:

```bash
curl --request GET \
  --url 'https://app.askelephant.ai/api/v2/contacts?limit=100&filter[updated_at][gte]=2026-03-01T00:00:00.000Z&order_by=updated_at:asc' \
  --header 'Authorization: sk-apik_<id>.<secret>' \
  --header 'Accept: application/json'
```

## Why `gte` is the safer default for syncs

`gt` is available and can reduce duplicate work:

```text
filter[updated_at][gt]=2026-03-01T00:00:00.000Z
```

For most data pipelines, `gte` is the safer default because it intentionally replays the checkpoint boundary. That lets your consumer deduplicate by `id` and `updated_at` instead of depending on a strict handoff at exactly one timestamp value.

If duplicate reads are significantly more expensive than deduplication, you can switch to `gt`. The tradeoff is that your checkpoint handling must be tighter because you are no longer replaying the boundary.

## Recommended checkpoint shape

Persist checkpoint state per resource type. A minimal shape is:

```json
{
  "resource": "contacts",
  "updated_at": "2026-03-04T18:25:00Z"
}
```

If your destination supports idempotent upserts, use that. It makes retries and boundary replays much simpler.

## Practical notes

* Keep separate checkpoints for each resource type because each collection paginates independently.
* Do not mix `cursor` values between different endpoints or different query shapes.
* Keep the same `order_by` and filter values for every page in a single traversal.
* Save checkpoints only after a successful run, not after each page.
* Use `updated_at:desc` for operational views and debugging, not for forward-moving export jobs.

See also: [Pagination and filtering](/pagination)