Pagination, idempotency & time zones
Three conventions that are not uniform across families yet. This page says exactly what each call does today, so you do not have to discover it.
Pagination
There is no single paging scheme. Each list endpoint uses one of three styles; its reference entry names the parameters.
Cursor
Only GET /v1/appointments: pass limit (default 100, at most 500) and, for the next page, the next_cursor from the previous response as cursor. next_cursor is null on the last page. The cursor is opaque; a cursor that was not issued by Norvya answers 422 invalid_cursor.
- GET
/v1/appointmentsYour practice's appointments, booked in Norvya or sent by a practice system
Offset and limit
The response carries total, offset, limit and items; ask for the next page with offset + limit until you reach total. Used by the at-risk roster (default 25, at most 100), the panel worklist (50 / 200), invitations (50 / 200), hospital-at-home episodes (50 / 200) and a roster's enrolment (100 / 1000, nested under members).
- GET
/v1/members/at-riskAt-risk roster: your members ranked for a trainer's attention - GET
/v1/panel/worklistYour practice's ranked worklist: who needs attention and why - GET
/v1/invitesYour invitations
Newest N
Some lists take only limit and return the newest items up to a hard cap; you cannot page further back. Examples: a member's messages (100 / 500, with since), webhook deliveries (50 / 200, newest first), rosters, questionnaire batches and the public-health submission and message lists. Other reads are windows rather than pages (days, hours, periods).
- GET
/v1/members/{member_ref}/messagesYour organisation's conversation with a member - GET
/v1/webhooks/{webhook_id}/deliveriesRecent deliveries of one webhook endpoint
Idempotency and safe retries
Idempotency-Key header. The API does not read one. Retry safety comes from natural keys on the writes that have them; the list below is every one there is.Safe to retry
PUT /v1/appointments/ext/{external_id}is an upsert keyed on your client and yourexternal_id.- Observations and workouts carry a per-item
external_id, unique per client. The same id with the same content is counted as aduplicateand nothing is written; the same id with different content, or for another member, is refused (external_id_reused,external_id_conflict); a retracted id can never be reused (external_id_retracted). Retractions answeralready_retractedwhen repeated. - A member's instrument request is idempotent with its
external_id; a questionnaire batch does not send a second request to a member who already has one pending. POST /v1/transitions/adtis idempotent on MSH-10: a control id already received is counted underduplicates. A message without one is refused (missing_control_id).- An invitation to an address that already has an open one returns that invitation with
deduplicated: trueand sends no second email. - A syndromic visit is keyed on your visit number: recording it again updates it.
- PUT
/v1/appointments/ext/{external_id}Create or update an appointment by YOUR appointment id - POST
/v1/members/{member_ref}/observationsWrite measurements to a member's record - POST
/v1/transitions/adtSend an HL7 v2 ADT batch from your feed
Detectable but not transparent
Some writes refuse a duplicate with 409 instead of returning the original: a post-discharge episode for the same patient and discharge date (episode_exists), sending the same syndromic message (duplicate_message), transmitting a case report twice (already_sent), enrolling a patient already enrolled (already_enrolled), opening a second hospital-at-home episode (episode_already_open). After a timeout, treat that 409 as “it went through” and read the resource.
Not idempotent
Each call writes again: member messages (capped at five per member per day, then 429), webhook registrations, population exports (one running export per client at a time: 409 export_in_progress), panel imports, roster uploads and claims imports (each upload stages a new import). Before retrying one of these after a timeout, list what exists.
Time zones and dates
- Send instants with an offset. Inputs such as an appointment's
start, an observation'seffective_atorcollected_at, a workout'sstarted_atand a transition contact'scontact_atmust carryZor an offset (2026-10-14T09:30:00-04:00). A naive time is refused with a…_timezone_required(orcontact_at_needs_offset) code rather than guessed. - Instants come back in UTC. Most families write ISO 8601 with a trailing
Z. A few older fields still omit theZ(for example in claims, dermatology and some public-health responses): every timestamp without an offset is UTC. Appointments also return the practice-local time with its offset, and theirpractice_zone. - Dates are dates. Fields such as
as_of,discharge_date, period bounds and the appointmentsfrom/towindow areYYYY-MM-DD, on the practice's calendar where the reference says so. - Choosing a zone. The care-transitions enrol, worklist and episode reads take
tz(an IANA name, default UTC) for day boundaries; appointments take atimezonefield that defaults to the practice's zone. Member-facing “today” (life stages, behavioral health, documents) is the member's own local date.