Workflows
Generate structured documents
Turn a recorded consultation into a JSON object that follows a structured template, by response or by callback, and handle the cases where nothing comes back.
Use this workflow when your system needs the visit as separate fields rather than as free text, for example to fill the History, Examination and Treatment fields of a practice-management journal form. You send a consultation and a structured template; Vetnio returns a JSON object with exactly the template's fields, filled from what was said.
A structured template (a JSON template in the API) is a JSON Schema that names the output fields and their types. Clinics build them in Vetnio's Structured Templates area; the API can list and read them but not create them. A structured template works on its own: you do not need to bind it to an ordinary template to generate a structured document. Templates explains how the two kinds relate.
Before you start
- A personal API key (see Authentication) for a user who can edit the consultation.
- A consultation with at least one recording uploaded. Document a visit covers creating the consultation and uploading audio.
- If you want the result by callback: an HTTPS endpoint that Vetnio can reach and that answers within 30 seconds.
Two endpoints produce structured output. This guide covers the first:
| Structured document | Structured clinical note | |
|---|---|---|
| Endpoint | POST/workspaces/{workspaceId}/consultations/{consultationId}/documents | POST/workspaces/{workspaceId}/consultations/{consultationId}/clinical-note/generate |
| Template | Any structured template, passed per request | The structured template linked to the consultation's clinical-note template |
| Uses | The transcript and the consultation's additional info | Also uploaded files, the template's instructions, and the clinic-wide and personal AI instructions |
| Stored as | A separate document, not the clinical note | The consultation's clinical note, with version history |
| Readable later through the API | No | Yes, with GET/workspaces/{workspaceId}/consultations/{consultationId}/records |
1. List structured templates
GET/workspaces/{workspaceId}/json-templates returns every structured template in the clinic, followed by Vetnio's two built-in ones, which exist in every clinic:
curl https://vetnio.com/api/v2/workspaces/demo-north-clinic/json-templates \
-H "Authorization: Bearer $VETNIO_API_KEY"[
{
"id": "2a859133-0e04-4f63-88d2-aa0ee318cd8d",
"name": "Soap Note for Dog",
"description": "A template for a soap note for a dog."
},
{
"id": "3891bd52-61d4-41c6-89d0-49dab63d0a39",
"name": "Hälta",
"description": "Hältaundersökning för häst."
}
]The clinic's own templates come first, in no guaranteed order, and have no description. Names are not unique, so store the id you choose.
2. Inspect the fields
GET/workspaces/{workspaceId}/json-templates/{jsonTemplateId} returns the JSON Schema. Its properties are the keys your document will contain, so use it to map fields to your system:
curl https://vetnio.com/api/v2/workspaces/demo-north-clinic/json-templates/2a859133-0e04-4f63-88d2-aa0ee318cd8d \
-H "Authorization: Bearer $VETNIO_API_KEY"{
"id": "2a859133-0e04-4f63-88d2-aa0ee318cd8d",
"name": "Soap Note for Dog",
"jsonTemplateSchema": {
"type": "object",
"title": "Canine Medical Report",
"properties": {
"subjectiveInformation": {
"type": "object",
"properties": {
"reasonForVisit": { "type": ["string", "null"] },
"symptoms": { "type": ["string", "null"] },
"pastIllnesses": { "type": ["string", "null"] },
"medications": { "type": ["string", "null"] }
},
"required": ["reasonForVisit", "symptoms", "pastIllnesses", "medications"],
"additionalProperties": false
},
"objectiveInformation": {
"type": "object",
"properties": {
"generalCondition": { "type": ["string", "null"] },
"vitalSigns": {
"type": "object",
"properties": {
"temperature": { "type": ["number", "null"] },
"pulse": { "type": ["number", "null"] },
"respiratoryRate": { "type": ["number", "null"] }
},
"required": ["temperature", "pulse", "respiratoryRate"],
"additionalProperties": false
},
"physicalExamination": { "type": ["string", "null"] },
"laboratoryResults": { "type": ["string", "null"] }
},
"required": ["generalCondition", "vitalSigns", "physicalExamination", "laboratoryResults"],
"additionalProperties": false
},
"assessment": {
"type": "object",
"properties": {
"diagnosis": { "type": ["string", "null"] },
"differentialDiagnoses": { "type": "array", "items": { "type": ["string", "null"] } },
"prognosis": { "type": ["string", "null"] }
},
"required": ["diagnosis", "differentialDiagnoses", "prognosis"],
"additionalProperties": false
},
"plan": {
"type": "object",
"properties": {
"treatment": { "type": ["string", "null"] },
"followUp": { "type": ["string", "null"] },
"ownerInformation": { "type": ["string", "null"] }
},
"required": ["treatment", "followUp", "ownerInformation"],
"additionalProperties": false
}
},
"required": ["subjectiveInformation", "objectiveInformation", "assessment", "plan"],
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false
},
"createdAt": "1970-01-01T00:00:00.000Z"
}Templates built in Vetnio use string, number, integer, boolean, array and object properties, with enum for single- and multiple-choice sections and a description that tells the AI what to extract. Fields listed in required are always present in the output; treat the others as optional. A field the conversation did not cover is left empty (for example null, when the schema allows it) rather than invented.
3. Check that the consultation is ready
Generation starts by transcribing anything still waiting and then waits for every recording to finish, but that wait counts against the request's 10-minute limit. Check first with GET/workspaces/{workspaceId}/consultations/{consultationId}. The consultation in this example holds a short lameness consultation that was added as text with POST/workspaces/{workspaceId}/consultations/{consultationId}/partial-transcriptions:
curl https://vetnio.com/api/v2/workspaces/demo-north-clinic/consultations/f6a37af3-aef7-4483-8ed3-a29b2cab45be \
-H "Authorization: Bearer $VETNIO_API_KEY"{
"id": "f6a37af3-aef7-4483-8ed3-a29b2cab45be",
"name": "Bella - lameness recheck (structured)",
"createdAt": "2026-10-04T16:25:05.513Z",
"recordingDurationInMilliseconds": 0,
"recordingDurationInMillisecondsExcludingSilence": 0,
"isGeneratingClinicalNote": false,
"isGeneratingCommunication": false,
"isReadyForGeneration": true,
"clinicalNoteJsonTemplateId": null,
"linkEntryId": null,
"linkJournalId": null,
"transcriptionStatus": "idle",
"transcriptionFailureReason": null
}Proceed when isReadyForGeneration is true: no recording is waiting for transcription and the consultation has something to work from. With audio, that coincides with transcriptionStatus becoming completed (or failed, in which case the transcript holds what could be transcribed). Text added as a partial transcription leaves the status at idle, as here. A consultation whose only content is an uploaded file is ready for a clinical note but cannot produce a structured document, which needs a transcript.
4. Request the document
Call POST/workspaces/{workspaceId}/consultations/{consultationId}/documents with the template ID. With callbackUrl, the request returns at once and the result is POSTed to your URL. Put a secret token in the URL, because callbacks are not signed:
curl -X POST https://vetnio.com/api/v2/workspaces/demo-north-clinic/consultations/f6a37af3-aef7-4483-8ed3-a29b2cab45be/documents \
-H "Authorization: Bearer $VETNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonTemplateId": "2a859133-0e04-4f63-88d2-aa0ee318cd8d",
"languageCode": "en",
"callbackUrl": "https://pms.example.com/webhooks/vetnio/document?token=9f2c7e"
}'{
"message": "Callback URL has been queued"
}languageCode sets the language the values are written in; the conversation can be in another language. It is a recording-language code, mostly ISO 639-1 (en, sv, de, no) plus a few three-letter codes such as yue. Without it, Vetnio uses the consultation's recording language. An unknown jsonTemplateId returns 404 JSON template not found before anything is queued.
Without callbackUrl the request is synchronous: it blocks until the document is ready and returns it as the response body, in the same shape as the callback below. Set your HTTP client timeout to at least 600 seconds.
5. Receive the callback
Vetnio sends one POST with a JSON body and an X-Vetnio-Callback-Status header. On success the body is the document itself. Generation needs AI, so this document is illustrative: it shows what the template above produces from this consultation, with fields that were not discussed left as null:
POST /webhooks/vetnio/document?token=9f2c7e HTTP/1.1
Content-Type: application/json
X-Vetnio-Callback-Status: success
{
"subjectiveInformation": {
"reasonForVisit": "Left hind lameness for about two weeks.",
"symptoms": "Limping on the left hind, mostly after walks; still eating normally.",
"pastIllnesses": null,
"medications": null
},
"objectiveInformation": {
"generalCondition": null,
"vitalSigns": { "temperature": 38.6, "pulse": 96, "respiratoryRate": null },
"physicalExamination": "Pain on extension of the left knee; no swelling.",
"laboratoryResults": null
},
"assessment": {
"diagnosis": "Suspected early cruciate ligament disease, left hind.",
"differentialDiagnoses": [],
"prognosis": null
},
"plan": {
"treatment": "Carprofen 2 mg/kg twice daily for 7 days; short lead walks only.",
"followUp": "Recheck in 2 weeks; radiographs if not improved.",
"ownerInformation": null
}
}On error the body names the problem and the consultation. This callback is real; it was sent for the request in step 4 when the AI model could not be reached:
POST /webhooks/vetnio/document?token=9f2c7e HTTP/1.1
Content-Type: application/json
X-Vetnio-Callback-Status: error
{
"error": {
"code": "INTERNAL_SERVER_ERROR",
"message": "Error generating object"
},
"workspaceId": "demo-north-clinic",
"consultationId": "f6a37af3-aef7-4483-8ed3-a29b2cab45be"
}The error messages you can receive:
error.code | error.message | Meaning |
|---|---|---|
BAD_REQUEST | No transcription available for document generation. Please ensure there are recordings and they have been transcribed. | The consultation has no transcript. |
INTERNAL_SERVER_ERROR | Error during transcription process. | The transcript could not be read. |
INTERNAL_SERVER_ERROR | Error generating object | Both the main and the backup model failed. |
INTERNAL_SERVER_ERROR | Generation failed | Any other failure, for example while saving. |
Your receiver must check the token, answer with a 2xx status within 30 seconds, and do the slow work afterwards:
import express from "express";
import crypto from "node:crypto";
const app = express();
app.use(express.json({ limit: "1mb" }));
const expected = Buffer.from(process.env.VETNIO_CALLBACK_TOKEN);
app.post("/webhooks/vetnio/document", (req, res) => {
const token = Buffer.from(String(req.query.token ?? ""));
if (token.length !== expected.length || !crypto.timingSafeEqual(token, expected)) {
return res.sendStatus(401);
}
res.sendStatus(204); // acknowledge first; there is no retry
const status = req.get("X-Vetnio-Callback-Status");
queue.push({ status, body: req.body, receivedAt: Date.now() }); // your own job queue
});
app.listen(8080);6. Store the result
There is no endpoint that reads a structured document back. Vetnio keeps the latest one per consultation for its own use, separately from the clinical note: it does not change what GET/workspaces/{workspaceId}/consultations/{consultationId}/records returns and has no version history. Save the response or the callback body in your own system as soon as it arrives.
Calling again generates a new document from scratch and replaces Vetnio's stored copy. Each call runs the AI model again, so it is not idempotent.
Know when it is finished
| Mode | Finished when | If nothing arrives |
|---|---|---|
| Synchronous | The response arrives | A 504 or a client timeout after 600 seconds means the job was cut off; nothing was returned |
| Callback | Your endpoint receives the POST | No callback 11 minutes after the request means it was lost; request again |
Structured documents usually take well under a minute once transcription is complete. The transcription wait comes on top, which is why step 3 matters.
Common errors
| Status | message | Cause | Fix |
|---|---|---|---|
400 | Input validation failed | Missing jsonTemplateId, a callbackUrl that is not an absolute URL, or an unknown languageCode. issues lists each field. | Fix the request body. |
400 | No transcription available for document generation. … | The consultation has no transcript (synchronous mode). | Upload and transcribe a recording first. |
403 | You cannot modify this consultation | data.reason is archived_consultation, or the user can only view the consultation. | Use an active consultation and a user with write access. |
403 | This API key cannot access this endpoint | An organization or clinic export key. | Use a personal API key. |
404 | JSON template not found | The template ID is not in this clinic and is not built in. | List templates again (step 1). |
404 | Consultation not found | Wrong consultation or clinic ID, or no access. | Check both IDs. |
415 | Missing content-type header | No JSON body. | Send Content-Type: application/json and a body. |
500 | An unexpected error occurred | Generation failed on both the main and the backup model, or the transcript could not be read. In callback mode the callback names the cause (step 5). | Retry once after a short wait. |
504 | — | The 10-minute limit was reached; the body is not a Vetnio error object. | Wait until the consultation is ready before calling, or use a callback. |
A structured clinical note instead
This is optional. Only use it if clinicians should see the structured result in Vetnio, edit it and keep its version history:
- Create a Clinical Note template with the structured template's ID as
jsonTemplateId(POST/workspaces/{workspaceId}/templates). This needs a structured template created in the clinic; the built-in ones cannot be linked, and the link cannot be changed later. - Create the consultation with that template as
clinicalNoteTemplateId(POST/workspaces/{workspaceId}/consultations). The consultation'sclinicalNoteJsonTemplateIdis then set. - Call POST
/workspaces/{workspaceId}/consultations/{consultationId}/clinical-note/generate. It answers{ "message": "Structured document generation initiated" }. - Poll GET
/workspaces/{workspaceId}/consultations/{consultationId}every 2–5 seconds untilisGeneratingClinicalNoteisfalse, then read the note with GET/workspaces/{workspaceId}/consultations/{consultationId}/records. It is a JSON object with every field of the template, and string values can contain inline HTML such as<strong>and<br />. The flag also clears when generation fails, so check that the note actually changed.
Records covers generation, polling and version history in detail.
End-to-end script
This Python script waits until the consultation is ready, generates a document synchronously and saves it. It needs requests and the VETNIO_API_KEY environment variable.
import json
import os
import time
import requests
BASE = "https://vetnio.com/api/v2"
WORKSPACE = "demo-north-clinic"
CONSULTATION = "f6a37af3-aef7-4483-8ed3-a29b2cab45be"
TEMPLATE_NAME = "Soap Note for Dog"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['VETNIO_API_KEY']}"
def find_template(name):
response = session.get(f"{BASE}/workspaces/{WORKSPACE}/json-templates", timeout=30)
response.raise_for_status()
matches = [t for t in response.json() if t["name"] == name]
if not matches:
raise SystemExit(f"No structured template named {name!r}")
return matches[0]["id"]
def wait_until_ready(max_wait_seconds=900):
deadline = time.monotonic() + max_wait_seconds
while time.monotonic() < deadline:
response = session.get(f"{BASE}/workspaces/{WORKSPACE}/consultations/{CONSULTATION}", timeout=30)
response.raise_for_status()
consultation = response.json()
if consultation["isReadyForGeneration"]:
return consultation["transcriptionStatus"]
if consultation["transcriptionStatus"] == "failed":
raise SystemExit(f"Transcription failed: {consultation['transcriptionFailureReason']}")
time.sleep(5)
raise SystemExit("The consultation did not become ready in time")
def generate(template_id):
url = f"{BASE}/workspaces/{WORKSPACE}/consultations/{CONSULTATION}/documents"
body = {"jsonTemplateId": template_id, "languageCode": "en"}
for attempt in (1, 2):
try:
response = session.post(url, json=body, timeout=620)
except requests.Timeout:
response = None
if response is not None and response.ok:
return response.json()
if response is not None and response.status_code < 500:
raise SystemExit(f"{response.status_code}: {response.json()['message']}")
if attempt == 1:
time.sleep(30) # one retry for 5xx and timeouts; each call runs the model again
raise SystemExit("Generation failed twice")
template_id = find_template(TEMPLATE_NAME)
print("Transcription status:", wait_until_ready())
document = generate(template_id)
with open(f"{CONSULTATION}.json", "w", encoding="utf-8") as output:
json.dump(document, output, ensure_ascii=False, indent=2)
print("Saved fields:", ", ".join(document))Next steps
- Document a visit: create the consultation and upload the recording this workflow starts from.
- Records: generate, read and version clinical notes, including structured ones.
- Templates: how structured templates relate to ordinary templates.
- Errors and Limits: the conventions every endpoint follows.
Endpoints used in this guide
- GET
/workspaces/{workspaceId}/json-templatesList JSON templates - GET
/workspaces/{workspaceId}/json-templates/{jsonTemplateId}Get JSON template detail - GET
/workspaces/{workspaceId}/consultations/{consultationId}Get consultation details - POST
/workspaces/{workspaceId}/consultations/{consultationId}/documentsGenerate a structured document - POST
/workspaces/{workspaceId}/consultations/{consultationId}/clinical-note/generateGenerate a clinical note - GET
/workspaces/{workspaceId}/consultations/{consultationId}/recordsGet a record - POST
/workspaces/{workspaceId}/consultationsCreate a new consultation - POST
/workspaces/{workspaceId}/templatesCreate a template - POST
/workspaces/{workspaceId}/consultations/{consultationId}/partial-transcriptionsCreate partial transcription
Last reviewed against the API on 2026-10-30.