Patients
A patient is the pet: a dog, cat, rabbit, parrot, or any other animal the clinic treats. Every patient belongs to one or more clients (the pet owners), and patients are the anchor for clinical workflows: appointments, SOAP records, vitals, prescriptions, imaging, and attached files all hang off a patient ID.
For file uploads, the API uses a three-step flow with an S3 presigned URL so binaries never transit the Shepherd application servers; see the file uploads section below.
Schema
Section titled “Schema”Authoritative schemas live in the live Swagger UI. The fields below are a representative subset for orientation.
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier for the patient. |
name | string | Patient name (e.g. “Biscuit”). |
identifier | string | Short human-facing identifier shown in the Shepherd UI. |
breedId | string | Reference to a breed from the breeds lookup. |
sexId | string | Reference to a patient sex from the sexes lookup. |
statusId | string | Reference to a patient status from the statuses lookup. |
color | string | Colour or markings, free text (e.g. “Brown”). |
dateOfBirth | string | Date of birth. Stored as a date; any time component is ignored. |
microchipNumber | string | Microchip number, free text. |
rabiesTag | string | Rabies tag number, free text. |
alerts | string | Important handling notes surfaced in the chart. |
additionalText | string | Free-text additional information. |
clients | array | Owning clients. Populated with embed=clients. |
breed / patientSex / patientStatus | object | Expanded lookup records. null unless embedded. |
dateDeceased | string | Date of death, or null. |
dateCreated | string | ISO 8601 UTC timestamp of creation. |
dateUpdated | string | ISO 8601 UTC timestamp of last update. |
deleted | boolean | Soft-delete flag. |
Weight is not a scalar field on the patient either. It is a separate series of weight entries; one can be created alongside the patient (see Create) and further entries are recorded through the patient-weight endpoint.
List / search
Section titled “List / search”/pav2/open-api-patients Returns a paginated collection of patients for the clinic. Use searchQuery for free-text matching against name, microchip number, and rabies tag, embed to pull related data inline (for example embed: "clients,clients.clientPhones"), and the standard page, rpp, sort, and dateCreatedFrom / dateCreatedTo parameters from Conventions. Narrow by owner with clientIds, by status with patientStatusIds, or drop deceased patients with excludeDeceased.
Bound every list: a searchQuery, an id filter such as clientIds, or a dateCreated / dateUpdated window of one calendar month or less. Deleted patients are included unless you send "isDeleted": false. The example below searches by name.
Example
Section titled “Example”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": 1, "rpp": 50, "sort": "name|asc", "searchQuery": "biscuit", "embed": "clients" }'import osimport requests
resp = requests.post( "https://open-api.shepherd.vet/pav2/open-api-patients", headers={ "Content-Type": "application/json", "X-Integration-Public-Key": os.environ["SHEPHERD_PUBLIC_KEY"], "X-Integration-Private-Key": os.environ["SHEPHERD_PRIVATE_KEY"], "X-Clinic-Id": os.environ["SHEPHERD_CLINIC_ID"], }, json={"page": 1, "rpp": 50, "sort": "name|asc", "searchQuery": "biscuit", "embed": "clients"}, timeout=30,)resp.raise_for_status()for patient in resp.json()["item"]: print(patient["id"], patient["name"], patient["breedId"])using System.Net.Http;using System.Net.Http.Json;
using var http = new HttpClient();http.DefaultRequestHeaders.Add("X-Integration-Public-Key", Environment.GetEnvironmentVariable("SHEPHERD_PUBLIC_KEY"));http.DefaultRequestHeaders.Add("X-Integration-Private-Key", Environment.GetEnvironmentVariable("SHEPHERD_PRIVATE_KEY"));http.DefaultRequestHeaders.Add("X-Clinic-Id", Environment.GetEnvironmentVariable("SHEPHERD_CLINIC_ID"));
var response = await http.PostAsJsonAsync( "https://open-api.shepherd.vet/pav2/open-api-patients", new { page = 1, rpp = 50, sort = "name|asc", searchQuery = "biscuit", embed = "clients" });response.EnsureSuccessStatusCode();<?php$ch = curl_init('https://open-api.shepherd.vet/pav2/open-api-patients');curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'X-Integration-Public-Key: ' . getenv('SHEPHERD_PUBLIC_KEY'), 'X-Integration-Private-Key: ' . getenv('SHEPHERD_PRIVATE_KEY'), 'X-Clinic-Id: ' . getenv('SHEPHERD_CLINIC_ID'), ], CURLOPT_POSTFIELDS => json_encode([ 'page' => 1, 'rpp' => 50, 'sort' => 'name|asc', 'searchQuery' => 'biscuit', 'embed' => 'clients', ]),]);$body = curl_exec($ch);curl_close($ch);$payload = json_decode($body, true);const response = await fetch('https://open-api.shepherd.vet/pav2/open-api-patients', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Integration-Public-Key': process.env.SHEPHERD_PUBLIC_KEY, 'X-Integration-Private-Key': process.env.SHEPHERD_PRIVATE_KEY, 'X-Clinic-Id': process.env.SHEPHERD_CLINIC_ID, }, body: JSON.stringify({ page: 1, rpp: 50, sort: 'name|asc', searchQuery: 'biscuit', embed: 'clients' }),});if (!response.ok) { throw new Error(`Shepherd API ${response.status}: ${await response.text()}`);}const payload = await response.json();Response
Section titled “Response”{ "item": [ { "id": "a1b2c3d4-e5f6-7890-abcd-ef0123456789", "name": "Biscuit", "identifier": "K2X9MV1", "breedId": "1a2b3c4d-5e6f-789a-bcde-0123456789ab", "sexId": "6a5b4c3d-2e1f-0a9b-8765-4321fedcba09", "statusId": "9f8e7d6c-5b4a-3210-a9f8-fedcba098765", "color": "Brown", "dateOfBirth": "2019-04-12T00:00:00Z", "microchipNumber": "985141001234567", "rabiesTag": "R123456", "dateDeceased": null, "clients": [], "breed": null, "patientSex": null, "patientStatus": null, "dateCreated": "2026-03-14T18:22:11Z", "dateUpdated": "2026-05-10T14:00:00Z", "deleted": false } ], "totalRecords": 421, "page": 1, "recordsPerPage": 50, "sort": "name|asc", "searchQuery": null, "embed": "clients", "links": []}breed, patientSex, and patientStatus are the expanded lookup records. They stay null until you name them in embed; the *Id fields are always present.
Create
Section titled “Create”/pav2/open-api-patients/write Creates a new patient under an existing client.
Every reference field is a lookup ID, not a name. There is no free-text species, breed, or sex on this endpoint; resolve each one against its lookup first (see Resolving the lookup IDs below).
Required on create:
| Field | Notes |
|---|---|
name | The patient’s name. |
clientId | The owning client. Must already exist. |
breedId | From the breeds lookup. Determines the species. |
sexId | From the patient-sexes lookup. |
statusId | From the patient-statuses lookup. |
dateOfBirth | Must not be in the future. Stored as a date; any time component is ignored. |
Everything else is optional: color, alerts, additionalText, microchipNumber, rabiesTag, discountId, patientCprstatusId, patientCPRStatusNote, preferredProviderIds, referralSourceIds, and patientWeight.
Unless the Shepherd account is configured as a group account, you do not need to supply a home clinic; the clinic in X-Clinic-Id is used. Default provider (preferredProviderIds) and every other field not listed above are optional.
Resolving the lookup IDs
Section titled “Resolving the lookup IDs”breedId, sexId, and statusId must come from the lookups on the clinic’s own environment. Lookup IDs differ between sandbox and production, and between clinics for breeds, so IDs harvested from one environment will not validate in another. This is the most common cause of a rejected create.
| Field | Source | Scoping |
|---|---|---|
breedId | POST /pav2/open-api-breeds | Clinic-scoped. Pass getBreedsByClinicId: true for the clinic’s own list, and isLegacy: false to skip retired entries. Filter by speciesId to offer a species-then-breed picker. |
sexId | POST /pav2/open-api-patient-sexes | Not clinic-scoped, and there is no legacy flag. The list is the same for every clinic in an environment. |
statusId | POST /pav2/open-api-patient-statuses | Not clinic-scoped, and there is no legacy flag. |
Cache these; they change rarely. See Lookups for the caching guidance.
Recording an initial weight
Section titled “Recording an initial weight”Weight is not a field on the patient. To capture an intake weight at the same time as the record, include the nested patientWeight object; weight and weightUnitId are both required within it, and date and note are optional.
{ "patientWeight": { "weight": 5.0, "weightUnitId": "a28f2c7e-5f45-4a32-ae3f-4e87c49a47a3", "date": "2026-08-20T10:12:45Z", "note": "Intake weight" }}A weight of zero or less is rejected, as is a value that fails validation for the unit you named.
Example
Section titled “Example”curl -X POST https://open-api.shepherd.vet/pav2/open-api-patients/write \ -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 '{ "name": "Biscuit", "clientId": "e45a6e12-3a1c-4e7d-9fd2-3473a4d8a7a6", "breedId": "f2c19d83-9f87-4f6f-a290-d08e59d7d29e", "sexId": "5a8d0cfc-c7c4-44a0-bdab-44f4e5f6b219", "statusId": "8fc9c6d1-a829-4b93-a5ea-1c83db96a1b4", "dateOfBirth": "2019-04-12T00:00:00Z", "color": "Chocolate", "patientWeight": { "weight": 5.0, "weightUnitId": "a28f2c7e-5f45-4a32-ae3f-4e87c49a47a3" } }'import osimport requests
resp = requests.post( "https://open-api.shepherd.vet/pav2/open-api-patients/write", headers={ "Content-Type": "application/json", "X-Integration-Public-Key": os.environ["SHEPHERD_PUBLIC_KEY"], "X-Integration-Private-Key": os.environ["SHEPHERD_PRIVATE_KEY"], "X-Clinic-Id": os.environ["SHEPHERD_CLINIC_ID"], }, json={ "name": "Biscuit", "clientId": "e45a6e12-3a1c-4e7d-9fd2-3473a4d8a7a6", "breedId": "f2c19d83-9f87-4f6f-a290-d08e59d7d29e", "sexId": "5a8d0cfc-c7c4-44a0-bdab-44f4e5f6b219", "statusId": "8fc9c6d1-a829-4b93-a5ea-1c83db96a1b4", "dateOfBirth": "2019-04-12T00:00:00Z", "color": "Chocolate", "patientWeight": { "weight": 5.0, "weightUnitId": "a28f2c7e-5f45-4a32-ae3f-4e87c49a47a3", }, }, timeout=30,)resp.raise_for_status()patient = resp.json()using System.Net.Http;using System.Net.Http.Json;
using var http = new HttpClient();http.DefaultRequestHeaders.Add("X-Integration-Public-Key", Environment.GetEnvironmentVariable("SHEPHERD_PUBLIC_KEY"));http.DefaultRequestHeaders.Add("X-Integration-Private-Key", Environment.GetEnvironmentVariable("SHEPHERD_PRIVATE_KEY"));http.DefaultRequestHeaders.Add("X-Clinic-Id", Environment.GetEnvironmentVariable("SHEPHERD_CLINIC_ID"));
var response = await http.PostAsJsonAsync( "https://open-api.shepherd.vet/pav2/open-api-patients/write", new { name = "Biscuit", clientId = "e45a6e12-3a1c-4e7d-9fd2-3473a4d8a7a6", breedId = "f2c19d83-9f87-4f6f-a290-d08e59d7d29e", sexId = "5a8d0cfc-c7c4-44a0-bdab-44f4e5f6b219", statusId = "8fc9c6d1-a829-4b93-a5ea-1c83db96a1b4", dateOfBirth = "2019-04-12T00:00:00Z", color = "Chocolate", patientWeight = new { weight = 5.0, weightUnitId = "a28f2c7e-5f45-4a32-ae3f-4e87c49a47a3", }, });response.EnsureSuccessStatusCode();<?php$ch = curl_init('https://open-api.shepherd.vet/pav2/open-api-patients/write');curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'X-Integration-Public-Key: ' . getenv('SHEPHERD_PUBLIC_KEY'), 'X-Integration-Private-Key: ' . getenv('SHEPHERD_PRIVATE_KEY'), 'X-Clinic-Id: ' . getenv('SHEPHERD_CLINIC_ID'), ], CURLOPT_POSTFIELDS => json_encode([ 'name' => 'Biscuit', 'clientId' => 'e45a6e12-3a1c-4e7d-9fd2-3473a4d8a7a6', 'breedId' => 'f2c19d83-9f87-4f6f-a290-d08e59d7d29e', 'sexId' => '5a8d0cfc-c7c4-44a0-bdab-44f4e5f6b219', 'statusId' => '8fc9c6d1-a829-4b93-a5ea-1c83db96a1b4', 'dateOfBirth' => '2019-04-12T00:00:00Z', 'color' => 'Chocolate', 'patientWeight' => [ 'weight' => 5.0, 'weightUnitId' => 'a28f2c7e-5f45-4a32-ae3f-4e87c49a47a3', ], ]),]);$body = curl_exec($ch);curl_close($ch);$patient = json_decode($body, true);const response = await fetch('https://open-api.shepherd.vet/pav2/open-api-patients/write', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Integration-Public-Key': process.env.SHEPHERD_PUBLIC_KEY, 'X-Integration-Private-Key': process.env.SHEPHERD_PRIVATE_KEY, 'X-Clinic-Id': process.env.SHEPHERD_CLINIC_ID, }, body: JSON.stringify({ name: 'Biscuit', clientId: 'e45a6e12-3a1c-4e7d-9fd2-3473a4d8a7a6', breedId: 'f2c19d83-9f87-4f6f-a290-d08e59d7d29e', sexId: '5a8d0cfc-c7c4-44a0-bdab-44f4e5f6b219', statusId: '8fc9c6d1-a829-4b93-a5ea-1c83db96a1b4', dateOfBirth: '2019-04-12T00:00:00Z', color: 'Chocolate', patientWeight: { weight: 5.0, weightUnitId: 'a28f2c7e-5f45-4a32-ae3f-4e87c49a47a3', }, }),});if (!response.ok) { throw new Error(`Shepherd API ${response.status}: ${await response.text()}`);}const patient = await response.json();Update
Section titled “Update”/pav2/open-api-patients/{id}/update Updates an existing patient. This is a PUT, so it is a full replacement: send the complete patient representation, not just the fields you changed. Any editable field omitted from the body is cleared. See Updating records for the general rule and the read-merge-write pattern.
Required on update: name, breedId, sexId, statusId, dateOfBirth, and color. Note that color is required here even though it is optional on create. dateOfBirth cannot be in the future.
No field is kept when omitted. Leave out alerts, additionalText, microchipNumber, rabiesTag, patientCprstatusId, or patientCPRStatusNote and it is cleared; leave out preferredProviderIds or referralSourceIds and the patient’s preferred providers or referral sources are removed.
Two of those are named differently on the patient you read, so map them when you merge:
| Update field | Take it from the record you read |
|---|---|
preferredProviderIds | patientPreferredProviders[].providerId (embed patientPreferredProviders) |
referralSourceIds | patientReferralSources[].referralSourceId (embed patientReferralSources) |
Two differences from create are worth calling out:
clientIdis not accepted. The patient’s client association cannot be changed through this endpoint, along with the home clinic, system identifiers, and deleted status.patientWeightis not accepted. Weight entries are their own series; record new ones through the patient-weight endpoint rather than through the patient update.
Returns 200 OK with the updated patient, not 201. A 404 means the {id} does not resolve within the clinic named by X-Clinic-Id.
Example: read, merge, write
Section titled “Example: read, merge, write”The example changes one field, alerts, and carries every other editable field through from the current record.
# 1. Read the current patient, embedding the links you will resend.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 '{ "ids": "a1b2c3d4-e5f6-7890-abcd-ef0123456789", "rpp": 1, "embed": "patientPreferredProviders,patientReferralSources" }' > current-patient.json
# 2. Build merged-patient.json from item[0]: every update field copied from# the record (mapping the two id lists), with your change applied.# 3. PUT the complete object back.curl -X PUT https://open-api.shepherd.vet/pav2/open-api-patients/a1b2c3d4-e5f6-7890-abcd-ef0123456789/update \ -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 @merged-patient.jsonimport osimport requests
BASE = "https://open-api.shepherd.vet/pav2"HEADERS = { "Content-Type": "application/json", "X-Integration-Public-Key": os.environ["SHEPHERD_PUBLIC_KEY"], "X-Integration-Private-Key": os.environ["SHEPHERD_PRIVATE_KEY"], "X-Clinic-Id": os.environ["SHEPHERD_CLINIC_ID"],}patient_id = "a1b2c3d4-e5f6-7890-abcd-ef0123456789"
# Update fields with the same name on the record you read.SAME_NAME = ( "name", "breedId", "sexId", "statusId", "dateOfBirth", "color", "microchipNumber", "rabiesTag", "alerts", "additionalText", "patientCprstatusId", "patientCPRStatusNote",)
# 1. Read, embedding the links you intend to resend.read = requests.post( f"{BASE}/open-api-patients", headers=HEADERS, json={ "ids": patient_id, "rpp": 1, "embed": "patientPreferredProviders,patientReferralSources", }, timeout=30,)read.raise_for_status()current = read.json()["item"][0]
# 2. Merge: copy every update field from the record, then apply the change.payload = {f: current[f] for f in SAME_NAME if f in current}payload["preferredProviderIds"] = [ p["providerId"] for p in current.get("patientPreferredProviders") or []]payload["referralSourceIds"] = [ r["referralSourceId"] for r in current.get("patientReferralSources") or []]payload["alerts"] = "Allergic to penicillin" # the actual change
# 3. Write the complete object back.write = requests.put( f"{BASE}/open-api-patients/{patient_id}/update", headers=HEADERS, json=payload, timeout=30,)write.raise_for_status()updated = write.json()using System.Net.Http.Json;using System.Text.Json.Nodes;
const string Base = "https://open-api.shepherd.vet/pav2";var patientId = "a1b2c3d4-e5f6-7890-abcd-ef0123456789";
// Update fields with the same name on the record you read.string[] sameName =[ "name", "breedId", "sexId", "statusId", "dateOfBirth", "color", "microchipNumber", "rabiesTag", "alerts", "additionalText", "patientCprstatusId", "patientCPRStatusNote",];
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };http.DefaultRequestHeaders.Add("X-Integration-Public-Key", Environment.GetEnvironmentVariable("SHEPHERD_PUBLIC_KEY"));http.DefaultRequestHeaders.Add("X-Integration-Private-Key", Environment.GetEnvironmentVariable("SHEPHERD_PRIVATE_KEY"));http.DefaultRequestHeaders.Add("X-Clinic-Id", Environment.GetEnvironmentVariable("SHEPHERD_CLINIC_ID"));
// 1. Read, embedding the links you intend to resend.var read = await http.PostAsJsonAsync($"{Base}/open-api-patients", new{ ids = patientId, rpp = 1, embed = "patientPreferredProviders,patientReferralSources",});read.EnsureSuccessStatusCode();var current = (await read.Content.ReadFromJsonAsync<JsonObject>())!["item"]![0]!.AsObject();
// 2. Merge: copy every update field from the record, then apply the change.var payload = new JsonObject();foreach (var field in sameName) if (current.TryGetPropertyValue(field, out var value)) payload[field] = value?.DeepClone();payload["preferredProviderIds"] = new JsonArray( (current["patientPreferredProviders"]?.AsArray() ?? []) .Select(p => (JsonNode?)p!["providerId"]!.DeepClone()).ToArray());payload["referralSourceIds"] = new JsonArray( (current["patientReferralSources"]?.AsArray() ?? []) .Select(r => (JsonNode?)r!["referralSourceId"]!.DeepClone()).ToArray());payload["alerts"] = "Allergic to penicillin"; // the actual change
// 3. Write the complete object back.var write = await http.PutAsJsonAsync($"{Base}/open-api-patients/{patientId}/update", payload);write.EnsureSuccessStatusCode();<?php$base = 'https://open-api.shepherd.vet/pav2';$patientId = 'a1b2c3d4-e5f6-7890-abcd-ef0123456789';$headers = [ 'Content-Type: application/json', 'X-Integration-Public-Key: ' . getenv('SHEPHERD_PUBLIC_KEY'), 'X-Integration-Private-Key: ' . getenv('SHEPHERD_PRIVATE_KEY'), 'X-Clinic-Id: ' . getenv('SHEPHERD_CLINIC_ID'),];
// Update fields with the same name on the record you read.$sameName = [ 'name', 'breedId', 'sexId', 'statusId', 'dateOfBirth', 'color', 'microchipNumber', 'rabiesTag', 'alerts', 'additionalText', 'patientCprstatusId', 'patientCPRStatusNote',];
// 1. Read, embedding the links you intend to resend.$ch = curl_init("$base/open-api-patients");curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_HTTPHEADER => $headers, CURLOPT_POSTFIELDS => json_encode([ 'ids' => $patientId, 'rpp' => 1, 'embed' => 'patientPreferredProviders,patientReferralSources', ]),]);$current = json_decode(curl_exec($ch), true)['item'][0];curl_close($ch);
// 2. Merge: copy every update field from the record, then apply the change.$payload = [];foreach ($sameName as $field) { if (array_key_exists($field, $current)) { $payload[$field] = $current[$field]; }}$payload['preferredProviderIds'] = array_column($current['patientPreferredProviders'] ?? [], 'providerId');$payload['referralSourceIds'] = array_column($current['patientReferralSources'] ?? [], 'referralSourceId');$payload['alerts'] = 'Allergic to penicillin'; // the actual change
// 3. Write the complete object back.$ch = curl_init("$base/open-api-patients/$patientId/update");curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'PUT', CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_HTTPHEADER => $headers, CURLOPT_POSTFIELDS => json_encode($payload),]);$updated = json_decode(curl_exec($ch), true);curl_close($ch);const BASE = 'https://open-api.shepherd.vet/pav2';const patientId = 'a1b2c3d4-e5f6-7890-abcd-ef0123456789';const headers = { 'Content-Type': 'application/json', 'X-Integration-Public-Key': process.env.SHEPHERD_PUBLIC_KEY, 'X-Integration-Private-Key': process.env.SHEPHERD_PRIVATE_KEY, 'X-Clinic-Id': process.env.SHEPHERD_CLINIC_ID,};
// Update fields with the same name on the record you read.const SAME_NAME = [ 'name', 'breedId', 'sexId', 'statusId', 'dateOfBirth', 'color', 'microchipNumber', 'rabiesTag', 'alerts', 'additionalText', 'patientCprstatusId', 'patientCPRStatusNote',];
// 1. Read, embedding the links you intend to resend.const read = await fetch(`${BASE}/open-api-patients`, { method: 'POST', headers, body: JSON.stringify({ ids: patientId, rpp: 1, embed: 'patientPreferredProviders,patientReferralSources', }), signal: AbortSignal.timeout(30_000),});if (!read.ok) throw new Error(`Shepherd API ${read.status}: ${await read.text()}`);const [current] = (await read.json()).item;
// 2. Merge: copy every update field from the record, then apply the change.const payload = {};for (const field of SAME_NAME) { if (field in current) payload[field] = current[field];}payload.preferredProviderIds = (current.patientPreferredProviders ?? []).map((p) => p.providerId);payload.referralSourceIds = (current.patientReferralSources ?? []).map((r) => r.referralSourceId);payload.alerts = 'Allergic to penicillin'; // the actual change
// 3. Write the complete object back.const write = await fetch(`${BASE}/open-api-patients/${patientId}/update`, { method: 'PUT', headers, body: JSON.stringify(payload), signal: AbortSignal.timeout(30_000),});if (!write.ok) throw new Error(`Shepherd API ${write.status}: ${await write.text()}`);const updated = await write.json();Troubleshooting a rejected write
Section titled “Troubleshooting a rejected write”Both write endpoints reject the whole request rather than partially applying it. When a create or update comes back 400, work down this list:
- Are all the required fields present? Create needs
name,clientId,breedId,sexId,statusId,dateOfBirth. Update needs those minusclientId, pluscolor. A missing required field produces the generic “The request is invalid.” envelope described in Errors, which does not currently name the offending field. - Are the lookup IDs from the right environment? Sandbox and production have entirely separate ID sets. An ID copied from production will not validate against sandbox, and vice versa.
- Is the breed from this clinic’s list and not retired? Query breeds with
getBreedsByClinicId: trueandisLegacy: false. - Is
dateOfBirthin the past? A future date of birth is rejected. - Is the weight valid? If you sent
patientWeight,weightmust be greater than zero and must pass validation for theweightUnitIdyou named. - Still stuck on a create? Include a valid
patientWeightblock; see the note under Create.
Note that unknown fields in the body are ignored rather than rejected, so a stray speciesId or a misspelled key will not itself cause a 400; it simply will not be applied.
File uploads
Section titled “File uploads”Patient files (lab PDFs, imaging stills, intake forms) are uploaded in three steps:
- Request a presigned upload URL.
POST /pav2/open-api-patients/file-presigned-upload-urltakespatientIdandfileName, and returnsurl(the short-lived S3 URL) andpath(the key that identifies the object). - PUT the binary directly to S3. Bypasses the Shepherd application servers entirely. Send the raw file bytes with the appropriate
Content-Type(e.g.application/pdf,image/jpeg). - Register the file entry.
POST /pav2/open-api-patients/file-entry-writerecordspatientId,fileName, and thepathreturned in step 1, so the file shows up in the patient chart.
For images, also send width and height on the step 3 request. They default to zero when omitted, which is rarely what you want for a stored image.
Step 1: request a presigned upload URL
Section titled “Step 1: request a presigned upload URL”curl -X POST https://open-api.shepherd.vet/pav2/open-api-patients/file-presigned-upload-url \ -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 '{ "patientId": "a1b2c3d4-e5f6-7890-abcd-ef0123456789", "fileName": "lab-results.pdf" }'import osimport requests
presign = requests.post( "https://open-api.shepherd.vet/pav2/open-api-patients/file-presigned-upload-url", headers={ "Content-Type": "application/json", "X-Integration-Public-Key": os.environ["SHEPHERD_PUBLIC_KEY"], "X-Integration-Private-Key": os.environ["SHEPHERD_PRIVATE_KEY"], "X-Clinic-Id": os.environ["SHEPHERD_CLINIC_ID"], }, json={ "patientId": "a1b2c3d4-e5f6-7890-abcd-ef0123456789", "fileName": "lab-results.pdf", }, timeout=30,)presign.raise_for_status()upload = presign.json()# upload["url"] and upload["path"] are used in steps 2 and 3.using System.Net.Http;using System.Net.Http.Json;
using var http = new HttpClient();http.DefaultRequestHeaders.Add("X-Integration-Public-Key", Environment.GetEnvironmentVariable("SHEPHERD_PUBLIC_KEY"));http.DefaultRequestHeaders.Add("X-Integration-Private-Key", Environment.GetEnvironmentVariable("SHEPHERD_PRIVATE_KEY"));http.DefaultRequestHeaders.Add("X-Clinic-Id", Environment.GetEnvironmentVariable("SHEPHERD_CLINIC_ID"));
var presign = await http.PostAsJsonAsync( "https://open-api.shepherd.vet/pav2/open-api-patients/file-presigned-upload-url", new { patientId = "a1b2c3d4-e5f6-7890-abcd-ef0123456789", fileName = "lab-results.pdf", });presign.EnsureSuccessStatusCode();var upload = await presign.Content.ReadFromJsonAsync<JsonElement>();// upload.GetProperty("url") and upload.GetProperty("path")<?php$ch = curl_init('https://open-api.shepherd.vet/pav2/open-api-patients/file-presigned-upload-url');curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'X-Integration-Public-Key: ' . getenv('SHEPHERD_PUBLIC_KEY'), 'X-Integration-Private-Key: ' . getenv('SHEPHERD_PRIVATE_KEY'), 'X-Clinic-Id: ' . getenv('SHEPHERD_CLINIC_ID'), ], CURLOPT_POSTFIELDS => json_encode([ 'patientId' => 'a1b2c3d4-e5f6-7890-abcd-ef0123456789', 'fileName' => 'lab-results.pdf', ]),]);$upload = json_decode(curl_exec($ch), true);curl_close($ch);// $upload['url'] and $upload['path']const presign = await fetch( 'https://open-api.shepherd.vet/pav2/open-api-patients/file-presigned-upload-url', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Integration-Public-Key': process.env.SHEPHERD_PUBLIC_KEY, 'X-Integration-Private-Key': process.env.SHEPHERD_PRIVATE_KEY, 'X-Clinic-Id': process.env.SHEPHERD_CLINIC_ID, }, body: JSON.stringify({ patientId: 'a1b2c3d4-e5f6-7890-abcd-ef0123456789', fileName: 'lab-results.pdf', }), },);if (!presign.ok) { throw new Error(`Shepherd API ${presign.status}: ${await presign.text()}`);}const upload = await presign.json();// upload.url, upload.pathStep 2: PUT the binary to S3
Section titled “Step 2: PUT the binary to S3”This call goes to S3, not to the Shepherd API; do not send the integration headers. Use the URL returned in step 1 and match the Content-Type you registered.
curl -X PUT "$UPLOAD_URL" \ -H "Content-Type: application/pdf" \ --data-binary @lab-results.pdfwith open("lab-results.pdf", "rb") as fh: put = requests.put( upload["url"], data=fh, headers={"Content-Type": "application/pdf"}, timeout=120, )put.raise_for_status()Step 3: register the file entry
Section titled “Step 3: register the file entry”curl -X POST https://open-api.shepherd.vet/pav2/open-api-patients/file-entry-write \ -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 '{ "patientId": "a1b2c3d4-e5f6-7890-abcd-ef0123456789", "path": "<path from step 1>", "fileName": "lab-results" }'entry = requests.post( "https://open-api.shepherd.vet/pav2/open-api-patients/file-entry-write", headers={ "Content-Type": "application/json", "X-Integration-Public-Key": os.environ["SHEPHERD_PUBLIC_KEY"], "X-Integration-Private-Key": os.environ["SHEPHERD_PRIVATE_KEY"], "X-Clinic-Id": os.environ["SHEPHERD_CLINIC_ID"], }, json={ "patientId": "a1b2c3d4-e5f6-7890-abcd-ef0123456789", "path": upload["path"], "fileName": "lab-results", # no extension here }, timeout=30,)entry.raise_for_status()var entry = await http.PostAsJsonAsync( "https://open-api.shepherd.vet/pav2/open-api-patients/file-entry-write", new { patientId = "a1b2c3d4-e5f6-7890-abcd-ef0123456789", path = upload.GetProperty("path").GetString(), fileName = "lab-results", // no extension here });entry.EnsureSuccessStatusCode();<?php$ch = curl_init('https://open-api.shepherd.vet/pav2/open-api-patients/file-entry-write');curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'X-Integration-Public-Key: ' . getenv('SHEPHERD_PUBLIC_KEY'), 'X-Integration-Private-Key: ' . getenv('SHEPHERD_PRIVATE_KEY'), 'X-Clinic-Id: ' . getenv('SHEPHERD_CLINIC_ID'), ], CURLOPT_POSTFIELDS => json_encode([ 'patientId' => 'a1b2c3d4-e5f6-7890-abcd-ef0123456789', 'path' => $upload['path'], 'fileName' => 'lab-results', // no extension here ]),]);curl_exec($ch);curl_close($ch);const entry = await fetch( 'https://open-api.shepherd.vet/pav2/open-api-patients/file-entry-write', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Integration-Public-Key': process.env.SHEPHERD_PUBLIC_KEY, 'X-Integration-Private-Key': process.env.SHEPHERD_PRIVATE_KEY, 'X-Clinic-Id': process.env.SHEPHERD_CLINIC_ID, }, body: JSON.stringify({ patientId: 'a1b2c3d4-e5f6-7890-abcd-ef0123456789', path: upload.path, fileName: 'lab-results', // no extension here }), },);if (!entry.ok) { throw new Error(`Shepherd API ${entry.status}: ${await entry.text()}`);}Listing attachments
Section titled “Listing attachments”POST /pav2/open-api-patients/attachments returns the registered files for a patient, paginated with the same conventions as every other collection endpoint. Pass a patientId filter to scope to a single chart.
See also
Section titled “See also”- Conventions: pagination, embed, sort, and date-range rules.
- Clients: the owners patients belong to.
- Appointments: scheduled visits for a patient.
- SOAP records: clinical notes captured during a visit.
- Errors: HTTP status codes and the error body shapes.