Skip to content

Conventions

The Shepherd API is consistent across resources. Once you know the conventions on this page, every endpoint reads about the same.

Every endpoint accepts POST with Content-Type: application/json and returns JSON. There are no GET, PATCH, or DELETE verbs. Reads, creates, searches, and deletes are all POSTs, distinguished by the URL path. See Overview for the reasoning.

The exception is updating an existing record: clients, patients, and appointments each expose a PUT .../{id}/update endpoint. Those behave differently enough to deserve their own section.

Update endpoints are PUT, and they mean it. A PUT submits the complete representation of the record, so any editable field you omit is interpreted as an intentional change and is cleared: set to null, empty, or its default. It is not a partial update, and there is no PATCH.

This catches integrators out most often on nested collections. If you send a client update whose clientCoOwner block has no clientCoOwnerPhones array, you are telling the API the co-owner has no phone numbers, and the existing ones are removed.

The safe pattern, and the one we recommend for any synchronisation logic, is three steps:

  1. Read the current record, embedding every nested collection you are about to send back.
  2. Merge your changes into that full representation in memory.
  3. Write the merged whole back to the update endpoint.
GET current state ──► apply your changes ──► PUT the complete object
(with embeds) (in memory) (nothing omitted)

Step 1 is where most data loss originates: if the read did not include a nested collection, the merged object will not contain it either, and the write will clear it. Embed defensively.

A few fields are deliberately exempt from the full-replace rule, either because they are immutable or because they are managed elsewhere:

ResourcePreserved when omittedCannot be changed here
ClientclientStatusId, the clientInfo contact blockHome clinic, system identifiers, deleted status
Patient(none)Home clinic, system identifiers, client association, deleted status

authorizedAgents on a client update is a further exception: it is an explicit edit list rather than a replacement. Each entry adds an agent (omit id), edits one in place (include id), or removes one (set authorizedAgentIdForRemove with a removalNote). Agents you do not reference are left untouched.

Everything not listed above follows the full-replace rule.

Update endpoints return 200 OK with the updated record, not 201 Created. A 404 means the {id} in the path does not resolve within the clinic named by X-Clinic-Id.

List endpoints return a wrapper, not a bare array. The shape is:

{
"item": [ /* the records on this page */ ],
"totalRecords": 1432,
"page": 1,
"recordsPerPage": 10,
"sort": "dateCreated|desc",
"searchQuery": null,
"embed": "clients,clientPhones.phoneType",
"links": []
}

A few things worth noting:

  • item is always an array, even when there is one record.
  • totalRecords is the total across all pages, not just this page.
  • links is currently always empty. Do not depend on it.
  • sort, searchQuery, and embed echo what you sent in the request.

Single-record endpoints return the record directly, without the envelope.

Pagination is driven by two body fields:

FieldDefaultMaximumNotes
page1n/a1-indexed. page: 0 is not valid.
rpp101000Records per page.

There is a hard ceiling of 10,000 items reachable through pagination: page * rpp must not exceed 10000. A request that would cross that line returns HTTP 400. To pull more than 10,000 records, narrow the result set with a filter (typically a date range) and re-paginate.

Most endpoints accept an embed field with comma-separated dot-paths. The API inlines the named relations into the response instead of returning only their IDs.

{
"page": 1,
"rpp": 50,
"embed": "clients,clientPhones.phoneType"
}

Dot-paths walk relationships. clientPhones.phoneType inlines the phones on each client and the phone-type lookup record for each phone. Embeds compound, so be deliberate: a deep embed on a large rpp can return a lot of JSON.

Sort with the sort field. The format is field|direction, where direction is asc or desc:

{ "sort": "dateCreated|desc" }

Only one sort key at a time is honored.

Many list endpoints require a date-range filter. The pairs are:

  • dateCreatedFrom / dateCreatedTo
  • dateUpdatedFrom / dateUpdatedTo

Both ends of the pair are required when filtering, and the range is capped at one calendar month. To pull a longer window, sweep month by month.

Timestamps are ISO 8601 in UTC, for example 2025-04-01T00:00:00Z.

Many records carry a deleted boolean flag. Records flagged deleted: true have been soft-deleted in Shepherd. Filter on deleted in the request body, or check it on each record before processing.

Every record includes:

  • dateCreated: when the record was first created, ISO 8601 UTC.
  • dateUpdated: when the record was last modified, ISO 8601 UTC.

Use dateUpdated for incremental syncs (paired with dateUpdatedFrom/dateUpdatedTo).

The following pulls the second page of 100 patients, sorted by most-recent update, with each patient’s breed inlined:

Terminal window
curl -X POST https://open-api.shepherd.vet/pav2/open-api-patients \
-H "Content-Type: application/json" \
-H "X-Integration-Public-Key: $SHEPHERD_PUBLIC_KEY" \
-H "X-Integration-Private-Key: $SHEPHERD_PRIVATE_KEY" \
-H "X-Clinic-Id: $SHEPHERD_CLINIC_ID" \
-d '{"page":2,"rpp":100,"sort":"dateUpdated|desc","embed":"breed"}'
  • Errors for the two JSON error body shapes and status codes.
  • Rate limits for how aggressive you can be with paging loops.