Workflows
Work with Link and Vettigo
Look up owners and animals in a clinic's Link or Vettigo practice system and write Vetnio clinical notes into it. Only for clinics that have connected that integration.
Vetnio has built-in integrations with two practice-management systems: Link (linkjournal.se), used by clinics in Sweden, and Vettigo, used in Denmark and Norway. The endpoints in this guide let your software use those integrations on a vet's behalf: look up owners and animals in the practice system, and write the clinical note of a Vetnio consultation into the animal's record there.
Link and Vettigo at a glance
| Link | Vettigo | |
|---|---|---|
| Connected by | Each vet, under Settings > Integrations > Link, with the clinic's Link subdomain and an API key | Each vet, by opening Vetnio from inside Vettigo once |
| Scope | Per user and per workspace | Per user, the same in every workspace: the Vettigo clinic of the user's latest launch |
| Status check | GET/workspaces/{workspaceId}/link/status | GET/workspaces/{workspaceId}/vettigo/status |
| Lookups | Form templates, customers, animals, journal entries | Clients and their animals |
| What an export writes | The consultation's structured clinical note, field by field, into a Link journal entry | The text you send, as a new note in the animal's medical record |
| Repeat an export | Updates the same entry and appends only new text | Adds another note every time |
Before you start
- The personal API key of the vet whose integration connection you use. Every call runs with that vet's own Link credentials or Vettigo account, and a colleague's key may see a different result. Organization and clinic API keys get
403; see Authentication. - A consultation with a transcribed recording; see Document a visit end to end.
The Link and Vettigo responses below are examples that follow the API schema; IDs, names and fields in a real clinic differ. Calls that reach Link or Vettigo take longer than other endpoints; Limits has the time limits.
Link
A Link journal entry is created from a form template that defines its fields. Vetnio mirrors the template as a structured template (a JSON template) so that the clinical note is generated with one value per Link field. The export then writes those values into the entry.
1. Check the Link connection
GET/workspaces/{workspaceId}/link/status reports whether the calling vet has saved Link credentials in this workspace. It never calls Link:
curl https://vetnio.com/api/v2/workspaces/demo-north-clinic/link/status \
-H "Authorization: Bearer $VETNIO_API_KEY"{
"configured": false
}When the vet has connected Link, the response carries the tenant and the default location for new entries:
{
"configured": true,
"subdomain": "riverside",
"defaultLocationId": 346,
"updatedAt": "2026-09-30T08:15:00.000Z"
}While configured is false, every other Link endpoint returns 412 with Link integration is not configured for your account.
2. Pick a Link template
GET/workspaces/{workspaceId}/link/templates returns the tenant's public templates in one unpaginated array. Request kind=animal_journal: the default list also includes herd templates, which cannot be exported to.
curl "https://vetnio.com/api/v2/workspaces/demo-north-clinic/link/templates?kind=animal_journal" \
-H "Authorization: Bearer $VETNIO_API_KEY"[
{ "id": 3423, "name": "Allmän undersökning hund", "description": null, "kind": "animal_journal", "status": "public" }
]GET/workspaces/{workspaceId}/link/templates/{templateId} shows the fields Vetnio can write, in template order. Each has a type: textarea (free text), select (one of options), multiCheckbox (several of options) or internalSample (a result row). An empty fieldMetadata means the template cannot be exported to.
3. Bind the template to the consultation
POST/workspaces/{workspaceId}/link/templates/{linkTemplateId}/resolve finds or creates the matching JSON template and, with consultationId, makes it the consultation's clinical-note template:
curl -X POST https://vetnio.com/api/v2/workspaces/demo-north-clinic/link/templates/3423/resolve \
-H "Authorization: Bearer $VETNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "consultationId": "62a25f84-3c4f-4402-b82a-d8f7e6fd9dee" }'{
"jsonTemplateId": "0f6c2d7e-3b1a-4c55-9f0e-2a8b7c6d5e4f",
"created": false,
"keys": ["anamnes", "allmaentillstand", "status", "bedoemning"]
}The call is idempotent. The first call for a template is slower, because an AI step writes instructions for each field. The consultationId is not checked, so confirm the binding with GET/workspaces/{workspaceId}/consultations/{consultationId}: its clinicalNoteJsonTemplateId must equal jsonTemplateId.
4. Generate the structured clinical note
Call POST/workspaces/{workspaceId}/consultations/{consultationId}/clinical-note/generate with {}. For a consultation with a JSON template, generation runs in the background and the call answers { "message": "Structured document generation initiated" }. Poll GET/workspaces/{workspaceId}/consultations/{consultationId} every few seconds until isGeneratingClinicalNote is false. The note is then a JSON object with one value per key, for example { "anamnes": "…", "allmaentillstand": "ua", … }. Structured documents covers this step in detail.
5. Find the customer and the animal
GET/workspaces/{workspaceId}/link/customers searches active customers. Use exactly one search mode per request:
| Mode | Parameters | Matching |
|---|---|---|
| Identifier | identifier | Exact match on the customer's identifier, typically a Swedish personal identity number. Vetnio also tries the spellings with and without century digits and hyphen. |
| Name fields | firstName and/or lastName | Case-insensitive substrings; at least one needs two or more characters. |
| Name | name | Link's partial match on first or last name. |
curl "https://vetnio.com/api/v2/workspaces/demo-north-clinic/link/customers?identifier=19850101-1234" \
-H "Authorization: Bearer $VETNIO_API_KEY"{
"customers": [
{
"id": 54967922,
"name": "Alex Morgan",
"firstName": "Alex",
"lastName": "Morgan",
"identifier": "198501011234",
"primaryEmail": "alex.morgan@example.com",
"primaryPhone": "0701234567"
}
],
"total": 1,
"sourceTotal": 1,
"page": 1,
"perPage": 100,
"scannedCount": 1,
"hasNextPage": false
}Results are paged with page and perPage (up to 100). The totals count Link's rows before Vetnio's own filtering, so in name-field mode a page can be empty while hasNextPage is true: keep reading pages until hasNextPage is false.
List the customer's animals with GET/workspaces/{workspaceId}/link/customers/{customerId}/animals/page, optionally filtered by name. Prefer it over GET/workspaces/{workspaceId}/link/customers/{customerId}/animals, which returns at most 100 animals without paging information. Only active animals are returned.
curl "https://vetnio.com/api/v2/workspaces/demo-north-clinic/link/customers/54967922/animals/page?name=Bel" \
-H "Authorization: Bearer $VETNIO_API_KEY"{
"animals": [
{ "id": 1234567, "name": "Bella", "kindName": "Hund", "breedName": "Labrador retriever", "genderName": "Tik", "birthdate": "2020-03-15", "deceased": false }
],
"total": 1,
"page": 1,
"perPage": 100,
"hasNextPage": false
}6. Export the note
POST/workspaces/{workspaceId}/consultations/{consultationId}/link/export reads the consultation's stored structured note (the request carries no note text) and writes it into Link. What it does depends on the body:
| Situation | Body | Result |
|---|---|---|
| First export | customerId, animalId, optional locationId (defaults to defaultLocationId) | Creates an entry; mode: created |
| Later exports | {} | Updates the entry recorded on the consultation; mode: updated |
| Write into an entry made in Link | targetEntryId, customerId, animalId | Updates that entry; mode: updated |
| Different animal | reassign: true, customerId, animalId | Creates a new entry and leaves the old one in Link |
GET/workspaces/{workspaceId}/consultations/{consultationId}/link/compatible-entries returns up to three recent entries in the animal's journal that can take this note, for example one the reception already opened for the visit. Pass a candidate's id as targetEntryId.
curl -X POST https://vetnio.com/api/v2/workspaces/demo-north-clinic/consultations/62a25f84-3c4f-4402-b82a-d8f7e6fd9dee/link/export \
-H "Authorization: Bearer $VETNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "customerId": 54967922, "animalId": 1234567 }'{
"linkEntryId": 8812346,
"linkJournalId": 2167548,
"entryUrl": "https://riverside.linkjournal.se/customers/54967922/animals/1234567?only_entry=8812346",
"mode": "created",
"fieldWrites": {
"create": { "applied": [98765, 98766, 98767, 98768], "skipped": [], "errors": [] }
}
}How fields are written:
- Text fields are append-only when updating. Vetnio compares the note with what it exported last time and appends only the new lines, after a blank line. Text the vet typed in Link is kept, and a retry does not duplicate text. Edited lines arrive as new lines; deleted lines are not removed from Link.
- Choice and result fields are overwritten. Fields missing from the note are not touched.
- Check
fieldWrites.create.errorseven on200. When an entry is created, fields Link rejects are only reported there. When an entry is updated, any rejection fails the request with500. - Locked, deleted or cancelled entries cannot be updated (
400).
The consultation stores linkEntryId and linkJournalId, and GET/workspaces/{workspaceId}/consultations/{consultationId}/link/entry-url returns the entry's address in Link at any time. The export does not set the consultation's "exported" mark in the Vetnio app.
Retrying a Link export
An update is safe to retry. A first export is not: if it times out or fails, the entry may exist in Link already. Read the consultation first. If linkEntryId is set, the export completed and a retry with {} updates it. If not, list the compatible entries and pass the new entry as targetEntryId instead of creating another.
Vettigo
1. Check the Vettigo connection
GET/workspaces/{workspaceId}/vettigo/status never fails; any problem is reported as not connected:
curl https://vetnio.com/api/v2/workspaces/demo-north-clinic/vettigo/status \
-H "Authorization: Bearer $VETNIO_API_KEY"{
"connected": false,
"ehrIntegrationName": null
}A connected vet gets { "connected": true, "ehrIntegrationName": "vettigo" }. While connected is false, the other Vettigo endpoints return 401 with Authentication failed - Vettigo session has expired, which in practice means the vet has never opened Vetnio from Vettigo.
2. Look up clients and animals
GET/workspaces/{workspaceId}/vettigo/clients searches client names in the vet's Vettigo clinic and returns up to 50 matches. Queries shorter than three characters return an empty list, and commas act as separators in Vettigo. clientNumber is the only way to tell clients with the same name apart:
curl "https://vetnio.com/api/v2/workspaces/demo-north-clinic/vettigo/clients?query=Morgan" \
-H "Authorization: Bearer $VETNIO_API_KEY"{
"clients": [
{ "id": 104552, "name": "Alex Morgan", "clientNumber": 2381 }
]
}GET/workspaces/{workspaceId}/vettigo/clients/{clientId}/patients returns all of the client's animals, including deceased ones, without paging. regNumber is the registration number, not the microchip, and breed can hold a species, depending on the clinic's data:
{
"patients": [
{ "id": 482913, "name": "Bella", "breed": "Labrador retriever", "regNumber": null }
]
}These are lookups only: nothing is created or linked in Vetnio, and the export below does not take an animal ID.
3. Export a clinical note
POST/workspaces/{workspaceId}/consultations/{consultationId}/vettigo/export adds a note to the medical record of the Vettigo animal behind the consultation. That works only for consultations that are already connected to Vettigo: started from Vettigo for an animal or an appointment, or linked to a patient that Vettigo synced to Vetnio. Other consultations fail with 400.
The note text comes from your request, not from the consultation. Send the clinical note as HTML or plain text, or a structured note as a JSON string; each key then becomes a Key name: block. HTML is converted to plain text and characters Vettigo cannot store are replaced. Text that Vetnio highlighted for the vet to confirm (<span data-open-item="…">) is left out, as in GET/workspaces/{workspaceId}/consultations/{consultationId}/records.
curl -X POST https://vetnio.com/api/v2/workspaces/demo-north-clinic/consultations/62a25f84-3c4f-4402-b82a-d8f7e6fd9dee/vettigo/export \
-H "Authorization: Bearer $VETNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "clinicalNote": "<p><strong>Anamnesis</strong><br>Limping on the left hind leg for 2 weeks.</p>" }'{
"status": "ok",
"httpStatus": 200,
"animalID": 482913
}animalID is null when the animal was found through the consultation's Vettigo appointment. The note is dated with the time of the export and authored by the vet's Vettigo user.
Common errors
| Status | Message | Cause | Fix |
|---|---|---|---|
412 | Link integration is not configured for your account. | The vet has no Link credentials in this workspace. | The vet connects Link in Vetnio's integration settings. |
401 | Authentication failed - Vettigo session has expired | The vet has no Vettigo connection. | The vet opens Vetnio from Vettigo once. |
401 | Link's message | Link rejected the stored API key. | The vet updates the key in Vetnio. |
400 | Input validation failed | No or several Link customer search modes. | Send exactly one mode. |
400 | Consultation has no Link template attached. | The consultation has no Link JSON template. | Run step 3, then generate the note. |
400 | No structured clinical note found for this consultation. … | The note was not generated. | Run step 4. |
400 | The selected Link entry is no longer editable. | The entry is locked, deleted or cancelled. | Export with reassign: true or another targetEntryId. |
404 | No Link template with a matching field signature was found … | The Link template was changed or deleted, or is not an animal journal template. | Resolve a current template and generate the note again. |
400 | Invalid Vettigo request | The consultation is not connected to Vettigo, the note is empty, or the input is invalid. | Check the consultation and the note. |
404 | Vettigo resource not found | Unknown consultation, or Vettigo does not know the record. | Check the IDs. |
429 | Vettigo API rate limit exceeded | Too many Vettigo requests. | Back off and retry lookups. |
500 | An unexpected error occurred | Link or Vettigo failed, or Link rejected a field during an update. | Retry Link updates; check Vettigo before retrying an export. |
See Errors for the error format.
Complete example
This Python script binds a Link template, generates the structured note and exports it to the owner's animal.
import os
import time
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']}"
def call(method, path, **kwargs):
response = session.request(method, BASE + path, **kwargs)
response.raise_for_status()
return response.json()
def export_to_link(consultation_id, link_template_id, owner_identifier, animal_name):
if not call("GET", "/link/status")["configured"]:
raise RuntimeError("This vet has not connected Link in Vetnio")
resolved = call("POST", f"/link/templates/{link_template_id}/resolve",
json={"consultationId": consultation_id})
consultation = call("GET", f"/consultations/{consultation_id}")
assert consultation["clinicalNoteJsonTemplateId"] == resolved["jsonTemplateId"]
call("POST", f"/consultations/{consultation_id}/clinical-note/generate", json={})
while call("GET", f"/consultations/{consultation_id}")["isGeneratingClinicalNote"]:
time.sleep(3)
customers = call("GET", "/link/customers", params={"identifier": owner_identifier})["customers"]
if len(customers) != 1:
raise RuntimeError(f"Expected one Link customer, found {len(customers)}")
customer_id = customers[0]["id"]
animals = call("GET", f"/link/customers/{customer_id}/animals/page",
params={"name": animal_name})["animals"]
if len(animals) != 1:
raise RuntimeError(f"Expected one animal named {animal_name}, found {len(animals)}")
result = call("POST", f"/consultations/{consultation_id}/link/export",
json={"customerId": customer_id, "animalId": animals[0]["id"]})
if result["fieldWrites"]["create"]["errors"]:
print("Link rejected fields:", result["fieldWrites"]["create"]["errors"])
return result["entryUrl"]
print(export_to_link("62a25f84-3c4f-4402-b82a-d8f7e6fd9dee", 3423, "19850101-1234", "Bella"))Next steps
- Structured documents: how JSON templates shape the clinical note.
- Document a visit end to end: record and transcribe the consultation first.
- Sync patients from a practice system: for practice systems without a built-in integration.
- API reference: Link and Vettigo.
Endpoints used in this guide
- GET
/workspaces/{workspaceId}/link/statusLink integration status - GET
/workspaces/{workspaceId}/link/templatesList Link form templates - GET
/workspaces/{workspaceId}/link/templates/{templateId}Get Link template detail - POST
/workspaces/{workspaceId}/link/templates/{linkTemplateId}/resolveResolve Link template to JSON template id - GET
/workspaces/{workspaceId}/link/customersSearch Link customers - GET
/workspaces/{workspaceId}/link/customers/{customerId}/animalsList animals for a Link customer - GET
/workspaces/{workspaceId}/link/customers/{customerId}/animals/pageList a page of animals for a Link customer - GET
/workspaces/{workspaceId}/consultations/{consultationId}/link/compatible-entriesList compatible Link entries - POST
/workspaces/{workspaceId}/consultations/{consultationId}/link/exportExport consultation to Link - GET
/workspaces/{workspaceId}/consultations/{consultationId}/link/entry-urlGet Link entry URL - GET
/workspaces/{workspaceId}/vettigo/statusVettigo integration status - GET
/workspaces/{workspaceId}/vettigo/clientsSearch Vettigo clients - GET
/workspaces/{workspaceId}/vettigo/clients/{clientId}/patientsList a Vettigo client's patients - POST
/workspaces/{workspaceId}/consultations/{consultationId}/vettigo/exportExport clinical note to Vettigo - POST
/workspaces/{workspaceId}/consultations/{consultationId}/clinical-note/generateGenerate a clinical note - GET
/workspaces/{workspaceId}/consultations/{consultationId}Get consultation details
Last reviewed against the API on 2026-10-13.