Pagination and incremental sync
Lists are cursor-paginated. Patients and appointments also take updated_since, so a sync can fetch only what changed.
Every list has one shape#
{
"object": "list",
"data": [ ... ],
"has_more": true,
"next_cursor": "eyJyIjoicGF0aWVudHMi..."
}
limitis 1 to 100. The default is 50.- While
has_moreistrue, passnext_cursorback ascursor. - A cursor is opaque, and belongs to the list that made it. Do not reuse it on another list.
- Keep the other parameters the same from page to page.
Incremental sync#
GET /patients and GET /appointments accept updated_since, an ISO 8601 time with an offset. They return records changed at or after it, oldest change first.
So walking to the last page is a complete sync. The recipe:
- First run: list with no
updated_since. Walk every page. - Keep the newest
updated_atyou saw. - Next run: send that time as
updated_since. Walk every page again. - Upsert by
id. The boundary time is inclusive, so the last record can come back once more.
import json, os, urllib.parse, urllib.request
from datetime import datetime
BASE = "https://api-au.claro.clinic/api/public/v1"
KEY = os.environ["CLARO_API_KEY"]
def get(url):
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {KEY}"})
with urllib.request.urlopen(req) as resp:
return json.load(resp)
def sync_patients(since=None):
"""Upsert every patient changed since `since`; return the new high-water mark."""
params = {"limit": 100}
if since:
params["updated_since"] = since
newest = since
while True:
page = get(f"{BASE}/patients?" + urllib.parse.urlencode(params))
for patient in page["data"]:
upsert(patient) # your code: insert or update by patient["id"]
changed = patient["updated_at"]
if newest is None or datetime.fromisoformat(changed) > datetime.fromisoformat(newest):
newest = changed
if not page["has_more"]:
return newest
params["cursor"] = page["next_cursor"]
Save the returned mark only after the walk finishes. A run that fails part way simply starts again from the old mark.
Compare times as times, not as strings: the fractional seconds can vary in length.
What a sync cannot see yet#
- Removals. A deactivated patient drops out of the list, and
GET /patients/{id}answers404. A sync alone never sees it go. Check the patients you hold now and then, and treat a404as removed. - Clinics and practitioners. They have no
updated_at, so they refuseupdated_sincewith400 parameter_unsupported. Re-list them in full. The lists are short.
Pair it with webhooks#
Webhooks tell you something changed, with the public ID to re-fetch. A sync makes sure you missed nothing. Run both: handle events as they arrive, and sync on a schedule, at least nightly. See Webhooks.