Cursor Pagination for Appointments
Traverse large appointment histories efficiently with a forward-only cursor.
Use GET /v3/appointments/cursor for exports, data pipelines, and backend
integrations that need to read many appointments sequentially. It avoids the growing
database cost of deep page offsets and does not calculate a total count.
Keep GET /v3/appointments when a UI needs a total count or direct access to a page.
The legacy page route remains supported, with an offset ceiling and a stricter request
budget. The deprecated /v3/appointments/filter alias should not be used for new work.
Start a traversal
The first request supplies the filters and traversal order. limit defaults to 20 and
accepts 1 through 100.
curl -G "https://v3.onsched.com/v3/appointments/cursor" \
-H "Authorization: Bearer <token>" \
-H "x-api-key: <company-key>" \
--data-urlencode "resourceIds=<resource-id>" \
--data-urlencode "from=2026-09-01" \
--data-urlencode "to=2026-10-04" \
--data-urlencode "sort=scheduled" \
--data-urlencode "order=asc" \
--data-urlencode "limit=100"The first request accepts the same list filters as GET /v3/appointments:
locationId, resourceIds, serviceIds, customerId, status, from, and to.
Resource, service, and status values may be repeated or comma-separated. Sort is
created or scheduled; order is asc or desc.
{
"success": true,
"data": [
{ "id": "<appointment-id>" }
],
"pageInfo": {
"hasMore": true,
"nextCursor": "<opaque-cursor>"
}
}Continue and stop
When hasMore is true, send only cursor on the next request. Do not resend filters,
sort, order, or limit; they are bound to the cursor. Stop when hasMore is false and
nextCursor is null. Never continue after that terminal response.
var uri = URI.create(apiBase + "/v3/appointments/cursor"
+ "?resourceIds=" + URLEncoder.encode(resourceId, UTF_8)
+ "&from=2026-09-01&to=2026-10-04"
+ "&sort=scheduled&order=asc&limit=100");
while (uri != null) {
var request = HttpRequest.newBuilder(uri)
.header("Authorization", "Bearer " + accessToken)
.header("x-api-key", companyApiKey)
.GET()
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 429) {
Thread.sleep(Long.parseLong(response.headers()
.firstValue("Retry-After").orElse("1")) * 1000L);
continue; // retry the same cursor
}
if (response.statusCode() != 200) {
throw new IllegalStateException(response.body());
}
var page = objectMapper.readTree(response.body());
persist(page.get("data"));
var nextCursor = page.at("/pageInfo/nextCursor");
uri = nextCursor.isNull()
? null
: URI.create(apiBase + "/v3/appointments/cursor?cursor="
+ URLEncoder.encode(nextCursor.asText(), UTF_8));
}Consistency and errors
Cursor walks read current appointment state. They are not a transactionally frozen
snapshot: appointments created, updated, cancelled, or deleted during a walk may affect
later pages. Use appointment webhooks as the authoritative source for ongoing changes;
use cursor traversal for initial or periodic reconciliation.
Cursors expire one hour after the first page and are scoped to the authenticated
Company. Invalid, expired, altered, or wrong-Company cursors return:
{
"success": false,
"message": "Invalid or expired appointment cursor"
}Cursor traversal uses a more relaxed Company request budget than page pagination, but
it still shares bounded database admission. On 429, honor Retry-After and retry the
same cursor rather than starting another parallel walk.
See also
Updated 21 days ago
