Pagination and sync

List endpoints page with an opaque keyset cursor and support incremental polling. Together those two things are the whole sync story: there are no webhooks.

Query parameters

ParameterTypeMeaning
limitintegerRows per page. Default 50, maximum 200. Out of range values return 400.
cursorstringThe nextCursor value from the previous page. Treat it as opaque; its encoding is not part of the contract.
updated_sinceISO 8601 timestampReturns only rows whose updatedAt is greater than or equal to this value.

Walking a list

Request the first page without a cursor. Every response carries nextCursor. Keep passing it back until it comes back null, which means you have reached the end.

curl -sS "https://app.enrollpilot.com/api/v1/providers?limit=100" \
  -H "Authorization: Bearer epk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
curl -sS "https://app.enrollpilot.com/api/v1/providers?limit=100&cursor=MjAyNi0wNy0xOVQxMzo0NDowMi4xMTVafGNsdjJoOWMxazAwMDFhYmNk" \
  -H "Authorization: Bearer epk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

Full walk in bash

#!/usr/bin/env bash
set -euo pipefail

KEY="$ENROLLPILOT_API_KEY"
URL="https://app.enrollpilot.com/api/v1/providers?limit=200"

while [ -n "$URL" ]; do
  BODY=$(curl -sS "$URL" -H "Authorization: Bearer $KEY")
  echo "$BODY" | jq -c '.data[]'

  CURSOR=$(echo "$BODY" | jq -r '.nextCursor // empty')
  if [ -z "$CURSOR" ]; then
    URL=""
  else
    URL="https://app.enrollpilot.com/api/v1/providers?limit=200&cursor=$CURSOR"
  fi
done

Why cursors and not page numbers

Rows are ordered by updatedAt then id, and the cursor resumes from an exact position rather than counting rows. Offsets would silently skip or repeat records whenever data changed mid walk, and they get slower with every page. Cursors stay fast at any depth.

Incremental sync with updated_since

Store the timestamp of your last successful sync. On the next run pass it as updated_since and page through the result the same way. Only records touched since then come back.

curl -sS "https://app.enrollpilot.com/api/v1/enrollments?updated_since=2026-07-19T00:00:00Z&limit=200" \
  -H "Authorization: Bearer epk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

The boundary is inclusive, so deduplicate by id

updated_since matches rows where updatedAt is greater than or equal to the value, so the row that set your watermark comes back again. That is deliberate: an exclusive boundary would drop rows that changed in the same millisecond.

For the same reason, a record edited while you are part way through a walk can move ahead of your cursor and appear a second time. Treat both cases the same way: upsert by id and overlap your watermark by a minute or two rather than trying to make the boundary exact.

A safe polling loop

  • Record the wall clock time before you start the run and use it as the next watermark only if the entire run succeeded.
  • Upsert every record by id. Never append.
  • Poll on the order of minutes, not seconds. The API allows 120 requests per minute per key.
  • Deletions are not reported. If you need to detect removals, run a full walk periodically and reconcile ids.

Expirables paginate differently

GET /expirables is a computed view across several record types rather than one table, so it has its own contract:

  • Sorted by expiration date, then source type, then source id, with the same limit and cursor behavior.
  • No updated_since. Use within_days instead, which defaults to 365 and accepts up to 730.