Skip to main content

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-Key header. 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

  1. Log in to your Zanda Health account
  2. Navigate to Tools → Zanda API Key
  3. Click the button to generate a secure key
  4. 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.

Authentication Header
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:

RegionBase URL
Australiahttps://zandaapi.zandahealth.com/api/v1
United Stateshttps://zandaapi.us.zandahealth.com/api/v1
United Kingdomhttps://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 TypeDescription
application/vnd.zandaapi.hateoas+jsonFull response with HATEOAS hypermedia links for API discoverability
application/vnd.zandaapi+jsonLightweight 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:

Request
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"
Response
{
  "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:

Request
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"
Response
{
  "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:

Request
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"
Response
{
  "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:

Request
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"
Response
{
  "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:MM

Examples:

  • 2025-09-05T15:30:00+10:00 — Australia/Sydney
  • 2025-09-05T01:30:00-04:00 — America/New_York
  • 2025-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:

400 Bad Request — Invalid Timezone
{
  "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:SS

Send 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:59

If a date-time value does not match the expected format, the API returns a 400 Bad Request:

400 Bad Request — Invalid DateTime Format
{
  "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.

Request
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-DD

Pagination

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 resources
  • links - Navigation links (see below). Omitted when the request uses Accept: application/vnd.zandaapi+json (no HATEOAS).
  • page - The current page number (1-based)
  • pageSize - The number of items per page
  • hasNextPage - true when a further page exists
  • nextCursor - In cursor mode, the opaque token for the next page (present when hasNextPage is true). In offset mode the page field 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 - the sort changed 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

Request
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"
Response
{
  "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:direction
  • field - The field name to sort by (see each endpoint for available fields)
  • direction - Either asc (ascending) or desc (descending). Defaults to asc if omitted.

Examples

QueryDescription
?sort=dateCreated:descSort by creation date, newest first
?sort=dateCreated:desc,id:ascSort by creation date descending, then by ID ascending
?sort=idSort 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:

ActionWhat to SendExample
Leave a field unchangedOmit it from the request body(field not present in JSON)
Clear a fieldSet it to null"dateOfBirth": null
Update a fieldProvide 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 Request
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.

CodeDescription
200Success - The request was successful
201Created - A resource was successfully created
400Bad Request - The request was invalid or cannot be served
401Unauthorized - Authentication failed or API key is missing
403Forbidden - You don't have permission to access this resource
404Not Found - The requested resource doesn't exist
406Not Acceptable - The requested media type is not supported
409Conflict - The request conflicts with the current state of the resource
422Unprocessable Entity - The request was well-formed but could not be processed due to a business rule violation
429Too Many Requests - Rate limit exceeded
500Internal Server Error - Something went wrong on our end
503Service 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:

LayerLimit429 response
ApplicationPer 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 ceilingNo rate-limit headers and no timing information
Telling them apart: a 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 TypeThresholdWindow
Per API Key100 requests60 seconds (rolling)
Per IP Address1,000 requests5 minutes (rolling)

Response Headers

The application layer includes rate limit information in response headers for authenticated requests:

HeaderDescriptionPresent
X-RateLimit-LimitMaximum requests allowed in the current window (100 per key, 1,000 per IP)All authenticated responses
X-RateLimit-RemainingRemaining requests in current windowAll authenticated responses
X-RateLimit-ResetUnix epoch timestamp when the window resetsAll authenticated responses
Retry-AfterSeconds 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-After when present — wait the indicated number of seconds before retrying.
  • When a 429 has no Retry-After (infrastructure layer), retry with exponential backoff and jitter.
  • Throttle proactively: watch X-RateLimit-Remaining and 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 429 as 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. a POST that 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:

429 Too Many Requests
{
  "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: