Zanda Health API DocumentationBeta
Overview
Welcome to the Zanda Health API documentation. Zanda Health is a comprehensive practice management system for healthcare practitioners.
The Zanda API is organized around REST principles with predictable resource-oriented URLs, JSON-encoded request/response bodies, and standard HTTP verbs and status codes.
Use this API to access client profiles, practitioners, appointments, invoices, locations, payments, and more from your Zanda Health account.
Security
The Zanda API is designed with security as a priority:
- HTTPS Required - All API requests must be made over HTTPS. Requests made over plain HTTP will be rejected.
- API Key Authentication - Every request must include a valid API key in the
X-API-Keyheader. Requests without authentication will fail. - Keep Keys Secret - Never expose your API keys in client-side code, public repositories, or support tickets.
Authentication
API keys is used to authenticate requests.
Generating an API Key
- Log in to your Zanda Health account
- Navigate to Tools → Zanda API Key
- Click the button to generate a secure key
- Copy your key immediately — for security reasons, it won't be shown again after you close the window
Permissions
API keys provide access to resources such as client profiles, appointments, invoices, practitioners, and more. In future releases, granular permission levels will be available, allowing you to configure read and write access on a per-resource basis.
curl https://api.zandahealth.com/api/v1/practitioners \
-H "X-API-Key: your_api_key_here"Base URL
The Zanda API is deployed across multiple regions. Use the base URL that corresponds to your account's region:
| Region | Base URL |
|---|---|
| Australia | https://zandaapi.zandahealth.com/api/v1 |
| United States | https://zandaapi.us.zandahealth.com/api/v1 |
| United Kingdom | https://zandaapi.uk.zandahealth.com/api/v1 |
Examples in this documentation use api.zandahealth.com as a placeholder. Replace it with your region-specific URL.
Response Structure
The Zanda API uses vendor-specific media types following RFC 6838 standards. This enables content negotiation and allows clients to request the response format that best suits their needs.
Media Types
Use the Accept header to specify your preferred response format. If not specified, the API defaults to application/vnd.zandaapi.hateoas+json.
| Media Type | Description |
|---|---|
application/vnd.zandaapi.hateoas+json | Full response with HATEOAS hypermedia links for API discoverability |
application/vnd.zandaapi+json | Lightweight response without hypermedia links |
Single Resource
Individual resources are wrapped in a data envelope. When using the HATEOAS media type, a links array provides related actions:
{
"data": {
"id": 123,
"firstName": "John",
"lastName": "Doe"
},
"links": [
{
"href": "/api/v1/practitioners/123",
"rel": "self",
"method": "GET"
}
]
}Collection (Paginated)
Collection responses contain an items array and a links array. Pagination is expressed through the links — follow next / previous hrefs to page through results rather than computing URLs from counts.
{
"items": [
{ "data": { ... }, "links": [...] },
{ "data": { ... }, "links": [...] }
],
"links": [
{ "href": "/api/v1/practitioners?page=2&pageSize=5", "rel": "self", "method": "GET" },
{ "href": "/api/v1/practitioners?page=3&pageSize=5", "rel": "next", "method": "GET" }
],
"page": 2,
"pageSize": 5,
"hasNextPage": true
}Quick Start
Here are complete examples to get you started:
With HATEOAS Links
To include hypermedia links for API discoverability, use the application/vnd.zandaapi.hateoas+json media type:
List Resources
Fetching a paginated list of practitioners:
curl -X GET "https://api.zandahealth.com/api/v1/practitioners?page=2&pageSize=10" \
-H "X-API-Key: your_api_key_here" \
-H "Accept: application/vnd.zandaapi.hateoas+json"{
"items": [
{
"data": {
"id": 123,
"firstName": "Jane",
"lastName": "Smith",
"email": "jane.smith@clinic.com"
},
"links": [
{
"href": "/api/v1/practitioners/123",
"rel": "self",
"method": "GET"
}
]
}
],
"links": [
{ "href": "/api/v1/practitioners?page=2&pageSize=10", "rel": "self", "method": "GET" },
{ "href": "/api/v1/practitioners?page=3&pageSize=10", "rel": "next", "method": "GET" }
],
"page": 2,
"pageSize": 10,
"hasNextPage": true
}Get by ID
Fetching a single practitioner by ID:
curl -X GET "https://api.zandahealth.com/api/v1/practitioners/123" \
-H "X-API-Key: your_api_key_here" \
-H "Accept: application/vnd.zandaapi.hateoas+json"{
"data": {
"id": 123,
"firstName": "Jane",
"lastName": "Smith",
"email": "jane.smith@clinic.com"
},
"links": [
{
"href": "/api/v1/practitioners/123",
"rel": "self",
"method": "GET"
}
]
}Without HATEOAS Links
For a lightweight response without hypermedia links, use the application/vnd.zandaapi+json media type:
List Resources
Fetching a paginated list of practitioners:
curl -X GET "https://api.zandahealth.com/api/v1/practitioners?page=1&pageSize=10" \
-H "X-API-Key: your_api_key_here" \
-H "Accept: application/vnd.zandaapi+json"{
"items": [
{
"data": {
"id": 123,
"firstName": "Jane",
"lastName": "Smith",
"email": "jane.smith@clinic.com"
}
}
],
"page": 1,
"pageSize": 10,
"hasNextPage": true
}Get by ID
Fetching a single practitioner by ID:
curl -X GET "https://api.zandahealth.com/api/v1/practitioners/123" \
-H "X-API-Key: your_api_key_here" \
-H "Accept: application/vnd.zandaapi+json"{
"data": {
"id": 123,
"firstName": "Jane",
"lastName": "Smith",
"email": "jane.smith@clinic.com"
}
}Date & Time Handling
Format
All timestamps in API responses use ISO 8601 format with a timezone offset:
YYYY-MM-DDTHH:MM:SS+HH:MMExamples:
2025-09-05T15:30:00+10:00— Australia/Sydney2025-09-05T01:30:00-04:00— America/New_York2025-09-05T05:30:00+00:00— UTC
Timezone
The API supports timezone-aware date and time handling via the X-Time-Zone header. This header accepts IANA timezone identifiers (e.g. Australia/Sydney, America/New_York).
If the header is not provided, the timezone defaults to UTC.
If the header contains an invalid timezone identifier, the API returns a 400 Bad Request:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "Bad Request",
"status": 400,
"detail": "The timezone 'Invalid/Zone' is not a valid IANA timezone identifier.",
"instance": "/api/v1/appointments"
}Inbound date-time values
All date-time values sent to the API — whether as query string filters or in request bodies (e.g. POST/PUT) — must use the exact format:
YYYY-MM-DDTHH:MM:SSSend values in the timezone specified by the X-Time-Zone header, or in UTC if the header is not provided. Do not include timezone information (e.g. Z suffix or +10:00 offset) — the timezone is determined by the X-Time-Zone header. Date-only values (e.g. 2025-09-05) are also not accepted.
?dateFrom=2025-09-05T00:00:00&dateTo=2025-09-05T23:59:59If a date-time value does not match the expected format, the API returns a 400 Bad Request:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"detail": "Please refer to the errors property for additional details.",
"instance": "/api/v1/invoices",
"errors": {
"invoiceDateFrom": [
"'2025-09-05T10:00:00+10:00' is not a valid datetime. Expected format: yyyy-MM-ddTHH:mm:ss (no timezone suffix). Provide the timezone via the X-Time-Zone header using an IANA name (e.g. 'Australia/Sydney'); values are interpreted in that timezone and converted to UTC."
]
}
}Worked example: modifiedSince with X-Time-Zone
List endpoints expose a modifiedSince filter to fetch records created or updated on or after a given point in time. The value is sent as a naive datetime and interpreted in the timezone supplied by the X-Time-Zone header. The API converts it to UTC before the database filter is applied, so two clients in different timezones can ask the same question ("anything modified since 10:00 local time on 1 Jan 2024") and get matching results.
curl -X GET "https://api.zandahealth.com/api/v1/appointments?modifiedSince=2024-01-01T10:00:00" \
-H "X-API-Key: your_api_key_here" \
-H "X-Time-Zone: Australia/Sydney"Sydney is UTC+11 in January (AEDT), so the API filters for records modified at or after 2023-12-31T23:00:00Z in UTC. Sending the same value with X-Time-Zone: America/New_York (UTC-5 in January) would filter from 2024-01-01T15:00:00Z instead. Omitting the header is equivalent to X-Time-Zone: UTC.
Response timestamps (outbound)
Response timestamps always include a timezone offset in ISO 8601 format:
- With timezone header — timestamps are converted to your local time with the corresponding UTC offset:
2025-09-05T15:30:00+10:00 - Without timezone header (UTC fallback) — timestamps are returned in UTC:
2025-09-05T05:30:00+00:00
Date-Only Fields
Some fields (like birth dates) use date-only format:
YYYY-MM-DDPagination
List endpoints support pagination using query parameters:
Query Parameters
page- Page number (default: 1)pageSize- Items per page (default: 10, min: 1, max: 100)
Response Shape
Paginated responses contain the following top-level fields:
items- The current page of resourceslinks- Navigation links (see below). Omitted when the request usesAccept: application/vnd.zandaapi+json(no HATEOAS).page- The current page number (1-based)pageSize- The number of items per pagehasNextPage-truewhen a further page existsnextCursor- In cursor mode, the opaque token for the next page (present whenhasNextPageistrue). In offset mode thepagefield is returned instead.
Navigation Links
The links array on a paginated response may contain up to two entries:
self- The current page (always present when links are included)next- Present when a further page exists
To determine whether you are on the last page, check for the absence of a next link. Follow the next href to page through results rather than constructing URLs yourself — the href preserves all current query parameters.
Cursor Pagination (recommended)
Every list endpoint also supports cursor (keyset) pagination. It is stable under concurrent inserts and deletes and avoids the deep-page slow-downs of offset paging. Pass an opaque cursor token instead of page:
cursor- An opaque, base64-encoded token returned by the previous response. Treat it as a black box — do not parse, decode, or build it yourself.pageSize- Items per page, as in offset mode (default: 10, min: 1, max: 100).
cursor and page are mutually exclusive — sending both returns 400 with code PaginationParametersConflict. To start a cursor walk, omit page (send pageSize alone, or with an empty cursor=). Each response returns a nextCursor field, plus a next link when HATEOAS is enabled; follow it until hasNextPage is false. In cursor mode the page field is omitted from the response.
Cursor errors
A cursor is validated against the request it is replayed on. The following all return 400:
PaginationCursorMalformed- the cursor is not a valid token.PaginationCursorWrongEntity- the cursor was issued by a different endpoint.PaginationCursorSortMismatch- thesortchanged mid-walk.PaginationCursorFilterMismatch- a filter value changed mid-walk.
Keep sort and every filter identical across a cursor walk; to change them, start a fresh walk without a cursor.
Mutable sort-key caveat
Cursor pagination guarantees no skipped or duplicated rows only while the sort value of already-seen rows does not change. On sortable endpoints whose sort column is mutable (for example invoiceDate or totalCharges), a row whose sort value changes mid-walk may be skipped or repeated. For exhaustive ETL / sync, pair the walk with the modifiedSince filter and reconcile by id.
Example
curl -X GET "https://api.zandahealth.com/api/v1/invoices?pageSize=2&cursor=" \
-H "X-API-Key: your_api_key_here" \
-H "Accept: application/vnd.zandaapi.hateoas+json"{
"items": [ /* ...page of resources... */ ],
"links": [
{ "href": "/api/v1/invoices?pageSize=2&cursor=", "rel": "self", "method": "GET" },
{ "href": "/api/v1/invoices?pageSize=2&cursor=eyJ2IjoxLCJlIjoiaW52b2ljZSIsLi4ufQ", "rel": "next", "method": "GET" }
],
"pageSize": 2,
"hasNextPage": true,
"nextCursor": "eyJ2IjoxLCJlIjoiaW52b2ljZSIsLi4ufQ"
}Offset Pagination is Deprecated
The legacy page / pageSize (offset) mode is deprecated in favour of cursor pagination. Offset-mode responses carry RFC 8594 headers — Deprecation: true, a Sunset date, and a Link; rel="successor-version" pointing at this guide — and the page parameter is marked deprecated in the OpenAPI schema. Because the Public API is in Beta, removal will be a dated cutover rather than a version bump. Migrate to cursor before the Sunset date.
Sorting
List endpoints support sorting via the sort query parameter. You can sort by one or more fields, each with an optional direction.
Format
sort=field:direction,field:directionfield- The field name to sort by (see each endpoint for available fields)direction- Eitherasc(ascending) ordesc(descending). Defaults toascif omitted.
Examples
| Query | Description |
|---|---|
?sort=dateCreated:desc | Sort by creation date, newest first |
?sort=dateCreated:desc,id:asc | Sort by creation date descending, then by ID ascending |
?sort=id | Sort by ID ascending (direction defaults to asc) |
Default Sort
If no sort parameter is provided, each endpoint applies its own default sort order (typically by ID descending). Refer to individual endpoint documentation for available sort fields and default behavior.
Partial Updates (JSON Merge Patch)
PATCH endpoints in the Zanda API use RFC 7396 JSON Merge Patch semantics. This allows you to update only the fields you specify, with clear behavior for each case:
| Action | What to Send | Example |
|---|---|---|
| Leave a field unchanged | Omit it from the request body | (field not present in JSON) |
| Clear a field | Set it to null | "dateOfBirth": null |
| Update a field | Provide the new value | "firstName": "Jane" |
Example
The following request updates the first name, clears the date of birth, and leaves all other fields unchanged:
PATCH /api/v1/client-profiles/123
Content-Type: application/merge-patch+json
{
"firstName": "Jane",
"dateOfBirth": null
}Fields not included in the request body (e.g. email, lastName) are not modified. Only the fields you explicitly include are affected.
Error Handling
The Zanda API uses conventional HTTP response codes to indicate the success or failure of an API request.
| Code | Description |
|---|---|
200 | Success - The request was successful |
201 | Created - A resource was successfully created |
400 | Bad Request - The request was invalid or cannot be served |
401 | Unauthorized - Authentication failed or API key is missing |
403 | Forbidden - You don't have permission to access this resource |
404 | Not Found - The requested resource doesn't exist |
406 | Not Acceptable - The requested media type is not supported |
409 | Conflict - The request conflicts with the current state of the resource |
422 | Unprocessable Entity - The request was well-formed but could not be processed due to a business rule violation |
429 | Too Many Requests - Rate limit exceeded |
500 | Internal Server Error - Something went wrong on our end |
503 | Service Unavailable - The feature is not enabled or an upstream dependency is temporarily unavailable |
Rate Limiting
To ensure service stability, the Zanda API applies rate limiting in two independent layers. If you exceed a limit, you'll receive a 429 Too Many Requests response.
Two layers of protection
Requests pass through an infrastructure (DDoS) layer before reaching the application layer. Each can return a 429, but they look different on the wire:
| Layer | Limit | 429 response |
|---|---|---|
| Application | Per API key and per IP (see below) | Includes Retry-After and X-RateLimit-* headers and a JSON problem body |
| Infrastructure (DDoS) | A high-volume safety ceiling | No rate-limit headers and no timing information |
429 from the application layer always carries Retry-After and X-RateLimit-* headers. A 429 without those headers came from the infrastructure layer — it provides no timing, so retry using exponential backoff with jitter rather than a fixed delay.Limits
Application-layer limits use a rolling (sliding) window — for example, no more than 100 requests in any 60-second period, not a fixed bucket that resets on the minute.
| Limit Type | Threshold | Window |
|---|---|---|
| Per API Key | 100 requests | 60 seconds (rolling) |
| Per IP Address | 1,000 requests | 5 minutes (rolling) |
Response Headers
The application layer includes rate limit information in response headers for authenticated requests:
| Header | Description | Present |
|---|---|---|
X-RateLimit-Limit | Maximum requests allowed in the current window (100 per key, 1,000 per IP) | All authenticated responses |
X-RateLimit-Remaining | Remaining requests in current window | All authenticated responses |
X-RateLimit-Reset | Unix epoch timestamp when the window resets | All authenticated responses |
Retry-After | Seconds until capacity frees up (time to recovery) | 429 responses only |
Interpreting Retry-After
Retry-After is the number of seconds until capacity frees up — an absolute time to recovery, not a per-request penalty. Wait for it to elapse, then resume; you only need to honour it once. Retrying earlier neither resets nor extends it, but each early request still returns a 429 with a counting-down value.
For example, Retry-After: 30 means wait 30 seconds, then continue. Polling every second in the meantime simply returns 429 with 30, 29, 28, and so on.
Recommended client behaviour
- Honour
Retry-Afterwhen present — wait the indicated number of seconds before retrying. - When a
429has noRetry-After(infrastructure layer), retry with exponential backoff and jitter. - Throttle proactively: watch
X-RateLimit-Remainingand slow down before it reaches zero. - Stay within the limits — at most ~100 requests per rolling 60-second window per API key, and ~1,000 per rolling 5 minutes per IP (shared by all keys behind the same IP).
- Treat
429as a transient signal rather than a failure. After backing off, retrying is safe for idempotent requests (e.g.GET); for non-idempotent requests (e.g. aPOSTthat creates a resource), confirm whether the previous attempt succeeded before retrying to avoid duplicates.
429 Response Body
When the application layer rate limits a request, it returns an RFC 7807 Problem Details response:
{
"type": "https://tools.ietf.org/html/rfc6585#section-4",
"title": "Too Many Requests",
"status": 429,
"detail": "Rate limit exceeded. Please retry after the specified period.",
"instance": "/api/v1/practitioners"
}A 429 from the infrastructure (DDoS) layer does not include this body or the rate-limit headers — detect it by the absence of Retry-After and X-RateLimit-*.
Download OpenAPI Spec
Download the OpenAPI specification to import into your favorite API client (Postman, Insomnia, etc.) or to generate client SDKs.
Download OpenAPI Spec (JSON)Support
If you have questions or need assistance with the Zanda API, please:
- Visit our Knowledge Base
- Contact our support team