VetFlash API

A realtime veterinary scribe. One WebSocket: audio in β†’ live transcript out β†’ clinical note out.

Overview

The API is a single WebSocket. A client streams microphone audio and receives live transcription from a real-time speech-to-text engine. The client supplies a typed prompt (the note template as free text); when the session stops, the transcript + prompt are sent to a language model and a structured clinical note is streamed back. A thin HTTP surface handles login and readiness.

Base URL

https://api.vetflash.io  Β·  WebSocket: wss://api.vetflash.io

Authentication

Exchange an email + password for a short-lived sessionToken. The realtime socket only ever consumes that token β€” never raw credentials.

POST /v3/auth/session

// Request
{ "email": "vet@clinic.com", "password": "β€’β€’β€’β€’β€’β€’β€’β€’" }

// 200 Response
{ "sessionToken": "v3.…", "expiresAt": "2026-…Z", "scribeCredits": 100 }

// 401 β€” wrong password, unknown email, or disabled account (indistinguishable)
{ "error": "invalid email or password" }

Connecting to the scribe

WS /v3/scribe/stream β€” pass the token one of two ways:

A bad or expired token is rejected with HTTP 401 before the socket opens. A disallowed browser Origin β†’ 403. An optional ?language= sets the transcription language (the start frame's language still wins).

Client β†’ Server frames

FrameEncodingShape & notes
startJSON text{ "type":"start", "prompt":"…", "language?":"en-GB", "mimetype?":"audio/webm" }
Must arrive before audio. prompt required, ≀ 8000 chars.
audiobinaryRaw Opus/WebM (from MediaRecorder) or linear16 PCM β€” forwarded to the speech engine as-is.
stopJSON text{ "type":"stop" } β€” ends transcription, charges credits, triggers the note.
cancelJSON text{ "type":"cancel" } β€” abort note generation / tear down.

Server β†’ Client frames

FrameShape
ready{ "type":"ready", "sessionId":"…", "model":"vetflash-scribe", "scribeCredits":100 }
transcript{ "type":"transcript", "text":"…", "isFinal":true|false }
note (delta){ "type":"note", "delta":"…", "done":false }
note (final){ "type":"note", "text":"…", "transcript":"…", "done":true, "creditsCharged":1, "scribeCreditsRemaining":99 }
error{ "type":"error", "code":"…", "message":"…" }
Transcripts: isFinal:false are interim (they update in place for live display); isFinal:true segments are committed and are what the note is built from.

Message sequence

Client                              Server
  β”‚  POST /v3/auth/session   ─────▢   { sessionToken }
  β”‚  WS connect ?sessionToken ────▢
  β”‚  {type:start, prompt}     ────▢   pre-check credits β†’ open transcription
  β”‚                           ◀────   {type:ready, scribeCredits}
  β”‚  Β«binary audioΒ»           ────▢   forward to speech engine
  β”‚                           ◀────   {type:transcript, isFinal:false}   interim
  β”‚                           ◀────   {type:transcript, isFinal:true}    committed
  β”‚  {type:stop}              ────▢   flush β†’ charge credits β†’ LLM
  β”‚                           ◀────   {type:note, delta} Γ— N
  β”‚                           ◀────   {type:note, done:true, text, transcript, creditsCharged}

Full browser example

const API = "https://api.vetflash.io";

const { sessionToken } = await fetch(`${API}/v3/auth/session`, {
  method: "POST", headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email, password }),
}).then(r => r.json());

const ws = new WebSocket(`${API.replace(/^http/,"ws")}/v3/scribe/stream?sessionToken=${sessionToken}`);

ws.onopen = async () => {
  ws.send(JSON.stringify({ type: "start", prompt: "Write a SOAP note.", language: "en-GB" }));
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  const rec = new MediaRecorder(stream, { mimeType: "audio/webm" });
  rec.ondataavailable = e => { if (ws.readyState === 1) ws.send(e.data); };
  rec.start(250);                        // emit a chunk every 250ms
  stopBtn.onclick = () => { rec.stop(); ws.send(JSON.stringify({ type: "stop" })); };
};

ws.onmessage = ev => {
  const m = JSON.parse(ev.data);
  if (m.type === "ready")      console.log("credits:", m.scribeCredits);
  if (m.type === "transcript") renderTranscript(m.text, m.isFinal);
  if (m.type === "note" && !m.done) appendNote(m.delta);
  if (m.type === "note" && m.done)  finishNote(m.text, m.creditsCharged, m.scribeCreditsRemaining);
  if (m.type === "error")      console.error(m.code, m.message);
};

Languages

Set language per session in the start frame (or ?language= on the URL). Default en-GB.

Credits

Error codes

codeMeaning
invalid_frameControl frame wasn't valid JSON / failed validation. Session stays open.
protocol_errorFrame sent in the wrong state (e.g. stop with no active session).
insufficient_creditsNo credits at start, or the session hit your credit boundary.
inactive_accountThe account was disabled after the token was issued.
session_in_progressThe account already has an active session. Connection stays open β€” retry after it ends.
audio_limit / session_limitSession exceeded the byte cap / the 45-minute cap.
no_speechstop produced an empty transcript; no note generated.
stt_error / note_failedThe speech engine signalled an error / the note model call failed.

HTTP endpoints

MethodPathPurpose
POST/v3/auth/sessionExchange credentials for a sessionToken.
GET/v3Self-describing API metadata (JSON).
GET/healthReadiness (also reports the active storage driver).
WS/v3/scribe/streamThe realtime scribe pipeline.
Ready to try it? Open the live scribe demo.