*** title: Bulk exports and incremental syncs subtitle: Recommended patterns for backfills and ongoing sync jobs using `updated_at` slug: bulk-exports ------------------ 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]=&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_.' \ --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=' \ --header 'Authorization: sk-apik_.' \ --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]=` 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_.' \ --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)