Developers

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..."
}
  • limit is 1 to 100. The default is 50.
  • While has_more is true, pass next_cursor back as cursor.
  • 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:

  1. First run: list with no updated_since. Walk every page.
  2. Keep the newest updated_at you saw.
  3. Next run: send that time as updated_since. Walk every page again.
  4. 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} answers 404. A sync alone never sees it go. Check the patients you hold now and then, and treat a 404 as removed.
  • Clinics and practitioners. They have no updated_at, so they refuse updated_since with 400 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.