Skip to main content

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.

POSTauth.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 }
GETauth.me

Get 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.

GETclients.list

List clients with optional search and pagination.

Input

{
  search?: string,
  limit?: number,    // 1-100, default 25
  offset?: number    // default 0
}

Response

{
  items: Client[],
  total: number
}
GETclients.search

Quick search clients by name, email, or phone. Returns up to 10 results.

Input

{ query: string }

Response

Client[]
GETclients.getById

Get a single client with their patients.

Input

{ id: string }

Response

{
  ...Client,
  patients: Patient[]
}
POSTclients.create

Create 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
POSTclients.rotatePortalAccessToken

Create or rotate a client's private portal link. Existing portal URLs stop working immediately after rotation.

Input

{ id: string }

Response

{
  id: string,
  accessToken: string
}
POSTclients.update

Update an existing client.

Input

{
  id: string,
  firstName?: string,
  lastName?: string,
  email?: string,
  phone?: string,
  address?: string,
  city?: string,
  state?: string,
  zip?: string
}

Response

Client
POSTclients.delete

Soft-delete a client record.

Input

{ id: string }

Response

{ success: true }

Patients

Manage animal patient records, weights, and allergies.

GETpatients.list

List 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
}
GETpatients.search

Quick search by patient, owner, or breed. Returns up to 10 deterministically ordered results.

Input

{ query: string }

Response

Patient[]
GETpatients.getById

Get full patient details including weights, allergies, and owner info.

Input

{ id: string }

Response

{
  ...Patient,
  weights: Weight[],
  allergies: Allergy[],
  ownerName: string
}
POSTpatients.create

Create 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
POSTpatients.update

Update 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
POSTpatients.delete

Soft-delete a patient record.

Input

{ id: string }

Response

{ success: true }
POSTpatients.addWeight

Record a weight measurement.

Input

{
  patientId: string,
  weight: number,
  unit: string
}

Response

Weight
POSTpatients.addAllergy

Record a known allergy.

Input

{
  patientId: string,
  allergen: string,
  severity?: string,
  notes?: string
}

Response

Allergy

Appointments

Schedule and manage appointments.

GETappointments.list

List appointments within a date range.

Input

{
  startDate: string,  // ISO date
  endDate: string,    // ISO date
  doctorId?: string,
  locationId?: string
}

Response

Appointment[]
GETappointments.getById

Get full appointment details.

Input

{ id: string }

Response

Appointment
POSTappointments.create

Schedule 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
POSTappointments.updateStatus

Update 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
GETappointments.listTypes

List available appointment types for the practice.

Response

AppointmentType[]
GETappointments.listDoctors

List veterinarians available for scheduling.

Response

Doctor[]
GETappointments.listLocations

List active clinic locations available for scheduling.

Response

Array<{ id: string, name: string, address: string | null, phone: string | null, isPrimary: boolean }>
GETappointments.listRooms

List exam rooms, optionally for one clinic location.

Input

{ locationId?: string }

Response

Room[]

Medical Records

SOAP notes, vaccinations, lab results, procedures, problems, and prescriptions.

GETrecords.listSoapNotes

List SOAP notes for a patient.

Input

{ patientId: string }

Response

SoapNote[]
POSTrecords.createSoapNote

Create 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
GETrecords.listVaccinations

List vaccination records for a patient.

Input

{ patientId: string }

Response

Vaccination[]
POSTrecords.createVaccination

Record a vaccination.

Input

{
  patientId: string,
  vaccineName: string,
  manufacturer?: string,
  lotNumber?: string,
  expirationDate?: string,
  nextDueDate?: string,
  notes?: string
}

Response

Vaccination
GETrecords.listLabResults

List lab results for a patient.

Input

{ patientId: string }

Response

LabResult[]
POSTrecords.createLabResult

Create a lab result entry.

Input

{
  patientId: string,
  testName: string,
  category?: string,
  results?: object,
  notes?: string
}

Response

LabResult
POSTrecords.updateLabResultStatus

Update the status of a lab result.

Input

{
  id: string,
  status: "pending" | "completed" | "reviewed"
}

Response

LabResult
GETrecords.listProcedures

List procedures performed on a patient.

Input

{ patientId: string }

Response

Procedure[]
POSTrecords.createProcedure

Record a procedure.

Input

{
  patientId: string,
  name: string,
  description?: string,
  notes?: string
}

Response

Procedure
GETrecords.listProblems

List active and resolved problems for a patient.

Input

{ patientId: string }

Response

Problem[]
POSTrecords.createProblem

Add a problem to the patient's problem list.

Input

{
  patientId: string,
  description: string,
  severity?: string,
  notes?: string
}

Response

Problem
POSTrecords.updateProblemStatus

Mark a problem as resolved or reactivate it.

Input

{
  id: string,
  status: "active" | "resolved"
}

Response

Problem
GETrecords.listPrescriptions

List prescriptions for a patient.

Input

{ patientId: string }

Response

Prescription[]
POSTrecords.createPrescription

Create 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.

GETbilling.listInvoices

List invoices with optional filters.

Input

{
  status?: string,
  isEstimate?: boolean,
  limit?: number,
  offset?: number
}

Response

{
  items: Invoice[],
  total: number
}
GETbilling.getInvoice

Get full invoice with line items and payments.

Input

{ id: string }

Response

{
  ...Invoice,
  items: InvoiceItem[],
  payments: Payment[]
}
POSTbilling.createInvoice

Create 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
POSTbilling.updateInvoiceStatus

Update 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
POSTbilling.convertEstimateToInvoice

Convert an approved estimate into a billable invoice.

Input

{ id: string }

Response

Invoice
POSTbilling.recordPayment

Record a payment against an invoice.

Input

{
  invoiceId: string,
  amount: number,
  method: "cash" | "credit_card" | "debit_card" | "check" | "online" | "other",
  notes?: string
}

Response

Payment
POSTbilling.createCardPaymentCheckout

Create a Stripe Checkout link for the remaining adjusted invoice balance.

Input

{ invoiceId: string }

Response

{ url: string }
GETbilling.listPayments

List payments for an invoice.

Input

{ invoiceId: string }

Response

Payment[]
GETbilling.listAdjustments

List credits and write-offs for an invoice.

Input

{ invoiceId: string }

Response

InvoiceAdjustment[]
POSTbilling.applyInvoiceAdjustment

Apply a credit or write-off to an invoice balance.

Input

{
  invoiceId: string,
  type: "credit" | "write_off",
  amount: number,
  reason?: string
}

Response

InvoiceAdjustment
POSTbilling.voidInvoice

Void an invoice with no payment or adjustment history.

Input

{ id: string }

Response

Invoice
GETbilling.listServices

List all services offered by the practice.

Response

Service[]
GETbilling.listArchivedServices

List archived services for administrator recovery.

Response

Service[]
POSTbilling.createService

Create a service in the practice charge catalog.

Input

{ name: string, code?: string, category?: string, defaultPrice: string }

Response

Service
POSTbilling.updateService

Update 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
POSTbilling.archiveService

Remove 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 }
POSTbilling.restoreService

Restore an archived service to future charge pickers.

Input

{ id: string, expected: { name: string, code?: string, category?: string, defaultPrice: string } }

Response

{ success: true }
GETbilling.listProducts

List 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.

GETportal.getClientPortal token

Get 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 }>
}
GETportal.getPetDetailPortal token

Get full pet details including medical history.

Input

{
  token: string,
  patientId: string
}

Response

{
  ...Patient,
  vaccinations: Vaccination[],
  prescriptions: Prescription[],
  weights: Weight[],
  allergies: Allergy[]
}
GETportal.getAppointmentsPortal token

List upcoming appointments for the client.

Input

{ token: string }

Response

Appointment[]
GETportal.getInvoicesPortal token

List invoices for the client.

Input

{ token: string }

Response

Invoice[]
GETportal.getMessagesPortal token

List 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
  }>
}
POSTportal.createMessagePortal token

Send a portal message from the client into the shared inbox.

Input

{
  token: string,
  content: string
}

Response

{ success: true, message: Communication }
POSTportal.markMessagesReadPortal token

Mark outbound clinic portal messages as read after the client opens the thread.

Input

{ token: string }

Response

{ success: true, updated: number }
GETportal.getAppointmentTypesPortal token

List appointment types available for portal booking.

Input

{ token: string }

Response

Array<{ id: string, name: string, durationMinutes: number, requiresDoctor: number }>
GETportal.availableSlotsPortal token

List 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 }>
POSTportal.requestAppointmentPortal token

Submit 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.

GETapiKeys.listAdmin only

List active API keys for the practice.

Response

Array<{
  id: string,
  name: string,
  keyPrefix: string,
  scopes: ApiScope[],
  lastUsedAt: string | null,
  createdAt: string
}>
POSTapiKeys.createAdmin only

Create 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 }
POSTapiKeys.revokeAdmin only

Revoke 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>.

GETGET /api/v1/clientsAPI key: clients:read

List clients for the authenticated practice.

Input

?limit=25&offset=0

Response

{ data: Client[], pagination: Pagination }
GETGET /api/v1/clients/:idAPI key: clients:read

Fetch a single client.

Response

{ data: Client }
GETGET /api/v1/patientsAPI key: patients:read

List patients, optionally filtered by client.

Input

?client_id=uuid&limit=25&offset=0

Response

{ data: Patient[], pagination: Pagination }
GETGET /api/v1/patients/:idAPI key: patients:read

Fetch a single patient.

Response

{ data: Patient }
GETGET /api/v1/appointmentsAPI key: appointments:read

List 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 }
GETGET /api/v1/appointments/:idAPI key: appointments:read

Fetch a single appointment.

Response

{ data: Appointment }
POSTPOST /api/v1/appointmentsAPI key: appointments:write

Create 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 }
POSTPOST /api/v1/soap-notesAPI key: records:write

Create 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 }
POSTPOST /api/v1/agentAPI key: agent:run; agent:write plus resource write scopes when allow_writes=true

Run 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.

GETwebhooks.listAdmin only

List all webhooks for the practice.

Response

Webhook[]
POSTwebhooks.createAdmin only

Create a webhook subscription. The secret is returned once and cannot be retrieved again.

Input

{
  url: string,
  events: WebhookEvent[]
}

Response

{
  ...Webhook,
  secret: string   // shown once
}
POSTwebhooks.toggleAdmin only

Enable or disable a webhook.

Input

{ id: string }

Response

Webhook
POSTwebhooks.deleteAdmin only

Delete a webhook subscription.

Input

{ id: string }

Response

{ success: true }

Inventory

Track products, stock levels, and suppliers.

GETinventory.list

List 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
  }
}
POSTinventory.create

Add 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
POSTinventory.update

Update 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
POSTinventory.adjustStock

Adjust stock quantity (positive or negative).

Input

{
  id: string,
  adjustment: number,
  reason: string
}

Response

InventoryItem
GETinventory.listSuppliers

List all suppliers.

Response

Supplier[]
POSTinventory.createSupplier

Add a new supplier.

Input

{
  name: string,
  contactEmail?: string,
  phone?: string,
  address?: string,
  notes?: string
}

Response

Supplier
POSTinventory.updateSupplier

Update 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.

GETreports.revenue

Revenue 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 }>
}
GETreports.appointments

Appointment 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 }>
}
GETreports.topServices

Top 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 }>
}
GETreports.inventoryAlerts

Current low-stock, expired, and expiring-product alerts.

Response

{
  lowStock: Product[],
  expired: Product[],
  expiringSoon: Product[]
}

Settings

Admin-only practice configuration endpoints.

GETsettings.listLocationsAdmin only

List active practice locations.

Response

Array<{
  id: string,
  name: string,
  address: string | null,
  phone: string | null,
  isPrimary: boolean
}>
POSTsettings.createLocationAdmin only

Create a practice location and sync hosted billing quantities.

Input

{
  name: string,
  address?: string,
  phone?: string,
  isPrimary?: boolean
}

Response

Location
POSTsettings.updateLocationAdmin only

Update a tenant-scoped location.

Input

{
  id: string,
  name?: string,
  address?: string,
  phone?: string
}

Response

Location
POSTsettings.setPrimaryLocationAdmin only

Make one active tenant location the primary location.

Input

{ id: string }

Response

Location
POSTsettings.deleteLocationAdmin only

Retire 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.

GETcommunications.list

List 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
}
GETcommunications.listConversations

List 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
}
GETcommunications.getByClient

Get all communications for a specific client.

Input

{ clientId: string }

Response

Array<Communication & {
  readAt: Date | null,
  providerMessageId: string | null,
  assignedToName: string | null
}>
POSTcommunications.markClientRead

Mark unread inbound messages for a client thread as read.

Input

{ clientId: string }

Response

{ ok: true, updated: number }
POSTcommunications.assignClient

Assign 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
}
POSTcommunications.linkCommunicationToClient

Link 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
}
POSTcommunications.create

Send 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
POSTcommunications.updateStatus

Mark 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.

EventDescription
appointment.createdNew appointment created
appointment.checked_inAppointment checked in
appointment.rescheduledAppointment rescheduled
appointment.cancelledAppointment cancelled
client.createdNew client record created
patient.createdNew patient record created
soap_note.createdSOAP note created
vaccination.recordedVaccination recorded
problem.createdProblem list item created
prescription.createdPrescription created
prescription.refill_dispensedPrescription refill dispensed
prescription.refill_authorizedExternal prescription refill authorized
prescription.completedPrescription completed
prescription.cancelledPrescription cancelled
prescription.expiredPrescription expired
lab_result.createdLab result created
procedure.createdProcedure created
invoice.paidInvoice 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)
  );
}