Get started
Errors
The error format, what every status code means and how to fix it, policy reason tokens, and which requests are safe to retry.
When a request fails, Vetnio answers with an HTTP status code and a JSON body that says what went wrong. Branch on the status and the code, show or log the message, and use the retry rules at the end of this guide to decide whether to try again.
The error object
Every error from a Vetnio endpoint has the same shape. This one comes from renaming a consultation that was deleted:
{
"message": "You cannot modify this consultation",
"code": "FORBIDDEN",
"data": {
"code": "FORBIDDEN",
"httpStatus": 403,
"path": "consultations.rename",
"zodError": null,
"reason": "archived_consultation"
}
}| Field | Meaning |
|---|---|
message | A human-readable explanation in English. Log it; do not parse it, because the wording can change. |
code | The error code, such as FORBIDDEN or NOT_FOUND. It always matches the HTTP status (table below). |
data.code | Same value as code. |
data.httpStatus | The HTTP status code, repeated in the body. |
data.path | Vetnio's internal name for the operation that failed. Useful in a support request; not a stable identifier. Absent when no operation matched the URL. |
data.zodError | For validation errors, the problems grouped by field (fieldErrors) and for the whole input (formErrors). null otherwise. |
data.reason | Present on some refusals: a fixed snake_case token that says why. See Reason tokens. |
issues | Present on validation errors only: one entry per problem. See Validation errors. |
Responses are always JSON with Content-Type: application/json, including errors. Two exceptions come from the hosting platform rather than from Vetnio and are not in this format: a request that runs longer than 10 minutes (504), and a request body that is too large (413). See Limits.
Status codes
| Status | code | Typical message | Cause | What to do |
|---|---|---|---|---|
400 | BAD_REQUEST | Input validation failed | A parameter or body field is missing, has the wrong type or is out of range. | Read issues, fix the request. |
400 | BAD_REQUEST | For example startDate must be on or before endDate. | The input is well-formed but breaks a rule of the endpoint. | Fix the request as the message says. |
400 | PARSE_ERROR | Failed to parse request body | The body is not valid JSON, or is empty. | Send valid JSON; send {} when there is nothing to send. |
401 | UNAUTHORIZED | This action requires an authenticated user | The key is missing, mistyped, deleted, revoked or expired, or the admin who created an organization key lost their role. | Check the Authorization header and the key in Vetnio. See Authentication. |
403 | FORBIDDEN | This API key cannot access this endpoint | An organization or clinic key was used on an endpoint it cannot call. | Use a personal key, or one of the four endpoints these keys can call. |
403 | FORBIDDEN | You do not have access to this workspace | The user is not a member of the clinic, or the clinic does not exist. | Check the workspaceId with GET /workspaces. |
403 | FORBIDDEN | You are not an admin of this workspace | The endpoint needs a clinic admin. | Use a key of an admin. |
403 | FORBIDDEN | You are not a member of this organization, You need admin access to perform this action | Wrong organizationId, or the user is not an organization admin. | Use the organizationId from GET /api-key or an admin's key. |
403 | FORBIDDEN | You cannot modify this consultation | A write to a consultation that cannot be changed; see data.reason. | Do not retry. |
403 | FORBIDDEN | ipAccessDenied | The clinic or organization only accepts requests from listed IP addresses. | Call from an allowed address. |
403 | FORBIDDEN | The vetsGPT module is not enabled | VetsGPT is not enabled for the clinic or the user, or the user is not a member of the clinic; see data.reason. | Ask a clinic admin to enable it. See Use VetsGPT. |
404 | NOT_FOUND | Not found | No endpoint matches the URL and method. A wrong method, such as POST on a GET endpoint, also returns this. | Check the path and method in the reference. |
404 | NOT_FOUND | For example Consultation not found, File not found | The resource does not exist, belongs to another clinic, or the user cannot see it. | Check the ID. |
409 | CONFLICT | For example File has already been uploaded | The request clashes with the current state: a duplicate, or a concurrent change. | See Retrying safely. |
412 | PRECONDITION_FAILED | For example The file upload is not complete. Wait for the upload to finish and try again. | A step the endpoint depends on has not finished. | Finish that step, then retry. |
415 | UNSUPPORTED_MEDIA_TYPE | Missing content-type header | A POST, PUT or PATCH without Content-Type: application/json. | Send the header and a JSON body, {} if empty. |
429 | TOO_MANY_REQUESTS | Rate limit of 60 requests per minute reached. Retry in 53 seconds. | An organization or clinic key exceeded its rate limit. | Wait as the message says. See Limits. |
500 | INTERNAL_SERVER_ERROR | An unexpected error occurred | Something failed inside Vetnio. | See Unexpected errors. |
Endpoints that talk to a practice system can also pass that system's failures through, for example 401 when the Vettigo session has expired or 429 when Vettigo itself rate limits. Practice systems covers those.
Validation errors
When the input does not match the endpoint's schema, the response is 400 with message Input validation failed. issues lists every problem, and path in each issue names the field. Here page and pageSize are out of range on GET/workspaces/{workspaceId}/patients:
curl "https://vetnio.com/api/v2/workspaces/demo-north-clinic/patients?page=0&pageSize=1000" \
-H "Authorization: Bearer $VETNIO_API_KEY"{
"message": "Input validation failed",
"code": "BAD_REQUEST",
"data": {
"code": "BAD_REQUEST",
"httpStatus": 400,
"path": "patientsAndClients.listPatientsV1",
"zodError": {
"formErrors": [],
"fieldErrors": {
"page": ["Too small: expected number to be >=1"],
"pageSize": ["Too big: expected number to be <=100"]
}
}
},
"issues": [
{
"origin": "number",
"code": "too_small",
"minimum": 1,
"inclusive": true,
"path": ["page"],
"message": "Too small: expected number to be >=1"
},
{
"origin": "number",
"code": "too_big",
"maximum": 100,
"inclusive": true,
"path": ["pageSize"],
"message": "Too big: expected number to be <=100"
}
]
}Each issue has a code (such as invalid_type, too_small, too_big, invalid_value or unrecognized_keys), a path and a message, plus fields specific to the code, such as minimum or the allowed values. To map issues to your own form fields, join path with dots.
A 400 for a business rule, such as an unknown column name in the usage export, has no issues and zodError is null; the message explains the problem.
Reason tokens
Some refusals carry a data.reason token so your code can react without parsing the message. Tokens are lowercase snake_case identifiers that stay the same when the wording of message changes. New tokens can be added, so treat an unknown token like a refusal without a reason.
data.reason | Seen with | Meaning |
|---|---|---|
archived_consultation | 403 on writes | The consultation was deleted. It can still be read, but nothing about it can change. |
hidden_consultation | 403 on writes | The consultation is hidden and cannot be changed. |
missing_consultation | 404 | No consultation with this ID exists in the clinic. |
insufficient_consultation_access | 403 or 404 | The consultation exists, but the user may not read or change it. |
missing_record, missing_recording | 404 | The record or recording inside the consultation does not exist. |
invalid_recording_key | 403 | A recording reference does not belong to this consultation. |
module_not_enabled_for_member | 403 | The feature (currently VetsGPT) is turned off for this user. |
module_not_enabled_for_workspace | 403 | The feature is turned off for the clinic. |
module_not_workspace_member | 403 | The user is not a member of the clinic, for example an organization admin working in a clinic of their organization. |
module_workspace_unavailable | 403 | The clinic is locked, or is being merged or erased. |
Unexpected errors
When something fails inside Vetnio, the response is 500 with the message An unexpected error occurred. The real cause is logged on Vetnio's side and is never sent to you, and stack traces are never included.
A few limits are currently reported this way instead of with a 4xx code, for example a record longer than 50,000 characters or a clinic that already has the maximum number of templates. If a 500 repeats for the same input, check Limits before you retry. Such errors may get a more specific status code in the future, so handle errors by status class rather than by exact code.
When you contact Vetnio support about an error, include the time of the request, the method and path, and the x-vercel-id header from the response. Never include your API key.
Retrying safely
Retry only when the error is temporary and the request is safe to repeat.
| Error | Retry? |
|---|---|
Network error or timeout, 500, 502, 503, 504 | Yes, with exponential backoff (for example 1, 2, 4, 8 seconds, at most 5 attempts), but only for requests that are safe to repeat (table below). |
429 | Yes, after the wait the message states. Every rejected request still counts toward the key's quota, so do not retry in a tight loop. |
409 | Depends on the endpoint (table below). |
412 | Yes, once the step the message names has finished. |
400, 401, 403, 404, 415 | No. Fix the request or the credentials first. |
A request that timed out may still have succeeded on Vetnio's side. Before repeating a request that creates something, check whether the first attempt went through.
| Operation | Safe to repeat? | How |
|---|---|---|
Any GET | Yes | Reads never change data. |
POST/workspaces/{workspaceId}/consultations | With externalId or clientConsultationId | Vetnio returns the existing active consultation with that externalId instead of creating a second one; a deleted consultation no longer matches, so the same externalId then creates a new one. A clientConsultationId (a UUID you generate) returns the consultation the first request created, unless it was archived; 409 means the ID belongs to another consultation. Without either, every call creates a new consultation. |
POST/workspaces/{workspaceId}/vetsgpt/ask | With the same idempotencyKey | Repeating the request with the same key returns the stored answer. 409 VetsGPT answer is still generating means the first request is still running: retry later with the same key. Use a new key for each new question. |
POST/workspaces/{workspaceId}/consultations/{consultationId}/uploaded-files | Deduplicated by sha256 | The same file (same SHA-256) in the same consultation returns 409 File has already been uploaded. List the consultation's files and reuse the existing one; if it never finished uploading, delete it and start again. |
POST/workspaces/{workspaceId}/consultations/{consultationId}/uploaded-files/{fileName}/finalize | Yes | Has no effect on a file that is already being analyzed or is analyzed. On a failed file it retries the analysis. |
POST/workspaces/{workspaceId}/consultations/{consultationId}/transcriptions/start | Yes | Starts work only for recordings that are not yet transcribed. |
POST/workspaces/{workspaceId}/consultations/{consultationId}/recording-chunks, POST/workspaces/{workspaceId}/consultations/{consultationId}/recording-operations | Yes | Repeating a chunk returns a fresh upload URL, or alreadyUploaded: true. Repeating a submission with the same operationId and payload returns the same receipt; a different payload under that ID returns 409. |
PUT/workspaces/{workspaceId}/consultations/{consultationId}/records, rename, additional info, external ID | Yes | They replace the stored value, so repeating them gives the same result. |
PUT/workspaces/{workspaceId}/templates/{templateId} | Yes | Send expectedContentVersion to get 409 instead of overwriting someone else's newer edit. |
POST/workspaces/{workspaceId}/consultations/{consultationId}/clinical-note/generate, structured documents, AI-written records | No | Every call generates again and saves a new version. After a timeout, check the record and its versions before retrying. |
| Creating patients, clients, workspaces and VetsGPT chats | No | Every call creates a new object. Look it up before retrying. |
DELETE/workspaces/{workspaceId}/consultations/{consultationId} and other deletes | Yes, with care | A repeated delete can return an error even though the first one worked: deleting a consultation twice returns 403 with archived_consultation, deleting a patient twice returns 404. Treat these as success. |
| Exports to practice systems | No | Each call writes again in the practice system. Check there before retrying. |
Next steps
- Limits: rate limits, timeouts and size limits behind
429,413,504and some500errors. - Pagination: how to page through lists without missing or repeating items.
- Authentication: the key types behind
401and403.
Endpoints used in this guide
- PATCH
/workspaces/{workspaceId}/consultations/{consultationId}Rename a consultation - POST
/workspaces/{workspaceId}/consultationsCreate a new consultation - DELETE
/workspaces/{workspaceId}/consultations/{consultationId}Archive a consultation - GET
/workspaces/{workspaceId}/patientsList patients - POST
/workspaces/{workspaceId}/consultations/{consultationId}/uploaded-filesInitialize a file upload - POST
/workspaces/{workspaceId}/consultations/{consultationId}/uploaded-files/{fileName}/finalizeFinalize a file upload and trigger analysis - POST
/workspaces/{workspaceId}/vetsgpt/askSend a message to VetsGPT - PUT
/workspaces/{workspaceId}/consultations/{consultationId}/recordsUpdate a record - POST
/workspaces/{workspaceId}/consultations/{consultationId}/clinical-note/generateGenerate a clinical note - POST
/workspaces/{workspaceId}/consultations/{consultationId}/transcriptions/startStart transcription - PUT
/workspaces/{workspaceId}/templates/{templateId}Update template content - POST
/workspaces/{workspaceId}/consultations/{consultationId}/recording-chunksPrepare an immutable recording chunk upload - POST
/workspaces/{workspaceId}/consultations/{consultationId}/recording-operationsDurably accept an idempotent recording submission
Last reviewed against the API on 2026-10-26.