Skip to main content
Everything TORTUS produces flows back to your application through events. You subscribe with client.on(event, callback), which returns an unsubscribe function.
const unsubscribe = client.on('consultation:started', ({ consultation }) => {
  console.log('Started:', consultation.reference);
});

unsubscribe(); // stop listening

Events

EventWhen it fires
consultation:startedA consultation has started
consultation:resumedAn in-progress consultation was re-opened (e.g. after client restart)
consultation:completedA consultation completed successfully
consultation:errorA consultation failed with an error
consultation:cancelledA consultation was cancelled
consultation:closedA consultation was closed
EventWhen it fires
meeting:startedA meeting has started
meeting:resumedAn in-progress meeting was re-opened
meeting:updatedMeeting metadata (notes/audio) was updated
meeting:completedA meeting completed successfully (suppressed when disableFinishMeetingButton is true)
meeting:cancelledA meeting was cancelled
meeting:closedA meeting was closed
For completion events, the payload separates metadata from output:
  • consultation (or meeting): all the metadata, including reference, mode, patient, customFields, and integration.
  • result: the generated artifacts only.

Completing a consultation

When a consultation completes you must call finish() to acknowledge receipt. Optionally pass a status to report whether your processing succeeded.
client.on('consultation:completed', async ({ result, finish }) => {
  try {
    await processConsultation(result);
    await finish({ status: 'success' }); // default if no parameter is provided
  } catch (error) {
    await finish({ status: 'failed' }); // TORTUS shows retry options to the user
  }
});
StatusMeaning
{ status: 'success' }Processing succeeded. Default when no parameter is given.
{ status: 'failed' }Processing failed; the user can retry from the TORTUS UI.
Calling finish() with no arguments is equivalent to finish({ status: 'success' }):
await finish(); // these are
await finish({ status: 'success' }); // equivalent

Reading artifacts

result.artifacts holds the structured medical output. Each artifact type is an array, so always filter by contentType rather than relying on array order.
const { medicalCodes, medicalNotes, letters, transcriptions } = result.artifacts;

const htmlNote = medicalNotes.find((n) => n.contentType === 'text/html');
const markdownNote = medicalNotes.find((n) => n.contentType === 'text/markdown');
const letter = letters.find((l) => l.contentType === 'text/html');
const transcription = transcriptions.find((t) => t.contentType === 'text/plain');

Medical codes

medicalCodes includes diagnoses and observations. Observational codes carry a measurement, and every code carries an evidence array (which may be empty).
medicalCodes.forEach((code) => {
  console.log(code.code); // e.g. 'I10' or '72313002'
  console.log(code.display); // e.g. 'Essential hypertension'

  if (code.value && code.unit) {
    console.log(code.value, code.unit); // e.g. '120 mmHg'
  }

  if (code.evidence.length > 0) {
    console.log(code.evidence); // e.g. ['Patient reported elevated BP']
  }
});

Other lifecycle events

client.on('consultation:error', ({ consultation, error }) => {
  console.error(`Consultation ${consultation.reference} failed:`, error);
});

client.on('consultation:resumed', ({ consultation }) => {
  console.log('Resumed:', consultation.reference, consultation.patient);
});

Putting it together

A complete integration that loads the client, subscribes, then starts a consultation on demand:
import { loadTortus } from '@tortus-ai/embed-client';

async function setupTortusClient() {
  const client = await loadTortus({
    publishableKey: 'pk_live_...',
    container: '#tortus-container',
    environment: 'production',
    fetchClientSecrets: async () => {
      const response = await fetch('/api/tortus/client-secrets');
      const data = await response.json();
      return { launchToken: data.launchToken };
    },
  });

  client.on('consultation:completed', async ({ consultation, result, finish }) => {
    try {
      await processConsultation(consultation, result);
      await finish({ status: 'success' });
    } catch (error) {
      await finish({ status: 'failed' });
    }
  });

  client.on('consultation:error', ({ consultation, error }) => {
    // show a notification, log, etc.
  });

  return client;
}

const client = await setupTortusClient();

document.getElementById('start-consultation').addEventListener('click', async () => {
  await client.consultations.start(
    { mode: 'FACE_TO_FACE', patient: await getPatientData(), integration: { system: 'EMIS' } },
    { returnTo: 'home' }
  );
});

async function processConsultation(consultation, result) {
  const { medicalNotes, letters, medicalCodes, transcriptions } = result.artifacts;

  const note =
    medicalNotes.find((n) => n.contentType === 'text/html') ??
    medicalNotes.find((n) => n.contentType === 'text/markdown');
  const letter =
    letters.find((l) => l.contentType === 'text/html') ??
    letters.find((l) => l.contentType === 'text/markdown');
  const transcription = transcriptions.find((t) => t.contentType === 'text/plain');

  await saveToEhr({
    reference: consultation.reference,
    note: note?.content,
    letter: letter?.content,
    codes: medicalCodes,
    transcription: transcription?.content,
  });
}

Cleanup

Unsubscribe from individual listeners, close a specific consultation, or destroy the whole client.
// Remove a single listener
const unsubscribe = client.on('consultation:completed', handleConsultationComplete);
unsubscribe();

// Close a specific consultation
await consultation.close();

// Destroy the entire client (cleans up all resources)
await client.destroy();
destroy() cleans up all resources but won’t remove historical data saved in the browser.