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.
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", "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", "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", 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', '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', 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.
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
Section titled “Example”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 '{ "name": "Biscuit", "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", "microchipNumber": "985141001234567", "rabiesTag": "R123456", "alerts": "Allergic to penicillin" }'import 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"
# 1. Read the current record.read = requests.post( f"{BASE}/open-api-patients", headers=HEADERS, json={"ids": patient_id}, timeout=30,)read.raise_for_status()current = read.json()["item"][0]
# 2. Merge, carrying every editable field through. Omission means deletion.EDITABLE = ( "name", "breedId", "sexId", "statusId", "dateOfBirth", "color", "microchipNumber", "rabiesTag", "alerts", "additionalText", "patientCprstatusId",)payload = {k: current.get(k) for k in EDITABLE}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;using System.Net.Http.Json;
const string Base = "https://open-api.shepherd.vet/pav2";var patientId = "a1b2c3d4-e5f6-7890-abcd-ef0123456789";
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.PutAsJsonAsync( $"{Base}/open-api-patients/{patientId}/update", new { name = "Biscuit", 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", microchipNumber = "985141001234567", rabiesTag = "R123456", alerts = "Allergic to penicillin", });response.EnsureSuccessStatusCode();<?php$patientId = 'a1b2c3d4-e5f6-7890-abcd-ef0123456789';$ch = curl_init("https://open-api.shepherd.vet/pav2/open-api-patients/$patientId/update");curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'PUT', 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', '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', 'microchipNumber' => '985141001234567', 'rabiesTag' => 'R123456', 'alerts' => 'Allergic to penicillin', ]),]);$updated = json_decode(curl_exec($ch), true);curl_close($ch);const patientId = 'a1b2c3d4-e5f6-7890-abcd-ef0123456789';const response = await fetch( `https://open-api.shepherd.vet/pav2/open-api-patients/${patientId}/update`, { method: 'PUT', 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', 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', microchipNumber: '985141001234567', rabiesTag: 'R123456', alerts: 'Allergic to penicillin', }), },);if (!response.ok) { throw new Error(`Shepherd API ${response.status}: ${await response.text()}`);}const updated = await response.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 two JSON error shapes.