Read and write your clinic from your own code: a REST API with scoped keys and per-key rate limits over clients, patients, appointments, invoices, payments and vaccinations, and signed webhooks when something changes.
API keys
The Veterical REST API gives your own systems the same records the clinic works with every day. Two of its nouns are easy to mix up: a client is the human who owns the animal, and a patient is the animal itself, so a patient always belongs to a client.
API keys are issued by an administrator of your organization and belong to a single clinic, so the clinic is implied by the key and never has to be sent. The secret is returned exactly once, when the key is created, and never again — store it somewhere safe.
Authenticate every request with HTTP Basic auth carrying only the key secret, base64-encoded, in the Authorization header.
# The Authorization header is HTTP Basic auth carrying only the key secret,
# with no username and no colon.
Authorization: Basic $(printf %s YOUR_API_KEY_SECRET | base64)Every endpoint lives under https://api.veterical.com. Requests made with a key are rate limited per key; going over the limit returns 429. Reading a record that belongs to another clinic returns 404 rather than 403, so ids cannot be probed.
Quick start
List the clients of your clinic, register an animal against one of them, then read it back.
# List the clients — the people who own the animals — of the key's clinic
curl https://api.veterical.com/api/clients \
-H "Authorization: Basic $(printf %s YOUR_API_KEY_SECRET | base64)"
# Register a patient — the animal — against the client who owns it
curl -X POST https://api.veterical.com/api/patients \
-H "Authorization: Basic $(printf %s YOUR_API_KEY_SECRET | base64)" \
-H "Content-Type: application/json" \
-d '{
"clientId": "CLIENT_ID",
"name": "Luna",
"species": "dog",
"breed": "Labrador Retriever",
"sex": "female",
"dateOfBirth": "2021-04-18"
}'
# Read that patient back
curl https://api.veterical.com/api/patients/PATIENT_ID \
-H "Authorization: Basic $(printf %s YOUR_API_KEY_SECRET | base64)"Browse the full API reference — every endpoint with its parameters, request body, responses and required scope.
Scopes
Each key carries a list of scopes, so an integration that only needs to read your appointment book never gets the ability to change a medical record. New keys start read-only; widen them explicitly. A request whose key is missing the scope an endpoint requires is refused with 403.
Everything else the dashboard can do — users, roles, invites, clinics, quotes, documents, templates, preferences, printers and subscriptions — is deliberately outside the key vocabulary and cannot be reached with an API key at all.
Webhooks
Add a webhook subscription for your clinic and Veterical POSTs the events you picked to your server as they happen. Subscribe to an empty list of events to receive all of them.
POST https://your-server.com/veterical-webhook
X-Veterical-Event: appointment.created
X-Veterical-Signature: t=1719000000,v1=<hmac-sha256 hex>
Content-Type: application/json
{
"event": "appointment.created",
"timestamp": 1719000000,
"data": { "...": "..." }
}Every delivery carries an X-Veterical-Signature header of the form t=timestamp,v1=signature, where the signature is an HMAC-SHA256 of timestamp.body keyed by the subscription secret shown to you once when the subscription was created. Recompute it over the raw body and compare before trusting the payload.
import crypto from 'node:crypto'
// body must be the RAW request body, byte for byte
function verify(header, body, secret) {
const [t, v1] = (header || '').split(',').map(part => part.split('=')[1])
if (!t || !v1) return false
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${body}`)
.digest('hex')
// timingSafeEqual throws on a length mismatch, so a malformed signature
// has to be rejected before the comparison rather than by it.
if (v1.length !== expected.length) return false
return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
}Delivery is one best-effort attempt with a five second timeout and no retries, so respond 2xx quickly and do the work asynchronously. An endpoint that fails twenty times in a row is disabled automatically and has to be re-enabled.
Start building