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
| Parameter | Type | Meaning |
|---|---|---|
limit | integer | Rows per page. Default 50, maximum 200. Out of range values return 400. |
cursor | string | The nextCursor value from the previous page. Treat it as opaque; its encoding is not part of the contract. |
updated_since | ISO 8601 timestamp | Returns 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
doneWhy 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
limitandcursorbehavior. - No
updated_since. Usewithin_daysinstead, which defaults to 365 and accepts up to 730.