Appointments
An appointment is a scheduled visit: a slot on the clinic calendar that ties a patient to a provider for a particular reason (wellness exam, dental, recheck, surgery, etc.). Appointments are the typical entry point for day-of workflows, and they’re the parent record that most SOAP notes are written against.
Appointment listings are heavily date-filtered in practice. Scope every list with a date window of one calendar month or less, or with an id filter; see Conventions for paging and date-window rules.
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 appointment. |
clientId | string | The owning client (paying customer). |
appointmentPatients | array | The patients on the appointment. An appointment can cover more than one pet for the same client, so there is no single patientId. |
providerId | string | The veterinarian or staff member assigned. |
startDate | string | ISO 8601 UTC scheduled start. |
endDate | string | ISO 8601 UTC scheduled end. |
appointmentTypeId | string | Reference to the appointment-type taxonomy. A null type means the slot is blocked off rather than booked. |
appointmentStatusId | string | Reference to the current status. Defaults to upcoming. |
appointmentCancellationReasonId | string | Set when the appointment was cancelled. |
visitReason | string | Free-text reason for visit. Becomes the initial complaint on the SOAP. |
schedulingMethod | string | How the appointment was booked. |
clinicId | string | The owning clinic. |
dateCreated | string | ISO 8601 UTC timestamp of creation. |
dateUpdated | string | ISO 8601 UTC timestamp of last update. |
isDeleted | boolean | Soft-delete flag. Note this is isDeleted here, not deleted. |
List / search
Section titled “List / search”/pav2/open-api-appointments Returns a paginated collection of appointments for the clinic. The most common pattern is a date-range query for “today” or “this week”; the example below shows the exact ISO 8601 timestamps to send.
Appointments have two kinds of date filter, and they answer different questions:
dateFrom/dateTomatch appointments whose scheduled time overlaps the range. Use these for “today’s schedule” or “next week’s bookings”.dateCreatedFrom/dateCreatedTo(anddateUpdatedFrom/dateUpdatedTo) match when the appointment record was created or last changed, not when the visit happens. UsedateUpdatedFrom/dateUpdatedTofor incremental sync.
Send both ends of whichever pair you use and keep the window to one calendar month or less. Keep page * rpp at or below 10,000; if a clinic’s volume risks exceeding that, use shorter windows. Deleted appointments are included unless you send "isDeleted": false. Embedding appointmentPatients.patient is expensive: keep rpp at 50 or less when you use it.
Example: today’s appointments
Section titled “Example: today’s appointments”For a clinic local to UTC, “today” (2026-05-22) is 2026-05-22T00:00:00Z through 2026-05-23T00:00:00Z. Adjust the bounds to your clinic’s local timezone as needed.
curl -X POST https://open-api.shepherd.vet/pav2/open-api-appointments \ -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": 100, "sort": "startDate|asc", "dateFrom": "2026-05-22T00:00:00Z", "dateTo": "2026-05-23T00:00:00Z", "embed": "appointmentPatients,client,provider" }'import osfrom datetime import datetime, timedelta, timezoneimport requests
start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)end = start + timedelta(days=1)
resp = requests.post( "https://open-api.shepherd.vet/pav2/open-api-appointments", 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": 100, "sort": "startDate|asc", "dateFrom": start.strftime("%Y-%m-%dT%H:%M:%SZ"), "dateTo": end.strftime("%Y-%m-%dT%H:%M:%SZ"), "embed": "appointmentPatients,client,provider", }, timeout=30,)resp.raise_for_status()for appt in resp.json()["item"]: print(appt["startDate"], appt["clientId"], appt.get("visitReason"))using System.Net.Http;using System.Net.Http.Json;
var start = DateTime.UtcNow.Date;var end = start.AddDays(1);
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-appointments", new { page = 1, rpp = 100, sort = "startDate|asc", dateFrom = start.ToString("yyyy-MM-ddTHH:mm:ssZ"), dateTo = end.ToString("yyyy-MM-ddTHH:mm:ssZ"), embed = "appointmentPatients,client,provider", });response.EnsureSuccessStatusCode();<?php$start = (new DateTimeImmutable('today', new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s\Z');$end = (new DateTimeImmutable('tomorrow', new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s\Z');
$ch = curl_init('https://open-api.shepherd.vet/pav2/open-api-appointments');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' => 100, 'sort' => 'startDate|asc', 'dateFrom' => $start, 'dateTo' => $end, 'embed' => 'appointmentPatients,client,provider', ]),]);$payload = json_decode(curl_exec($ch), true);curl_close($ch);const start = new Date();start.setUTCHours(0, 0, 0, 0);const end = new Date(start);end.setUTCDate(end.getUTCDate() + 1);
const response = await fetch('https://open-api.shepherd.vet/pav2/open-api-appointments', { 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: 100, sort: 'startDate|asc', dateFrom: start.toISOString().replace(/\.\d{3}Z$/, 'Z'), dateTo: end.toISOString().replace(/\.\d{3}Z$/, 'Z'), embed: 'appointmentPatients,client,provider', }),});if (!response.ok) { throw new Error(`Shepherd API ${response.status}: ${await response.text()}`);}const payload = await response.json();Response
Section titled “Response”{ "item": [ { "id": "b96d21ae-435f-4ef1-8cf7-3efbc3a3649a", "clientId": "e45a6e12-3a1c-4e7d-9fd2-3473a4d8a7a6", "appointmentPatients": [ { "patientId": "a1b2c3d4-e5f6-7890-abcd-ef0123456789" } ], "providerId": "5a8d0cfc-c7c4-44a0-bdab-44f4e5f6b219", "startDate": "2026-05-22T14:30:00Z", "endDate": "2026-05-22T15:00:00Z", "appointmentTypeId": "8fc9c6d1-a829-4b93-a5ea-1c83db96a1b4", "appointmentStatusId": "9f8e7d6c-5b4a-3210-a9f8-fedcba098765", "appointmentCancellationReasonId": null, "visitReason": "Annual wellness exam", "clinicId": "9890662f-52db-4a41-a5c1-b2e300d400b4", "dateCreated": "2026-05-22T09:11:00Z", "dateUpdated": "2026-05-22T09:11:00Z", "isDeleted": false } ], "totalRecords": 27, "page": 1, "recordsPerPage": 100, "sort": "startDate|asc", "searchQuery": null, "embed": "appointmentPatients,client,provider", "links": []}Create
Section titled “Create”/pav2/open-api-appointments/write Creates a new appointment on the clinic calendar.
Required: startDate, endDate, and providerId. Everything else is optional, but the optional fields interact:
appointmentTypeIdcontrols the rest. Leave it null and the call blocks off time for the provider rather than booking a visit;clientIdandpatientIdscan only be set when a type is present.patientIdsis an array and requiresclientId. An appointment belongs to a client and can cover several of that client’s pets, so there is no singlepatientId.startDateandendDatemust fall on 5-minute boundaries (for example14:30:00, not14:32:00), the end must be after the start, and the appointment can span at most 24 hours.appointmentStatusIddefaults to upcoming when omitted. The pending status is rejected on create.visitReasonbecomes the initial complaint on the resulting SOAP record.sendEmailNotification,sendSmsNotification, andsendFormEmailNotificationall default to false. Leave them false unless the clinic wants your integration to message its clients.
The response is the created appointment, or a collection envelope if you sent embed. A create is not idempotent: every successful call books a new appointment, so never retry it automatically after a timeout or a 5xx. Search for the appointment first.
Example
Section titled “Example”curl -X POST https://open-api.shepherd.vet/pav2/open-api-appointments/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 '{ "startDate": "2026-05-22T14:30:00Z", "endDate": "2026-05-22T15:00:00Z", "providerId": "5a8d0cfc-c7c4-44a0-bdab-44f4e5f6b219", "appointmentTypeId": "8fc9c6d1-a829-4b93-a5ea-1c83db96a1b4", "clientId": "e45a6e12-3a1c-4e7d-9fd2-3473a4d8a7a6", "patientIds": ["a1b2c3d4-e5f6-7890-abcd-ef0123456789"], "visitReason": "Annual wellness exam" }'import osimport requests
resp = requests.post( "https://open-api.shepherd.vet/pav2/open-api-appointments/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={ "startDate": "2026-05-22T14:30:00Z", "endDate": "2026-05-22T15:00:00Z", "providerId": "5a8d0cfc-c7c4-44a0-bdab-44f4e5f6b219", "appointmentTypeId": "8fc9c6d1-a829-4b93-a5ea-1c83db96a1b4", "clientId": "e45a6e12-3a1c-4e7d-9fd2-3473a4d8a7a6", "patientIds": ["a1b2c3d4-e5f6-7890-abcd-ef0123456789"], "visitReason": "Annual wellness exam", }, timeout=30,)resp.raise_for_status()appointment = 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-appointments/write", new { startDate = "2026-05-22T14:30:00Z", endDate = "2026-05-22T15:00:00Z", providerId = "5a8d0cfc-c7c4-44a0-bdab-44f4e5f6b219", appointmentTypeId = "8fc9c6d1-a829-4b93-a5ea-1c83db96a1b4", clientId = "e45a6e12-3a1c-4e7d-9fd2-3473a4d8a7a6", patientIds = new[] { "a1b2c3d4-e5f6-7890-abcd-ef0123456789" }, visitReason = "Annual wellness exam", });response.EnsureSuccessStatusCode();<?php$ch = curl_init('https://open-api.shepherd.vet/pav2/open-api-appointments/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([ 'startDate' => '2026-05-22T14:30:00Z', 'endDate' => '2026-05-22T15:00:00Z', 'providerId' => '5a8d0cfc-c7c4-44a0-bdab-44f4e5f6b219', 'appointmentTypeId' => '8fc9c6d1-a829-4b93-a5ea-1c83db96a1b4', 'clientId' => 'e45a6e12-3a1c-4e7d-9fd2-3473a4d8a7a6', 'patientIds' => ['a1b2c3d4-e5f6-7890-abcd-ef0123456789'], 'visitReason' => 'Annual wellness exam', ]),]);$appointment = json_decode(curl_exec($ch), true);curl_close($ch);const response = await fetch('https://open-api.shepherd.vet/pav2/open-api-appointments/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({ startDate: '2026-05-22T14:30:00Z', endDate: '2026-05-22T15:00:00Z', providerId: '5a8d0cfc-c7c4-44a0-bdab-44f4e5f6b219', appointmentTypeId: '8fc9c6d1-a829-4b93-a5ea-1c83db96a1b4', clientId: 'e45a6e12-3a1c-4e7d-9fd2-3473a4d8a7a6', patientIds: ['a1b2c3d4-e5f6-7890-abcd-ef0123456789'], visitReason: 'Annual wellness exam', }),});if (!response.ok) { throw new Error(`Shepherd API ${response.status}: ${await response.text()}`);}const appointment = await response.json();Update
Section titled “Update”/pav2/open-api-appointments/{id}/update Updates an existing appointment. This is a PUT, so it is a full replacement: send the complete appointment 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 that keeps it safe.
Required on update: startDate, endDate, and providerId.
The remaining editable fields are clientId, patientIds, appointmentTypeId, appointmentStatusId, visitReason, appointmentCancellationReasonId, cancellationNote, sendEmailNotification, and sendSmsNotification. The same time rules as create apply: 5-minute boundaries, end after start, 24 hours at most.
What omission does on this endpoint:
patientIdsomitted removes every patient from the appointment. The record you read has nopatientIdsfield; build it fromappointmentPatients[].patientIdand resend it even when the patients have not changed.appointmentStatusIdomitted resets the status to upcoming. An appointment that was checked in or completed goes back to upcoming. Resend the current status.appointmentTypeId,clientId,visitReason,cancellationNote, andappointmentCancellationReasonIdomitted become null.
To change one thing, read the appointment with {"ids": "<id>", "rpp": 1}, copy every editable field from it (mapping patientIds as above), apply your change, and send the whole body.
Returns 204 No Content with an empty body on success. Treat any 2xx as success and don’t try to parse a body. A 404 means the id does not resolve within the clinic named by X-Clinic-Id.
Sub-resources
Section titled “Sub-resources”Appointment-adjacent data lives on its own endpoints, each following the same list / write conventions as the parent resource.
| Endpoint | Purpose |
|---|---|
/pav2/open-api-appointment-notes/write | Free-form notes attached to an appointment. Write only; there is no list endpoint for appointment notes. |
/pav2/open-api-appointment-statuses | Status taxonomy (scheduled, checked-in, in-room, completed, no-show). |
/pav2/open-api-appointment-types | Type taxonomy (wellness, dental, surgery, recheck, etc.). |
/pav2/open-api-appointment-cancellation-reasons | Cancellation reason taxonomy used when an appointment is cancelled. |
See also
Section titled “See also”- Conventions: pagination, embed, sort, and date-window rules.
- Patients: the pet the appointment is scheduled for.
- SOAP records: clinical notes written against an appointment.
- Errors: HTTP status codes and the error body shapes.