Bulk exports and incremental syncs

Recommended patterns for backfills and ongoing sync jobs using updated_at

View as Markdown

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

GoalRecommended request shapeWhy
Initial bulk exportorder_by=updated_at:asc&limit=100Walks oldest to newest so your checkpoint moves forward predictably.
Incremental syncfilter[updated_at][gte]=<checkpoint>&order_by=updated_at:asc&limit=100Re-reads the boundary timestamp so your sync can deduplicate instead of risking a gap.
Latest activity vieworder_by=updated_at:desc&limit=25Returns 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:

$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:

$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:

$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:

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.

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

1{
2 "resource": "contacts",
3 "updated_at": "2026-03-04T18:25:00Z"
4}

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