# Visit Messages API Landing: https://developers.vsee.io/api/visit-messages-api · Spec JSON: https://developers.vsee.io/specs/visit_messages_api_openapi.json Base URL: https://api.vseepreview.com Asynchronous messaging on an eConsult visit: patients send and read messages through the API while providers reply from the VSee Clinic dashboard. Every new message fires a `visit.message.created` webhook so an integrator's backend can notify its users without polling. Authorization is participant-scoped — a user can only read or write messages on visits they are part of (patient, assigned provider, or host). ## Webhooks This API emits one event. Its payload, delivery contract and handling steps are documented on its own page — see **`visit.message.created`** in the sidebar. | Event | Fires when | |-------|------------| | `visit.message.created` | Any new message is added to a visit thread, in either direction | ## GET /api/visits/{visit_id}/messages URL: https://developers.vsee.io/api/visit-messages-api/get/api/visits/%7Bvisit_id%7D/messages operationId: getVisitMessages Read the conversation Returns the visit's message thread. The list is append-only and chronological (oldest first by default). **Polling for new replies** — because the thread is append-only and chronological, request `?skip=`; the response contains only what's new, and `total_count` tells you when you're caught up. Polling is only needed as a fallback: subscribe to the `visit.message.created` webhook instead and fetch the thread when it fires. Parameters: - X-ApiToken (header, string, required) — API token — the same token returned by the SSO endpoint (`data.token.token`) - visit_id (path, string, required) — ID of the visit whose thread is being read - limit (query, integer, optional) — Page size. Default 25, maximum 200. [default: 25] - skip (query, integer, optional) — Offset into the list. Default 0. [default: 0] - page (query, integer, optional) — Alternative to `skip`: skip = (page − 1) × limit. - sort (query, string, optional) — Sort field. Use `sort=created&order=desc` (or the compact `sort=created:desc`) to return latest first. Default is oldest first. - order (query, string, optional) — Sort direction, used with `sort`. [enum: asc, desc] Responses: - 200 — Message thread retrieved successfully ```json { "data": [ { "id": "b505f15c-8b98-43ea-a8e8-e29188b5f757", "type": "patient_message", "actor": { "id": "1000004436", "type": 200, "display_name": "An VSee" }, "content": "Thank you doctor — what did you find?", "created": 1786745961.166 } ], "total_count": 7 } ``` - 400 — Missing token ```json { "code": 400, "message": "Missing token" } ``` - 401 — Invalid or expired token ```json { "code": 401, "message": "Invalid token" } ``` - 403 — The authenticated user is not a participant on this visit ```json { "code": 403, "message": "Not a participant on this visit" } ``` - 404 — Visit not found ```json { "code": 404, "message": "Visit not found" } ``` ## POST /api/visits/{visit_id}/messages URL: https://developers.vsee.io/api/visit-messages-api/post/api/visits/%7Bvisit_id%7D/messages operationId: createVisitMessage Send a patient message Posts a message to the visit thread as the authenticated user. The message appears in the provider's Visit Chat in real time. Attachments reuse the existing `POST /files` upload flow — pass the returned file ids in `attachment_ids`. A closed eConsult returns `409`: history stays readable, but a new concern should open a new eConsult. A successful post fires a `visit.message.created` webhook with `data.actor_type` set to the sender — see the webhook section on the API overview page. Parameters: - X-ApiToken (header, string, required) — API token — the same token returned by the SSO endpoint (`data.token.token`) - visit_id (path, string, required) — ID of the visit to post the message to Request body (application/json): - content (string, required) — Message text (UTF-8). Required, must be a non-empty string. - attachment_ids (array[string], optional) — Optional file ids from the `POST /files` upload flow Example request: ```json { "content": "Hi doctor, my molar still aches when I bite down.", "attachment_ids": [] } ``` Responses: - 201 — Message created successfully ```json { "data": { "id": "83c3b3d7-346c-4ac9-8276-f96ac8356e25", "type": "patient_message", "actor": { "id": "1000004436", "type": 200, "display_name": "An VSee" }, "content": "Hi doctor, my molar still aches when I bite down.", "created": 1786755119.482 } } ``` - 400 — Missing token, or `content` missing/empty ```json { "code": 400, "message": "content is required" } ``` - 401 — Invalid or expired token ```json { "code": 401, "message": "Invalid token" } ``` - 403 — The authenticated user is not a participant on this visit ```json { "code": 403, "message": "Not a participant on this visit" } ``` - 404 — Visit not found ```json { "code": 404, "message": "Visit not found" } ``` - 409 — Visit is closed for new messages (history remains readable) ```json { "code": 409, "message": "Visit is closed for new messages" } ``` ## WEBHOOK visit.message.created URL: https://developers.vsee.io/api/visit-messages-api/webhook/visit.message.created API: Visit Messages API — https://developers.vsee.io/api/visit-messages-api Direction: inbound — delivered to your configured webhook URL, not called by you. New message on the visit thread Fires for **every** new message on the visit, in either direction — patient messages sent through this API and provider replies sent from the VSee Clinic dashboard. Filter on `data.actor_type` if you only want one side. The payload is a thin pointer: it deliberately carries **no message content and no PHI**. Treat it as a signal to fetch, then read the thread with `GET /api/visits/{visit_id}/messages`. ## Delivery Configure the destination URL per clinic account in Admin settings. There is no signature header — restrict your endpoint by network or use an unguessable path. | Behaviour | Contract | |-----------|----------| | Acknowledgement | Return any `2xx` within 30 seconds | | Retries | Up to 5 attempts, roughly 60 seconds apart | | De-duplication | Retries repeat a delivery — de-duplicate on `data.message_id` | | Ordering | Not guaranteed; use `data.ts` to order | | Latency | Seconds for messages sent through this API, up to ~2 minutes for provider replies | ## Handling 1. Acknowledge immediately — do the work after responding, not before. 2. Skip the event if you have already seen its `data.message_id`. 3. Call `GET /api/visits/{visit_id}/messages?skip=` to pull only what is new. Payload (application/json): - id (string, required) — Unique id for this delivery. Changes on each retry — do not de-duplicate on it. - type (string, required) — Event name [one of: visit.message.created] - created (integer, required) — UNIX timestamp when the event was emitted - account_code (string, required) — Clinic account the visit belongs to - version (string, required) — Payload version - data (object, required) — Pointer to the new message. Carries no message content or PHI. - data.visit_id (string, required) — Visit whose message thread changed - data.message_id (string, required) — The new message. De-duplicate on this — retries repeat a delivery. - data.actor_type (string, required) — Who sent the message [one of: patient, provider] - data.ts (integer, required) — UNIX timestamp when the message was created Example delivery: ```json { "id": "6a47ea02-4809-4af6-b8f6-c56bc2bd2adb", "type": "visit.message.created", "created": 1786755119, "account_code": "aubrey", "version": "1.0", "data": { "visit_id": "1000021648", "message_id": "83c3b3d7-346c-4ac9-8276-f96ac8356e25", "actor_type": "provider", "ts": 1786755119 } } ```