Query Parameters
Example
Response Format
Every paginated response includes apagination object alongside the data:
Pagination Fields
Iterating Through All Pages
To fetch all records, loop untilpage equals totalPages:
Tips
- Use the largest
pageSizethat makes sense for your use case to minimize the number of requests — up to 500 on/patients,/insurance-policiesand/appointments, up to 100 elsewhere. - On
/bills, passincludeTotals=falseif you do not needtotalCount. The totals cost a second aggregate over the whole filtered set, recomputed on every page of a walk, and they are the dominant cost of that endpoint. Leave them on if you use the count to decide a walk was complete. - The first page is
page=1, notpage=0. - An empty page returns an empty array with
totalCount: 0. - Some list endpoints support search filters (e.g.,
?search=smithon/v3/patients). Pagination applies after filtering.
Delta sync and cursorId
/bills, /patients, /insurance-policies and /appointments accept
updatedSince (and updatedUntil) to return only records that changed.
Always send a full instant with an explicit UTC offset —
2026-08-01T00:00:00Z, not 2026-08-01. On /patients,
/insurance-policies and /appointments a zone-less value is rejected with a
400. /bills predates that rule and still accepts one, but it is resolved
against the database’s timezone rather than as UTC, so the same string can mean a
different instant there than on the other three. Send the offset everywhere and
the difference never arises.
cursorId is available on /patients, /insurance-policies and
/appointments. /bills does not have it yet: unknown query parameters are
ignored rather than rejected, so sending cursorId there is silently dropped and
you get the offset walk described below. Until it does, walk /bills with
updatedUntil pinned to the instant your walk started, keep includeTotals on,
and treat a totalCount that shrinks between pages as a walk you must not trust
— re-read the window instead of advancing your cursor.
Do not walk a delta feed with page/offset. The rows are ordered by the
same change stamp the sync workers rewrite, so a record on an earlier page that
changes mid-walk moves to the end of the results. Everything after it shifts one
position toward the front, and the offset for your next page steps over whichever
record moved into that slot. That record is never returned, and its stamp is
below the cursor your walk ends on, so you never see it again — while the walk
itself looks like it completed cleanly.
Use cursorId instead, always requesting page 1:
cursorId page, pagination.totalCount counts the window that starts at
your cursor, so it shrinks as you walk — 250, then 150, then 50. That is
expected. Stop on a short page rather than on the count, and do not read the
shrink as a signal that the walk is untrustworthy (that signal belongs to the
offset-paged /bills walk described above, where a shrinking total means a row
left the window and may have been skipped).
Two limits to design around:
- A change feed cannot report a deletion. If you prune local records the source no longer has, you still need a periodic full walk; the delta only replaces the reading.
- Each feed tracks its own row.
/appointmentscompareslastSyncedAt, which moves only when an appointment’s own columns change — renaming a patient changespatientLastNamein the appointment response without the appointment appearing in the feed. Likewise/insurance-policiesdoes not move when payer or plan names change, and/patientsdoes not move forinclude=contactorinclude=identitydata. Mirror those from their own endpoints.
