OpenVPM API Reference
Complete API documentation for the OpenVPM veterinary practice management system. The dashboard API uses tRPC, client portal flows use portal tokens, and external integrations use API keys with REST endpoints under /api/v1.
Authentication
Dashboard calls use NextAuth session cookies, portal flows use client tokens, and REST integrations use API keys.
Multi-tenancy
All data is scoped to the authenticated user's practice. No cross-practice data access is possible.
Real-time Events
Subscribe to the live webhook catalog. Events are HMAC signed with each subscription secret.
Authentication
Register practices and retrieve the current user session. Dashboard procedures use session cookies; portal and REST endpoints use their own token/key flows.
auth.registerNone (public)Register a new practice with an admin user account.
Input
{
practiceName: string,
country: "US" | "CA" | "GB" | "IE" | "AU",
name?: string,
email: string,
password: string // min 8 characters
}Response
{ success: true }auth.meGet the current authenticated user and practice details.
Response
{
id: string,
email: string,
name: string,
role: "admin" | "veterinarian" | "technician" | "front_desk",
practiceId: string,
practiceName: string
}Clients
Manage pet owners / client records.
clients.listList clients with optional search and pagination.
Input
{
search?: string,
limit?: number, // 1-100, default 25
offset?: number // default 0
}Response
{
items: Client[],
total: number
}clients.searchQuick search clients by name, email, or phone. Returns up to 10 results.
Input
{ query: string }Response
Client[]
clients.getByIdGet a single client with their patients.
Input
{ id: string }Response
{
...Client,
patients: Patient[]
}clients.createCreate a new client record and issue a private portal access token.
Input
{
firstName: string,
lastName: string,
email?: string,
phone?: string,
address?: string,
city?: string,
state?: string,
zip?: string
}Response
Client
clients.rotatePortalAccessTokenCreate or rotate a client's private portal link. Existing portal URLs stop working immediately after rotation.
Input
{ id: string }Response
{
id: string,
accessToken: string
}clients.updateUpdate an existing client.
Input
{
id: string,
firstName?: string,
lastName?: string,
email?: string,
phone?: string,
address?: string,
city?: string,
state?: string,
zip?: string
}Response
Client
clients.deleteSoft-delete a client record.
Input
{ id: string }Response
{ success: true }Patients
Manage animal patient records, weights, and allergies.
patients.listList patients with optional filters.
Input
{
search?: string,
species?: string,
status?: string,
limit?: number, // 1-100, default 25
offset?: number // default 0
}Response
{
items: Patient[],
total: number
}patients.searchQuick search by patient, owner, or breed. Returns up to 10 deterministically ordered results.
Input
{ query: string }Response
Patient[]
patients.getByIdGet full patient details including weights, allergies, and owner info.
Input
{ id: string }Response
{
...Patient,
weights: Weight[],
allergies: Allergy[],
ownerName: string
}patients.createCreate a new patient record.
Input
{
clientId: string,
name: string,
species: string,
breed?: string,
color?: string,
sex: "male" | "female" | "male_neutered" | "female_spayed" | "unknown",
dateOfBirth?: string,
microchipId?: string
}Response
Patient
patients.updateUpdate an existing patient record.
Input
{
id: string,
name?: string,
species?: string,
breed?: string,
color?: string,
sex?: string,
dateOfBirth?: string,
microchipId?: string,
status?: string
}Response
Patient
patients.deleteSoft-delete a patient record.
Input
{ id: string }Response
{ success: true }patients.addWeightRecord a weight measurement.
Input
{
patientId: string,
weight: number,
unit: string
}Response
Weight
patients.addAllergyRecord a known allergy.
Input
{
patientId: string,
allergen: string,
severity?: string,
notes?: string
}Response
Allergy
Appointments
Schedule and manage appointments.
appointments.listList appointments within a date range.
Input
{
startDate: string, // ISO date
endDate: string, // ISO date
doctorId?: string,
locationId?: string
}Response
Appointment[]
appointments.getByIdGet full appointment details.
Input
{ id: string }Response
Appointment
appointments.createSchedule a new appointment.
Input
{
patientId: string,
clientId: string,
typeId: string,
doctorId: string,
locationId?: string, // required when the clinic has multiple locations unless room/provider identifies one
roomId?: string,
startTime: string, // ISO datetime
endTime: string, // ISO datetime
notes?: string,
reason?: string
}Response
Appointment
appointments.updateStatusUpdate appointment status (e.g., confirm, check in, exam, check out, cancel).
Input
{
id: string,
status: "scheduled" | "confirmed" | "checked_in" | "in_exam" | "checked_out" | "no_show" | "cancelled"
}Response
Appointment
appointments.listTypesList available appointment types for the practice.
Response
AppointmentType[]
appointments.listDoctorsList veterinarians available for scheduling.
Response
Doctor[]
appointments.listLocationsList active clinic locations available for scheduling.
Response
Array<{ id: string, name: string, address: string | null, phone: string | null, isPrimary: boolean }>appointments.listRoomsList exam rooms, optionally for one clinic location.
Input
{ locationId?: string }Response
Room[]
Medical Records
SOAP notes, vaccinations, lab results, procedures, problems, and prescriptions.
records.listSoapNotesList SOAP notes for a patient.
Input
{ patientId: string }Response
SoapNote[]
records.createSoapNoteCreate an immediately finalized, immutable SOAP note for an active in-exam appointment. Conflicts with an existing draft or effective finalized note.
Input
{
patientId: string,
appointmentId: string,
subjective: string,
objective: string,
assessment: string,
plan: string
}Response
SoapNote
records.listVaccinationsList vaccination records for a patient.
Input
{ patientId: string }Response
Vaccination[]
records.createVaccinationRecord a vaccination.
Input
{
patientId: string,
vaccineName: string,
manufacturer?: string,
lotNumber?: string,
expirationDate?: string,
nextDueDate?: string,
notes?: string
}Response
Vaccination
records.listLabResultsList lab results for a patient.
Input
{ patientId: string }Response
LabResult[]
records.createLabResultCreate a lab result entry.
Input
{
patientId: string,
testName: string,
category?: string,
results?: object,
notes?: string
}Response
LabResult
records.updateLabResultStatusUpdate the status of a lab result.
Input
{
id: string,
status: "pending" | "completed" | "reviewed"
}Response
LabResult
records.listProceduresList procedures performed on a patient.
Input
{ patientId: string }Response
Procedure[]
records.createProcedureRecord a procedure.
Input
{
patientId: string,
name: string,
description?: string,
notes?: string
}Response
Procedure
records.listProblemsList active and resolved problems for a patient.
Input
{ patientId: string }Response
Problem[]
records.createProblemAdd a problem to the patient's problem list.
Input
{
patientId: string,
description: string,
severity?: string,
notes?: string
}Response
Problem
records.updateProblemStatusMark a problem as resolved or reactivate it.
Input
{
id: string,
status: "active" | "resolved"
}Response
Problem
records.listPrescriptionsList prescriptions for a patient.
Input
{ patientId: string }Response
Prescription[]
records.createPrescriptionCreate a prescription.
Input
{
patientId: string,
medicationName: string,
dosage: string,
frequency: string,
startDate: string,
endDate?: string,
quantity?: number,
productId?: string,
refillsRemaining?: number,
instructions?: string,
acknowledgeSafetyWarnings?: boolean
}Response
Prescription
Billing
Invoices, payments, services, and estimates.
billing.listInvoicesList invoices with optional filters.
Input
{
status?: string,
isEstimate?: boolean,
limit?: number,
offset?: number
}Response
{
items: Invoice[],
total: number
}billing.getInvoiceGet full invoice with line items and payments.
Input
{ id: string }Response
{
...Invoice,
items: InvoiceItem[],
payments: Payment[]
}billing.createInvoiceCreate an invoice or estimate.
Input
{
clientId: string,
patientId?: string,
isEstimate?: boolean,
items: {
serviceId?: string,
productId?: string,
description: string,
quantity: number,
unitPrice: number
}[],
notes?: string
}Response
Invoice
billing.updateInvoiceStatusUpdate a workflow status. Paid is derived from recorded payments or adjustments and cannot be set directly.
Input
{
id: string,
status: "draft" | "sent" | "overdue" | "void"
}Response
Invoice
billing.convertEstimateToInvoiceConvert an approved estimate into a billable invoice.
Input
{ id: string }Response
Invoice
billing.recordPaymentRecord a payment against an invoice.
Input
{
invoiceId: string,
amount: number,
method: "cash" | "credit_card" | "debit_card" | "check" | "online" | "other",
notes?: string
}Response
Payment
billing.createCardPaymentCheckoutCreate a Stripe Checkout link for the remaining adjusted invoice balance.
Input
{ invoiceId: string }Response
{ url: string }billing.listPaymentsList payments for an invoice.
Input
{ invoiceId: string }Response
Payment[]
billing.listAdjustmentsList credits and write-offs for an invoice.
Input
{ invoiceId: string }Response
InvoiceAdjustment[]
billing.applyInvoiceAdjustmentApply a credit or write-off to an invoice balance.
Input
{
invoiceId: string,
type: "credit" | "write_off",
amount: number,
reason?: string
}Response
InvoiceAdjustment
billing.voidInvoiceVoid an invoice with no payment or adjustment history.
Input
{ id: string }Response
Invoice
billing.listServicesList all services offered by the practice.
Response
Service[]
billing.listArchivedServicesList archived services for administrator recovery.
Response
Service[]
billing.createServiceCreate a service in the practice charge catalog.
Input
{ name: string, code?: string, category?: string, defaultPrice: string }Response
Service
billing.updateServiceUpdate a service if its browser version is still current.
Input
{ id: string, expected: { name: string, code?: string, category?: string, defaultPrice: string }, name: string, code?: string, category?: string, defaultPrice: string }Response
Service
billing.archiveServiceRemove a service from future charge pickers without changing historical invoices.
Input
{ id: string, expected: { name: string, code?: string, category?: string, defaultPrice: string } }Response
{ success: true }billing.restoreServiceRestore an archived service to future charge pickers.
Input
{ id: string, expected: { name: string, code?: string, category?: string, defaultPrice: string } }Response
{ success: true }billing.listProductsList products available for invoicing.
Response
Product[]
Client Portal
Token-based public access for pet owners. No session required -- uses a unique access token per client.
portal.getClientPortal tokenGet client profile and pets via portal token.
Input
{ token: string }Response
{
client: Client,
pets: Patient[],
locations: Array<{ id: string, name: string, address: string | null, phone: string | null, isPrimary: boolean }>
}portal.getPetDetailPortal tokenGet full pet details including medical history.
Input
{
token: string,
patientId: string
}Response
{
...Patient,
vaccinations: Vaccination[],
prescriptions: Prescription[],
weights: Weight[],
allergies: Allergy[]
}portal.getAppointmentsPortal tokenList upcoming appointments for the client.
Input
{ token: string }Response
Appointment[]
portal.getInvoicesPortal tokenList invoices for the client.
Input
{ token: string }Response
Invoice[]
portal.getMessagesPortal tokenList portal messages for the client.
Input
{ token: string }Response
{
timezone: string | null,
items: Array<{
id: string,
direction: "inbound" | "outbound",
subject: string | null,
content: string | null,
status: string,
readAt: Date | null,
createdAt: Date | null
}>
}portal.createMessagePortal tokenSend a portal message from the client into the shared inbox.
Input
{
token: string,
content: string
}Response
{ success: true, message: Communication }portal.markMessagesReadPortal tokenMark outbound clinic portal messages as read after the client opens the thread.
Input
{ token: string }Response
{ success: true, updated: number }portal.getAppointmentTypesPortal tokenList appointment types available for portal booking.
Input
{ token: string }Response
Array<{ id: string, name: string, durationMinutes: number, requiresDoctor: number }>portal.availableSlotsPortal tokenList suggested open times for a portal booking date.
Input
{
token: string,
date: string, // YYYY-MM-DD
typeId?: string, // uses the verified type duration and provider coverage
locationId?: string, // required when the clinic has multiple locations
durationMinutes?: number // legacy fallback when typeId is omitted
}Response
Array<{ time: string, iso: string }>portal.requestAppointmentPortal tokenSubmit an appointment request from the portal using an exact requested time.
Input
{
token: string,
patientId: string,
typeId: string,
locationId?: string, // required when the clinic has multiple locations
reason: string,
preferredDate: string, // YYYY-MM-DD
preferredTime: string // 24-hour HH:MM
}Response
{ success: true, appointmentId: string, message: string }API Keys
Admin-only API key management for server-to-server integrations. Raw keys are returned once at creation.
apiKeys.listAdmin onlyList active API keys for the practice.
Response
Array<{
id: string,
name: string,
keyPrefix: string,
scopes: ApiScope[],
lastUsedAt: string | null,
createdAt: string
}>apiKeys.createAdmin onlyCreate an API key for REST integrations. The raw key is returned once and is never stored in plaintext. The agent:write scope must be paired with agent:run or *.
Input
{
name: string,
scopes: Array<"clients:read" | "patients:read" | "appointments:read" | "appointments:write" | "records:write" | "agent:run" | "agent:write" | "*">
}Response
{ ...ApiKey, key: string }apiKeys.revokeAdmin onlyRevoke an API key.
Input
{ id: string }Response
{ success: true }REST API
API-key authenticated /api/v1 endpoints for external integrations. Send Authorization: Bearer <api-key>.
GET /api/v1/clientsAPI key: clients:readList clients for the authenticated practice.
Input
?limit=25&offset=0
Response
{ data: Client[], pagination: Pagination }GET /api/v1/clients/:idAPI key: clients:readFetch a single client.
Response
{ data: Client }GET /api/v1/patientsAPI key: patients:readList patients, optionally filtered by client.
Input
?client_id=uuid&limit=25&offset=0
Response
{ data: Patient[], pagination: Pagination }GET /api/v1/patients/:idAPI key: patients:readFetch a single patient.
Response
{ data: Patient }GET /api/v1/appointmentsAPI key: appointments:readList appointments, optionally filtered by client, patient, clinic location, status, or start-time window. Date-only filters use UTC day bounds.
Input
?client_id=uuid&patient_id=uuid&location_id=uuid&status=scheduled&from=YYYY-MM-DD-or-ISO-timestamp&to=YYYY-MM-DD-or-ISO-timestamp&limit=25&offset=0
Response
{ data: Appointment[], pagination: Pagination }GET /api/v1/appointments/:idAPI key: appointments:readFetch a single appointment.
Response
{ data: Appointment }POST /api/v1/appointmentsAPI key: appointments:writeCreate an appointment and emit the appointment.created webhook with camelCase appointment fields.
Input
{
client_id?: string,
patient_id?: string,
doctor_id?: string,
type_id?: string,
location_id?: string, // required when multiple locations and no room/provider resolves one
room_id?: string,
start_time: string, // timezone-qualified ISO timestamp
end_time: string, // timezone-qualified ISO timestamp
notes?: string
}Response
{ data: Appointment }POST /api/v1/soap-notesAPI key: records:writeCreate an immediately finalized, immutable SOAP note for an external AI scribe during an active in-exam appointment and emit the soap_note.created webhook. Returns a conflict when the encounter already has a draft or effective finalized note.
Input
{
patient_id: string,
appointment_id: string,
author_id?: string,
subjective?: string,
objective?: string,
assessment?: string,
plan?: string,
source: string
}Response
{ data: SoapNote }POST /api/v1/agentAPI key: agent:run; agent:write plus resource write scopes when allow_writes=trueRun the OpenVPM Agent from an external automation. Instruction text is trimmed and must be nonblank. Cloud trials require signed Stripe billing-setup evidence before AI is enabled; the rest of the free trial remains available. Write-enabled runs require agent:write plus each write tool's resource scope.
Input
{
instruction: string,
allow_writes?: boolean
}Response
{ data: AgentRunResult }Webhooks
Subscribe to real-time events. Webhook payloads are signed with HMAC-SHA256 using the secret provided at creation.
webhooks.listAdmin onlyList all webhooks for the practice.
Response
Webhook[]
webhooks.createAdmin onlyCreate a webhook subscription. The secret is returned once and cannot be retrieved again.
Input
{
url: string,
events: WebhookEvent[]
}Response
{
...Webhook,
secret: string // shown once
}webhooks.toggleAdmin onlyEnable or disable a webhook.
Input
{ id: string }Response
Webhook
webhooks.deleteAdmin onlyDelete a webhook subscription.
Input
{ id: string }Response
{ success: true }Inventory
Track products, stock levels, and suppliers.
inventory.listList inventory items with optional filters.
Input
{
search?: string,
category?: string,
alert?: "all" | "attention" | "low_stock" | "expired" | "expiring_soon",
limit?: number,
offset?: number
}Response
{
items: Array<InventoryItem & {
stockStatus: "not_tracked" | "ok" | "low" | "out",
expirationStatus: "ok" | "expired" | "expiring_soon"
}>,
total: number,
alertCounts: {
attention: number,
lowStock: number,
expired: number,
expiringSoon: number
}
}inventory.createAdd a new inventory item.
Input
{
name: string,
sku?: string,
category?: string,
unitPrice: string,
costPrice?: string,
stockQuantity?: number,
reorderPoint?: number,
lotNumber?: string,
expirationDate?: "YYYY-MM-DD"
}Response
InventoryItem
inventory.updateUpdate inventory item metadata. Use inventory.adjustStock for stock quantity changes so every movement has a reason.
Input
{
id: string,
name?: string,
sku?: string,
category?: string,
unitPrice?: string,
costPrice?: string,
reorderPoint?: number,
lotNumber?: string,
expirationDate?: "YYYY-MM-DD" | null
}Response
InventoryItem
inventory.adjustStockAdjust stock quantity (positive or negative).
Input
{
id: string,
adjustment: number,
reason: string
}Response
InventoryItem
inventory.listSuppliersList all suppliers.
Response
Supplier[]
inventory.createSupplierAdd a new supplier.
Input
{
name: string,
contactEmail?: string,
phone?: string,
address?: string,
notes?: string
}Response
Supplier
inventory.updateSupplierUpdate supplier contact details.
Input
{
id: string,
name?: string,
contactEmail?: string | null,
phone?: string | null,
address?: string | null,
notes?: string | null
}Response
Supplier
Reports
Run practice analytics over configurable date ranges.
reports.revenueRevenue totals, previous-period comparison, and daily revenue for a selected range.
Input
{
startDate?: "YYYY-MM-DD",
endDate?: "YYYY-MM-DD"
}Response
{
range: ReportDateRange,
total: number,
previousTotal: number,
daily: Array<{ date: string, amount: number }>
}reports.appointmentsAppointment KPIs and doctor breakdown for a selected range.
Input
{
startDate?: "YYYY-MM-DD",
endDate?: "YYYY-MM-DD"
}Response
{
range: ReportDateRange,
total: number,
completed: number,
noShows: number,
cancelled: number,
fillRate: number,
byDoctor: Array<{ doctorName: string, total: number, completed: number }>
}reports.topServicesTop billed service items by count and revenue for a selected range.
Input
{
startDate?: "YYYY-MM-DD",
endDate?: "YYYY-MM-DD"
}Response
{
range: ReportDateRange,
items: Array<{ name: string, count: number, revenue: number }>
}reports.inventoryAlertsCurrent low-stock, expired, and expiring-product alerts.
Response
{
lowStock: Product[],
expired: Product[],
expiringSoon: Product[]
}Settings
Admin-only practice configuration endpoints.
settings.listLocationsAdmin onlyList active practice locations.
Response
Array<{
id: string,
name: string,
address: string | null,
phone: string | null,
isPrimary: boolean
}>settings.createLocationAdmin onlyCreate a practice location and sync hosted billing quantities.
Input
{
name: string,
address?: string,
phone?: string,
isPrimary?: boolean
}Response
Location
settings.updateLocationAdmin onlyUpdate a tenant-scoped location.
Input
{
id: string,
name?: string,
address?: string,
phone?: string
}Response
Location
settings.setPrimaryLocationAdmin onlyMake one active tenant location the primary location.
Input
{ id: string }Response
Location
settings.deleteLocationAdmin onlyRetire a location, disable its texting setup, preserve at least one active location, and sync hosted billing quantities.
Input
{ id: string }Response
{ success: true }Communications
Track client communications across channels.
communications.listList communications with optional filters. The sent inbox filter includes sent, delivered, and read outbound messages.
Input
{
clientId?: string,
status?: string,
inboxFilter?: "all" | "unread" | "sent",
limit?: number,
offset?: number
}Response
{
items: Array<Communication & {
readAt: Date | null,
providerMessageId: string | null,
assignedToName: string | null
}>,
total: number
}communications.listConversationsList one latest message per shared-inbox conversation, with unread counts derived server-side. The sent inbox filter includes sent, delivered, and read outbound conversations.
Input
{
inboxFilter?: "all" | "unread" | "sent",
limit?: number,
offset?: number
}Response
{
items: Array<Communication & {
readAt: Date | null,
providerMessageId: string | null,
assignedToName: string | null,
unreadCount: number
}>,
total: number
}communications.getByClientGet all communications for a specific client.
Input
{ clientId: string }Response
Array<Communication & {
readAt: Date | null,
providerMessageId: string | null,
assignedToName: string | null
}>communications.markClientReadMark unread inbound messages for a client thread as read.
Input
{ clientId: string }Response
{ ok: true, updated: number }communications.assignClientAssign or unassign a client conversation in the shared inbox.
Input
{
clientId: string,
action: "assign_to_me" | "unassign",
expectedAssignedTo: string | null
}Response
{
ok: true,
assignedTo: string | null,
assignedToName: string | null,
updated: number
}communications.linkCommunicationToClientLink an unmatched inbound inbox message to a tenant client.
Input
{
communicationId: string,
clientId: string
}Response
{
ok: true,
communicationId: string,
clientId: string,
assignedTo: string | null,
assignedToName: string | null
}communications.createSend outbound SMS/email from the inbox, or send/log internal portal communications visible in the client portal.
Input
{
clientId: string,
channel: "phone" | "sms" | "email" | "portal",
direction: "inbound" | "outbound",
subject?: string,
content: string,
status?: "pending" | "sent" | "delivered" | "read" | "failed",
requestId?: string // Required UUID for outbound SMS/email; reuse for retries of the same send.
}Response
Communication
communications.updateStatusMark one unread inbound communication as read. Delivery lifecycle statuses are managed by send and provider webhook handlers.
Input
{
id: string,
status: "read"
}Response
Communication
Webhook Events
Available event types for webhook subscriptions. Payloads are signed with HMAC-SHA256 using the webhook secret.
| Event | Description |
|---|---|
appointment.created | New appointment created |
appointment.checked_in | Appointment checked in |
appointment.rescheduled | Appointment rescheduled |
appointment.cancelled | Appointment cancelled |
client.created | New client record created |
patient.created | New patient record created |
soap_note.created | SOAP note created |
vaccination.recorded | Vaccination recorded |
problem.created | Problem list item created |
prescription.created | Prescription created |
prescription.refill_dispensed | Prescription refill dispensed |
prescription.refill_authorized | External prescription refill authorized |
prescription.completed | Prescription completed |
prescription.cancelled | Prescription cancelled |
prescription.expired | Prescription expired |
lab_result.created | Lab result created |
procedure.created | Procedure created |
invoice.paid | Invoice marked paid |
Webhook Payload Format
POST https://your-server.com/webhook
Content-Type: application/json
X-Webhook-Event: appointment.created
X-Webhook-Signature: <hmac-sha256-hex>
{
"event": "appointment.created",
"timestamp": "2026-03-17T14:30:00Z",
"data": {
"id": "uuid",
"patientId": "uuid",
"clientId": "uuid",
"locationId": "uuid",
"startTime": "2026-03-18T09:00:00Z",
"status": "scheduled"
}
}Verifying Signatures
import crypto from "crypto";
function verifySignature(
payload: string,
signature: string,
secret: string
): boolean {
const expected = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}