Skip to content

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:

Sandbox:     https://api.staging.spryhealth.care
Production:  https://api.sprypt.com

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:

Authorization: Bearer <access_token>

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

{ "items": [], "page": 1, "page_size": 25, "total": 0, "total_pages": 0 }

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:

GET /v2/appointments/418907?expand=patient,provider,clinic,coverage

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" and id: 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=ACTIVE skips 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

  1. Get an Access Token
  2. List Clinics and List Providers to establish context
  3. Create Patient, then Create Case
  4. List Bookable Slots, then Create Appointment