Workflows
Manage templates
List, create, update, duplicate and delete a clinic's templates, keep them in sync from your own system without overwriting edits made in Vetnio, and handle permission errors.
Use these endpoints when your system provides templates to a clinic, for example when a clinic group rolls out one standard lameness work-up to every clinic, or when a practice system lets vets pick a Vetnio template for a visit. Each step below is one call, with the real response recorded against a Vetnio instance with synthetic data. Read Templates first for what a template is and how Vetnio chooses one.
Before you start
- A personal API key (see Authentication) for a user who is a direct member of the clinic. Organization admins without a clinic membership can list and edit templates but cannot create or duplicate them.
- To edit templates other people created, the user must be a clinic admin or an admin of the clinic's organization (also when they are a plain member of the clinic), or the clinic's template permissions must be Full Access.
- Template bodies in your system, written as headings with instructions in parentheses.
Who can do what:
| Action | Endpoint | Who |
|---|---|---|
| List | GET/workspaces/{workspaceId}/templates | Any clinic member; the list is filtered by the clinic's template permissions |
| Create | POST/workspaces/{workspaceId}/templates | Any direct clinic member |
| Update the body | PUT/workspaces/{workspaceId}/templates/{templateId} | The creator, clinic admins, organization admins, and under Full Access other members for templates members wrote that are not managed. Never Vetnio defaults or templates shared by the clinic's organization. |
| Duplicate | POST/workspaces/{workspaceId}/templates/{templateId}/duplicate | Any direct clinic member who can see the template |
| Delete | DELETE/workspaces/{workspaceId}/templates/{templateId} | The same users as update |
| Read the body | GET/workspaces/{workspaceId}/data-export/templates/version | Clinic admins; not Vetnio defaults |
1. List templates
curl https://vetnio.com/api/v2/workspaces/demo-north-clinic/templates \
-H "Authorization: Bearer $VETNIO_API_KEY"The response is an array sorted by name, ignoring case. Two of the clinic's templates:
[
{
"id": "7ce0fa1b-777b-55f9-a58b-7fcabc410504",
"name": "DEMO SOAP note",
"type": "clinical_note",
"creatorId": "c1b0f2a0-f819-5fcc-a2ed-85db0cef66a7",
"creatorRole": "admin",
"createdAt": "2026-10-04T15:45:44.068Z",
"jsonTemplateId": null,
"isDefault": true,
"isManaged": false,
"isHidden": false,
"isPinned": false
},
{
"id": "3ae1a5c2-04b7-43cb-9c1f-d3bcb4a4596a",
"name": "Vaccination visit",
"type": "clinical_note",
"creatorId": "c1b0f2a0-f819-5fcc-a2ed-85db0cef66a7",
"creatorRole": "admin",
"createdAt": "2026-10-04T16:06:01.542Z",
"jsonTemplateId": null,
"isDefault": false,
"isManaged": false,
"isHidden": false,
"isPinned": false
}
]The list is not paginated and contains metadata only. Names are not unique: a clinic can hold several templates called Vaccination visit, so keep the id of every template you create in your own system rather than finding templates by name. isDefault: true marks a read-only Vetnio default template (see step 5).
2. Create a template
curl -X POST https://vetnio.com/api/v2/workspaces/demo-north-clinic/templates \
-H "Authorization: Bearer $VETNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Lameness work-up",
"type": "clinical_note",
"template": "<p><strong>History</strong></p><p>(Summarize the presenting complaint, duration and progression as discussed. If absent, write \"Not recorded\".)</p><p><strong>Examination</strong></p><p>(Include only findings explicitly recorded in this consultation, including gait assessment and joint palpation. Preserve measurements and units.)</p><p><strong>Plan</strong></p><p>(List only the treatments and follow-up actually discussed, one bullet per action.)</p>"
}'{
"templateId": "b034e407-5d2b-48c5-b26b-11d0b9468421"
}The template belongs to the calling user, with their current clinic role as creatorRole. type defaults to clinical_note. To make clinical notes from this template structured, pass the ID of a structured template created in the clinic as jsonTemplateId; the built-in structured templates cannot be linked. The link is optional: a structured template can be filled without one (Generate a structured document). Name and type cannot be changed through the API later, and the link cannot be changed at all.
Creating is not idempotent: retrying a timed-out request can create a second template. If you are unsure whether a create succeeded, list the templates before retrying.
3. Update the body
PUT/workspaces/{workspaceId}/templates/{templateId} replaces the whole body. The response contains a contentVersion tag; store it:
curl -X PUT https://vetnio.com/api/v2/workspaces/demo-north-clinic/templates/b034e407-5d2b-48c5-b26b-11d0b9468421 \
-H "Authorization: Bearer $VETNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"template": "<p><strong>History</strong></p><p>(Summarize the presenting complaint, duration and progression as discussed. If absent, write \"Not recorded\".)</p><p><strong>Examination</strong></p><p>(Include only findings explicitly recorded in this consultation: gait, lameness grade, joint palpation. Preserve measurements and units.)</p><p><strong>Plan</strong></p><p>(List only the treatments and follow-up actually discussed, one bullet per action.)</p>"
}'{
"success": true,
"contentVersion": "\"e3824548205b4da7ec67c9754d97a3b0\""
}On the next update, send that tag as expectedContentVersion, exactly as you received it, including the double quotes. The update succeeds only if nobody changed the body in between, in Vetnio or through the API:
curl -X PUT https://vetnio.com/api/v2/workspaces/demo-north-clinic/templates/b034e407-5d2b-48c5-b26b-11d0b9468421 \
-H "Authorization: Bearer $VETNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"template": "<p><strong>History</strong></p><p>(Summarize the presenting complaint, duration and progression as discussed. If absent, write \"Not recorded\".)</p><p><strong>Examination</strong></p><p>(Include only findings explicitly recorded in this consultation: gait, lameness grade (0-5), joint palpation. Preserve measurements and units.)</p><p><strong>Plan</strong></p><p>(List only the treatments and follow-up actually discussed, one bullet per action.)</p>",
"expectedContentVersion": "\"e3824548205b4da7ec67c9754d97a3b0\""
}'{
"success": true,
"contentVersion": "\"42373ca515f34969b0e046fffa0e11e4\""
}Sending the old tag again fails, because the body has changed since that version:
{
"message": "The template was changed elsewhere. Reload to see the latest version.",
"code": "CONFLICT",
"data": {
"code": "CONFLICT",
"httpStatus": 409,
"path": "templates.updateContent",
"zodError": null
}
}Nothing is saved on a 409. Treat it as "a person edited this template": read the current body (step 4), decide with the clinic whose version wins, and then update without expectedContentVersion to overwrite. Create returns no tag, and no endpoint returns the tag of the current body, so the first update after a create, or after a conflict, is always unconditional.
The new body applies to every later generation that uses the template, for every user of the clinic. Notes generated earlier are not rewritten. Vetnio keeps the previous body in the template's history.
4. Read the current body
There is no template-body endpoint for members, but clinic admins can read bodies through the configuration export. GET/workspaces/{workspaceId}/data-export/templates/versions lists the saved versions of a body, newest first, and GET/workspaces/{workspaceId}/data-export/templates/version returns one:
curl "https://vetnio.com/api/v2/workspaces/demo-north-clinic/data-export/templates/version?templateId=b034e407-5d2b-48c5-b26b-11d0b9468421&versionId=6724a04a60c67b97c95396bef78c5de5" \
-H "Authorization: Bearer $VETNIO_API_KEY"{
"currentVersionId": "6724a04a60c67b97c95396bef78c5de5",
"version": {
"versionId": "6724a04a60c67b97c95396bef78c5de5",
"isLatest": true,
"lastModified": "2026-10-04T16:15:10.000Z",
"size": 450,
"contentType": "text/html",
"audit": {
"id": "068e37a7-2dbb-4b6a-b110-7cee67d8816f",
"occurredAt": "2026-10-04T16:15:10.478Z",
"actorName": "Alex Demo",
"actorType": "user",
"action": "template.content_updated",
"s3VersionId": "6724a04a60c67b97c95396bef78c5de5"
}
},
"content": "<p><strong>History</strong></p><p>(Summarize the presenting complaint, duration and progression as discussed. If absent, write \"Not recorded\".)</p><p><strong>Examination</strong></p><p>(Include only findings explicitly recorded in this consultation: gait, lameness grade (0-5), joint palpation. Preserve measurements and units.)</p><p><strong>Plan</strong></p><p>(List only the treatments and follow-up actually discussed, one bullet per action.)</p>"
}content is the body as Vetnio stores it after sanitizing: unsafe markup is removed and, in plain text, &, < and > become HTML entities. Compare against this, not against what you sent. versionId is not the contentVersion tag and cannot be used as expectedContentVersion.
5. Customize a default template
Vetnio default templates (isDefault: true) are read-only. Updating or deleting one returns 403:
{
"message": "Default templates cannot be modified",
"code": "FORBIDDEN",
"data": {
"code": "FORBIDDEN",
"httpStatus": 403,
"path": "templates.updateContent",
"zodError": null
}
}POST/workspaces/{workspaceId}/templates/{templateId}/duplicate makes an editable copy owned by the caller, with the same body, type and structured-template link. Vetnio adds a space between the original name and copySuffix, so do not start the suffix with one:
curl -X POST https://vetnio.com/api/v2/workspaces/demo-north-clinic/templates/7ce0fa1b-777b-55f9-a58b-7fcabc410504/duplicate \
-H "Authorization: Bearer $VETNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "copySuffix": "(copy)" }'{
"id": "5b87cb7b-61aa-4a40-be7e-bde6f3a4617f"
}The copy is listed as DEMO SOAP note (copy), with isDefault: false and isManaged: false. Note that the field is id here, while create returns templateId. Duplicating works on any template you can see, which also makes it the way to customize a managed or organization template. Use your users' language for the suffix, for example (kopia) in Swedish.
6. Use the template
Select the template when you create a consultation, with clinicalNoteTemplateId on POST/workspaces/{workspaceId}/consultations:
curl -X POST https://vetnio.com/api/v2/workspaces/demo-north-clinic/consultations \
-H "Authorization: Bearer $VETNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Bella - lameness work-up",
"recordingLanguageCode": "en",
"clinicalNoteTemplateId": "b034e407-5d2b-48c5-b26b-11d0b9468421"
}'{
"consultationId": "bacc1d9a-9663-404f-a659-e3a6d434f7a7",
"recordingUploadUrl": "https://storage.example.com/recordings/demo-north-clinic/bacc1d9a-9663-404f-a659-e3a6d434f7a7/1791131228530-en?X-Amz-Expires=86400&X-Amz-Signature=…",
"consultation": {
"id": "bacc1d9a-9663-404f-a659-e3a6d434f7a7",
"name": "Bella - lameness work-up",
"createdAt": "2026-10-04T16:20:28.529Z",
"recordingDurationInMilliseconds": 0,
"uploadedRecordingDurationInMilliseconds": 0,
"isGeneratingClinicalNote": false,
"isGeneratingCommunication": false,
"externalId": null
}
}Or pass templateId to POST/workspaces/{workspaceId}/consultations/{consultationId}/clinical-note/generate, which also saves it as the consultation's selection. Neither call rejects an unknown ID: the note is then written without a template. clinicalNoteTemplateId is ignored when you also send externalId. Templates has the full selection rules.
7. Delete a template
curl -X DELETE https://vetnio.com/api/v2/workspaces/demo-north-clinic/templates/5b87cb7b-61aa-4a40-be7e-bde6f3a4617f \
-H "Authorization: Bearer $VETNIO_API_KEY"{}The template disappears from every list and picker in the clinic and cannot be restored. Notes generated with it are unchanged. Consultations that had it selected lose the selection, so their next generation runs without a template unless you pass one. Deleting it again returns 404 Template not found, so treat a 404 on delete as "already gone".
Common errors
| Status | message | Cause | Fix |
|---|---|---|---|
400 | Input validation failed | A required field is missing (template, name, copySuffix) or type is not one of the three values. issues lists each field. | Fix the request body. |
403 | Default templates cannot be modified | Updating or deleting a Vetnio default template. | Duplicate it and edit the copy. |
403 | Organisation templates are changed on the organisation page | Updating or deleting a template that the clinic's organization shares to the clinic. | Ask an organization admin to change it in Vetnio, or duplicate it and edit the copy. |
403 | You cannot modify this template | The user may see the template but not change it under the clinic's template permissions. | Use the creator's key or a clinic admin's key. |
403 | This API key cannot access this endpoint | An organization or clinic export key. | Use a personal API key. |
404 | Template not found | The template does not exist, was deleted, belongs to another clinic or is hidden from the user. On create, the jsonTemplateId is unknown. | List templates with the same key to see what it can reach. |
409 | The template was changed elsewhere. Reload to see the latest version. | expectedContentVersion no longer matches the body. | Read the body, resolve the difference, update again. |
415 | Missing content-type header | No JSON body. | Send Content-Type: application/json and a body. |
500 | An unexpected error occurred | The clinic already holds more than 100 templates; the caller is not a direct clinic member; or jsonTemplateId names a built-in structured template. | Delete unused templates, use a clinic member's key, or link a structured template created in the clinic. |
Keep templates in sync
This Python script pushes the templates defined in your system to a clinic. It remembers each template's id and contentVersion in a local state file, so it never matches templates by name and never silently overwrites a template someone changed in Vetnio. It needs requests and the VETNIO_API_KEY environment variable.
import json
import os
import pathlib
import requests
BASE = "https://vetnio.com/api/v2"
WORKSPACE = "demo-north-clinic"
STATE_FILE = pathlib.Path("vetnio-templates.json")
TEMPLATES = {
"Lameness work-up": {
"type": "clinical_note",
"template": "<p><strong>History</strong></p><p>(Summarize the presenting complaint as discussed. If absent, write \"Not recorded\".)</p>"
"<p><strong>Examination</strong></p><p>(Include only findings explicitly recorded in this consultation.)</p>"
"<p><strong>Plan</strong></p><p>(List only the treatments and follow-up actually discussed, one bullet per action.)</p>",
},
}
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['VETNIO_API_KEY']}"
templates_url = f"{BASE}/workspaces/{WORKSPACE}/templates"
state = json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else {}
def update(template_id, body, expected_version=None):
payload = {"template": body}
if expected_version:
payload["expectedContentVersion"] = expected_version
response = session.put(f"{templates_url}/{template_id}", json=payload, timeout=30)
if response.status_code == 409:
return None
response.raise_for_status()
return response.json()["contentVersion"]
visible_ids = {item["id"] for item in session.get(templates_url, timeout=30).json()}
for name, spec in TEMPLATES.items():
known = state.get(name)
if known and known["id"] in visible_ids:
version = update(known["id"], spec["template"], known.get("contentVersion"))
if version is None:
print(f"{name}: changed in Vetnio since the last sync; skipped for review")
continue
known["contentVersion"] = version
print(f"{name}: updated")
else:
response = session.post(templates_url, json={"name": name, **spec}, timeout=30)
response.raise_for_status()
template_id = response.json()["templateId"]
# Create returns no version tag; an immediate update of the same body provides one.
state[name] = {"id": template_id, "contentVersion": update(template_id, spec["template"])}
print(f"{name}: created {template_id}")
STATE_FILE.write_text(json.dumps(state, indent=2))Next steps
- Templates: visibility rules, built-in templates and how a template is chosen.
- Records: generate a clinical note with your template and read the result.
- Export configuration: read every template body and its history as a clinic admin.
- Errors: the error format shared by every endpoint.
Endpoints used in this guide
- GET
/workspaces/{workspaceId}/templatesList templates - POST
/workspaces/{workspaceId}/templatesCreate a template - PUT
/workspaces/{workspaceId}/templates/{templateId}Update template content - POST
/workspaces/{workspaceId}/templates/{templateId}/duplicateDuplicate a template - DELETE
/workspaces/{workspaceId}/templates/{templateId}Delete a template - GET
/workspaces/{workspaceId}/data-export/templates/versionsList retained versions of a workspace note template - GET
/workspaces/{workspaceId}/data-export/templates/versionGet a retained version of a workspace note template - POST
/workspaces/{workspaceId}/consultationsCreate a new consultation - POST
/workspaces/{workspaceId}/consultations/{consultationId}/clinical-note/generateGenerate a clinical note
Last reviewed against the API on 2026-10-26.