API v2 Overview¶
v2 is a normalized, RESTful contract over the same data v1 exposes, with consistent naming, status codes and error shapes.
v2 is in beta
v2 is a preview. Field names, shapes and endpoints may still change, and it does not yet cover everything v1 does. API v1 is the current, supported version — build production integrations there for now, and treat this reference as the direction of travel. Talk to Spry before depending on v2 in production.
Moving from v1? Read Migrating from v1 first — v2 isn't a drop-in replacement.
Base URL¶
v2 has its own host and doesn't sit behind the v1 /apis gateway prefix:
There's no gateway prefix, so the full URL is the host plus the documented path:
https://api.sprypt.com/v2/patients. Examples in this reference use {base_url} for the host.
Authentication¶
OAuth 2.0 client credentials. Exchange your client_id and client_secret for a bearer token
with Get an Access Token, then send it on every request:
Tokens last one hour. Contact Spry to be issued credentials.
What changed from v1¶
The differences that'll affect your client code, in rough order of impact.
Responses are the resource, not an envelope¶
v1 wraps every response in { code: 2000, data: {...}, message: "Success" } and returns 200
whatever the outcome. In v2 the HTTP status is authoritative and a success body is the resource
itself.
// v1
{ "code": 2000, "data": { "id": "63120" }, "message": "Success" }
// v2 — 200 OK
{ "patient_id": "63120" }
So if (response.code === 2000) becomes if (response.ok), and response.data.x becomes
response.x.
Errors have a machine-readable code¶
{
"code": "not_found",
"message": "Patient 63120 not found",
"errors": [{ "field": "patient_id", "message": "must be a string" }]
}
code is a stable string slug paired with the matching HTTP status, replacing v1's numeric codes
above 2000. errors[] names the offending fields on a validation failure and is empty when there's
nothing field-specific to report.
| Status | code |
|---|---|
400 Bad Request |
bad_request |
401 Unauthorized |
unauthorized |
403 Forbidden |
forbidden |
404 Not Found |
not_found |
409 Conflict |
conflict |
412 Precondition Failed |
precondition_failed |
428 Precondition Required |
precondition_required |
429 Too Many Requests |
rate_limited |
500 Internal Server Error |
internal_error |
502 Bad Gateway |
upstream_error |
One name per concept¶
v1 used several names for the same thing — doctor, therapist, physician, provider, physio, staff and user could all mean the same record. v2 fixes one term per concept:
| Concept | v2 term | Names it replaces |
|---|---|---|
| Clinician | provider |
doctor, therapist, physician, physio |
| Location | clinic |
branch, location |
| Tenant | organisation |
org, organization |
| Visit type | appointment_type |
category, appointment category |
| Administrative gender | gender |
sex |
Wire format is snake_case throughout, and timestamps are ISO-8601 UTC. Booleans are unprefixed
(active, enabled, billable), not is_*. v1's mangled keys — logo_u_r_l,
signature_u_r_l — are logo_url and signature_url.
Methods and status codes are conventional¶
| Operation | v1 | v2 |
|---|---|---|
| Create | POST, 200 with envelope |
POST, 201 with a Location header |
| Update | POST /resource/{id} |
PATCH /resource/{id}, 200 with the updated resource |
| Delete | DELETE, 200 with envelope |
DELETE, 204 with no body |
| Lifecycle action | verb in the path, e.g. /appointment-cancel |
sub-resource POST, e.g. /appointments/{id}/cancel |
PATCH is a JSON Merge Patch¶
PATCH follows RFC 7386: an omitted field is left alone, an explicit null clears it, and an
array replaces the existing one wholesale. Bodies are typed and validated, unlike the untyped JSON
objects v1 accepted on case and authorization updates.
Pagination is one envelope¶
List endpoints take ?page= and ?page_size= (default 25, max 100). v1 had two different
pagination shapes. The onboarding endpoints are the exception — they're cursor-paginated.
References are ids, resolved server-side¶
Responses carry ids, not nested copies of other resources. Where a v1 payload embedded a
denormalized copy — insurance cards on a case, providers on a clinic — v2 stores a pointer, so the
same record can't drift between places. Pass ?expand= on the endpoints that support it to hydrate
referenced records in one call:
Idempotency¶
Send an Idempotency-Key header on any POST to make it safe to retry.
Rate limiting¶
Requests are limited per client token. Exceeding the limit returns 429 with
code: "rate_limited"; back off and retry.
Onboarding vs operational records¶
Worth knowing before you write code against clinics or providers.
A clinic or provider exists as two records at two lifecycle stages: an onboarding record (the configuration being assembled, addressed by a UUID) and an operational record (the live row that appointments, patients and cases reference, addressed by an integer id). Creation goes through onboarding, which is what sets up the feature configs, appointment types and defaults a clinic needs to be usable.
On the wire:
- An unpublished record has
status: "ONBOARDING"andid: null. - The null id is the enforcement mechanism, not just a marker: there's no value you could put in an appointment payload, so an unpublished clinic or provider can't be booked against.
- Address such a record by its
onboarding_clinic_id/onboarding_provider_id, e.g.GET /v2/clinics/{onboarding_clinic_id}?onboarding=true. - List endpoints merge both surfaces and report a published record once.
?status=ACTIVEskips the onboarding lookup entirely.
Publishing isn't part of these APIs. A record you create stays ONBOARDING until it's
published separately. If you need a self-service path from create to live, raise it with Spry.
Read-after-write¶
Patient reads lag writes slightly. A write is durable as soon as it returns, but a following
GET may briefly show the previous values — typically under two seconds.
Write responses return the record as written rather than re-reading it, so if you use the response body you always see your own change. Do that instead of polling a read.
Where to start¶
- Get an Access Token
- List Clinics and List Providers to establish context
- Create Patient, then Create Case
- List Bookable Slots, then Create Appointment