Workflows
Sync patients from a practice system
Mirror owners and animals from your practice-management system into Vetnio, link them to visits, and handle duplicates and retries safely.
This guide is for developers of a practice-management system (PMS) or another system of record who want every Vetnio consultation to land in the right animal's history. Your system stays the source of truth and pushes owners (clients) and animals (patients) to Vetnio in one direction. Read Patients and clients first for the data model.
Two properties of the API shape the whole workflow: patients and clients have no field for your own ID, and creating them is not idempotent. So the core of a reliable sync is a mapping table on your side plus a find-or-create routine that never creates blindly.
Before you start
- A personal API key. Organization and clinic API keys get
403on these endpoints; see Authentication. - The clinic's workspace ID, from GET
/workspaces. - A table in your database that maps your animal ID to the Vetnio patient ID and client ID.
- The
adminrole in the workspace, but only for the cleanup step (deleting).
If the clinic also connects a practice system through a built-in Vetnio integration (for example Provet Cloud or Vettigo), that integration creates and updates patients too, and its next sync can overwrite your changes. Agree with the clinic which system owns patient data before you start.
The sync for one animal has four steps:
| Step | When | Call |
|---|---|---|
| 1. Look up | No mapping yet | GET /patients?searchQuery=<microchip> |
| 2. Create | Nothing found | POST /patients with the owner in client |
| 3. Update | Mapping exists | PATCH /patients/{id} and PATCH /clients/{id} |
| 4. Link | A visit is documented in Vetnio | POST /consultations/{id}/patient |
1. Look for the animal in Vetnio
When your mapping has no entry for the animal, check whether the clinic already registered it in Vetnio. The microchip number is the best key. GET/workspaces/{workspaceId}/patients matches searchQuery as a case-insensitive substring of the patient name, the identifier and the owner's name:
curl "https://vetnio.com/api/v2/workspaces/demo-north-clinic/patients?searchQuery=941000024681357&pageSize=100" \
-H "Authorization: Bearer $VETNIO_API_KEY"{
"patients": [],
"totalCount": 0
}Because the search is a substring match, keep only results whose identifier equals the microchip exactly. Then:
- One match: adopt it. Save its
idandclientIdin your mapping and continue with step 3. - Several matches: the clinic has duplicates. Prefer the one whose
clientNamematches your owner; if that does not settle it, ask the clinic which record to keep rather than guessing. - No match: create the patient in step 2.
For animals without a microchip, search by name and compare name, species, date of birth and owner name before you adopt a result.
2. Create the patient with its owner
POST/workspaces/{workspaceId}/patients creates the patient and a new owner in one call:
curl -X POST https://vetnio.com/api/v2/workspaces/demo-north-clinic/patients \
-H "Authorization: Bearer $VETNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Max",
"species": "Dog - Border Collie",
"breed": "Border Collie",
"dateOfBirth": "2020-09-14",
"gender": "Male Neutered",
"weight": "19.2 kg",
"identifier": "941000024681357",
"insurance": "Petplan",
"client": {
"name": "Jordan Lee",
"email": "jordan.lee@example.com",
"address": "3 Station Road, Riverside",
"zipCode": "114 60",
"phoneNumbers": ["+46705550123"]
}
}'{
"id": "e55637f1-4969-4156-b856-24700fb174a7",
"name": "Max",
"species": "Dog - Border Collie",
"breed": "Border Collie",
"dateOfBirth": "2020-09-14",
"gender": "Male Neutered",
"weight": "19.2 kg",
"identifier": "941000024681357",
"insurance": "Petplan",
"clientId": "2893d7cf-3ac3-4012-b122-eb4ea0902710",
"createdAt": "2026-10-04T16:17:51.778Z",
"updatedAt": "2026-10-04T16:17:51.778Z",
"client": {
"id": "2893d7cf-3ac3-4012-b122-eb4ea0902710",
"name": "Jordan Lee",
"address": "3 Station Road, Riverside",
"zipCode": "114 60",
"phoneNumbers": ["+46705550123"],
"email": "jordan.lee@example.com"
}
}Save id and clientId in your mapping as soon as the response arrives. Map your values to Vetnio's conventions: dateOfBirth as YYYY-MM-DD (other formats fail or are read month first), gender as Male, Female, Male Neutered or Female Neutered, weight with its unit, and species as the English species name plus - <breed>.
3. Keep details up to date
When the animal or owner changes in your system, send only the changed fields. PATCH/workspaces/{workspaceId}/patients/{patientId} leaves omitted fields untouched; an empty string blanks a field, and null is rejected:
curl -X PATCH https://vetnio.com/api/v2/workspaces/demo-north-clinic/patients/e55637f1-4969-4156-b856-24700fb174a7 \
-H "Authorization: Bearer $VETNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "weight": "19.8 kg", "insurance": "" }'{
"success": true
}Owner details go to PATCH/workspaces/{workspaceId}/clients/{clientId}. phoneNumbers replaces the whole list, and the body must contain at least one field (an empty body currently fails with 500):
curl -X PATCH https://vetnio.com/api/v2/workspaces/demo-north-clinic/clients/2893d7cf-3ac3-4012-b122-eb4ea0902710 \
-H "Authorization: Bearer $VETNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "email": "jordan.lee@example.net", "phoneNumbers": ["+46705550123", "+46851234567"] }'{
"success": true
}Both updates return only success. GET/workspaces/{workspaceId}/patients/{patientId} shows the result with the owner embedded:
{
"id": "e55637f1-4969-4156-b856-24700fb174a7",
"name": "Max",
"species": "Dog - Border Collie",
"breed": "Border Collie",
"dateOfBirth": "2020-09-14",
"gender": "Male Neutered",
"weight": "19.8 kg",
"identifier": "941000024681357",
"insurance": "",
"clientId": "2893d7cf-3ac3-4012-b122-eb4ea0902710",
"createdAt": "2026-10-04T16:17:51.778Z",
"updatedAt": "2026-10-04T16:17:57.640Z",
"client": {
"id": "2893d7cf-3ac3-4012-b122-eb4ea0902710",
"name": "Jordan Lee",
"address": "3 Station Road, Riverside",
"zipCode": "114 60",
"phoneNumbers": ["+46705550123", "+46851234567"],
"email": "jordan.lee@example.net"
}
}A 404 with Resource not found means the record was deleted in Vetnio. Remove the stale mapping and run steps 1 and 2 again.
4. Link the patient to the visit
Create the visit's consultation with your appointment ID as externalId (POST/workspaces/{workspaceId}/consultations returns the existing consultation when the externalId was used before, so that call is safe to retry). Then link the patient with POST/workspaces/{workspaceId}/consultations/{consultationId}/patient:
curl -X POST https://vetnio.com/api/v2/workspaces/demo-north-clinic/consultations/431b6977-dbec-4029-94dc-ddc35fd6391a/patient \
-H "Authorization: Bearer $VETNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "patientId": "e55637f1-4969-4156-b856-24700fb174a7" }'{
"success": true
}The link replaces any patient already on the consultation, so repeating it is safe. Archived consultations return 403. To check a link, read the consultation list from GET/workspaces/{workspaceId}/consultations; each item carries patientId (shortened here):
{
"consultations": [
{
"id": "431b6977-dbec-4029-94dc-ddc35fd6391a",
"name": "Max - lameness check",
"createdAt": "2026-10-04T16:17:57.813Z",
"externalId": "pims-visit-20977",
"patientId": "e55637f1-4969-4156-b856-24700fb174a7",
"clientId": null,
…
}
],
"count": 1,
"itemsPerPage": 5,
"totalPages": 1
}The consultation's clientId stays null: linking sets only the patient. Read the owner from the patient.
Handle duplicates and retries
| Call | Safe to repeat? | On timeout or 5xx |
|---|---|---|
| Search patients | Yes | Retry. |
| Create patient | No | Search by microchip, then create only if nothing is found. |
| Update patient or client | Yes | Retry with the same body. |
| Link patient | Yes | Retry; the result is the same. |
Create consultation with externalId | Yes | Retry; you get the same consultation. |
| Delete | Yes | A 404 on retry means the first delete succeeded. |
Vetnio does not lock anything for you, so two workers that sync the same animal at the same moment can both create it. Process each animal in one worker at a time, for example with a per-animal lock or queue key.
If duplicates already exist, link their consultations to the patient you keep, then delete the duplicate patient (admin only). Deleting a patient keeps its consultations but unlinks them, so move them first.
What the API cannot do today
| You need to | Today | Workaround |
|---|---|---|
| Store your own ID on a patient or owner | No field for it | Keep the mapping in your database. Do not put your ID in identifier; vets read it as the microchip number. |
| Give an existing owner another animal | Every create makes a new owner | Accept one Vetnio owner per animal. Map all of them to your owner ID and update each one when the owner's details change. |
| Move an animal to a new owner | clientId cannot change | Update the patient's own owner record with the new owner's details. Owners created through the API belong to exactly one patient; for older records, first search patients by the owner's name and confirm no other patient has the same clientId. |
| Unlink a consultation | No endpoint | Link the correct patient instead. |
| List consultations for one patient | No filter | Filter the consultation list by patientId on your side. |
| Find records changed in Vetnio | No change feed or change filter | Sync one way, from your system to Vetnio. |
| Search owners | No search | GET/workspaces/{workspaceId}/clients returns all owners in one unpaginated response. Search patients by owner name instead. |
Clean up
Remove test data with the delete endpoints (admin role required). Deleting a client also deletes every patient it owns; each returns an empty object:
curl -X DELETE https://vetnio.com/api/v2/workspaces/demo-north-clinic/clients/2893d7cf-3ac3-4012-b122-eb4ea0902710 \
-H "Authorization: Bearer $VETNIO_API_KEY"{}Common errors
| Status | Message | Cause | Fix |
|---|---|---|---|
400 | Input validation failed | A value has the wrong type (for example null), or pageSize is over 100. | Fix the field named in issues. |
403 | This API key cannot access this endpoint | Organization or clinic API key. | Use a personal API key. |
403 | You are not an admin of this workspace | Delete without the admin role. | Use an admin's key, or ask the clinic. |
403 | You cannot modify this consultation | The consultation is archived. | Link the patient in the Vetnio app. |
404 | Resource not found | Unknown patient or client ID, or one from another workspace. | Drop the mapping and find or create again. |
404 | Consultation not found | Unknown consultation ID. | Check the ID and workspace. |
500 | An unexpected error occurred | Invalid dateOfBirth, or an empty client update. | Send YYYY-MM-DD; send at least one field. |
See Errors for the error format and Limits for request limits.
Complete example
This Python script syncs one animal and links it to a visit. Replace the in-memory mapping with your database.
import os
import requests
BASE = "https://vetnio.com/api/v2/workspaces/demo-north-clinic"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['VETNIO_API_KEY']}"
mapping = {} # your animal id -> {"patientId": ..., "clientId": ...}
def call(method, path, **kwargs):
response = session.request(method, BASE + path, timeout=30, **kwargs)
response.raise_for_status()
return response.json()
def without_nulls(body):
return {key: value for key, value in body.items() if value is not None} # null is rejected
def patient_body(animal):
return without_nulls({
"name": animal["name"], "species": animal["species"], "breed": animal["breed"],
"gender": animal["sex"], "identifier": animal["microchip"], "insurance": animal["insurer"],
"dateOfBirth": animal["birth_date"].isoformat() if animal["birth_date"] else None,
"weight": f"{animal['weight_kg']} kg" if animal["weight_kg"] else None,
})
def owner_body(owner):
return without_nulls({
"name": owner["name"], "email": owner["email"], "address": owner["address"],
"zipCode": owner["zip"], "phoneNumbers": owner["phones"],
})
def find_patient(animal):
page = call("GET", "/patients", params={"searchQuery": animal["microchip"], "pageSize": 100})
matches = [p for p in page["patients"] if p["identifier"] == animal["microchip"]]
if len(matches) > 1:
matches = [p for p in matches if p["clientName"] == animal["owner"]["name"]]
if len(matches) > 1:
raise RuntimeError(f"Duplicate patients for {animal['microchip']}; ask the clinic which to keep")
return matches[0] if matches else None
def sync_animal(animal):
known = mapping.get(animal["id"])
if known:
try:
call("PATCH", f"/patients/{known['patientId']}", json=patient_body(animal))
owner = owner_body(animal["owner"])
if owner: # an empty client update fails
call("PATCH", f"/clients/{known['clientId']}", json=owner)
return known["patientId"]
except requests.HTTPError as error:
if error.response.status_code != 404:
raise
del mapping[animal["id"]] # deleted in Vetnio: find or create again
patient = find_patient(animal) if animal["microchip"] else None
if patient is None:
try:
patient = call("POST", "/patients",
json={**patient_body(animal), "client": owner_body(animal["owner"])})
except (requests.Timeout, requests.ConnectionError):
# The create may have succeeded: look again before anyone retries it.
patient = find_patient(animal) if animal["microchip"] else None
if patient is None:
raise
mapping[animal["id"]] = {"patientId": patient["id"], "clientId": patient["clientId"]}
return patient["id"]
def link_visit(animal, visit):
patient_id = sync_animal(animal)
consultation = call("POST", "/consultations", json={
"name": f"{animal['name']} - {visit['reason']}",
"recordingLanguageCode": "en",
"externalId": visit["id"],
})
call("POST", f"/consultations/{consultation['consultationId']}/patient", json={"patientId": patient_id})
return consultation["consultationId"]Next steps
- Patients and clients: field formats, search semantics and delete rules.
- Document a visit end to end: record and transcribe the consultation you linked.
- Errors and Limits: status codes, retries and request limits.
- API reference: Patients and Clients.
Endpoints used in this guide
- GET
/workspaces/{workspaceId}/patientsList patients - POST
/workspaces/{workspaceId}/patientsCreate a patient - GET
/workspaces/{workspaceId}/patients/{patientId}Get patient details - PATCH
/workspaces/{workspaceId}/patients/{patientId}Update a patient - PATCH
/workspaces/{workspaceId}/clients/{clientId}Update a client (owner) - POST
/workspaces/{workspaceId}/consultations/{consultationId}/patientLink a patient to a consultation - DELETE
/workspaces/{workspaceId}/patients/{patientId}Delete a patient - DELETE
/workspaces/{workspaceId}/clients/{clientId}Delete a client (owner) - GET
/workspaces/{workspaceId}/clientsList clients (owners) - POST
/workspaces/{workspaceId}/consultationsCreate a new consultation - GET
/workspaces/{workspaceId}/consultationsGet a page of consultations - GET
/workspacesList workspaces
Last reviewed against the API on 2026-10-26.