# Authentication
Source: https://docs.tortus.ai/authentication
Mint short-lived launch tokens on your backend to authenticate the SDK.
The SDK authenticates each session with a short-lived **launch token**. You mint this token on your **secure backend** and hand it to the client through the [`fetchClientSecrets`](/configuration) callback.
Never expose your client ID or client secret to the frontend, and never mint launch tokens in the
browser. Always retrieve them from a backend you control.
## How it fits together
The SDK calls your `fetchClientSecrets` function, which hits a route on your own server.
Authenticated with your client ID and secret, your server requests a launch token.
The SDK exchanges the token to load and authenticate the embedded app.
## The launch endpoint
Authenticate with **HTTP Basic Authentication** using your TORTUS-provided credentials:
| Part | Value |
| - | - |
| Username | Your client ID (e.g. `cli_abc123xyz`) |
| Password | Your client secret (e.g. `sec_def456uvw`) |
Base64-encode `clientId:clientSecret` and send it in the `Authorization` header.
```bash Production theme={null}
POST https://api.tortus.ai/v1/oauth/launch
```
```bash Sandbox theme={null}
POST https://api.staging.tortus.ai/v1/oauth/launch
```
### Request body
All fields are optional. Send what's relevant to your integration.
To help identify and resume the correct user, send their first and last name on every launch. After their first launch, also send the returned userId. Include any other fields relevant to your integration.
If you don't send `userId` for someone who has launched before, TORTUS creates a **new user**
every time. Store the `user_id` you get back and resend it on every launch. See [Persisting and
resuming users](#persisting-and-resuming-users).
Your own internal user ID, kept as a reference label on the TORTUS user. This is **not** a resume
or lookup key: sending it does not match an existing user. Only `userId` resumes a user.
User information to associate with this session.
User's email address.
User's first name.
User's last name.
External organisation identifier (e.g. ODS code, facility ID).
External organisation name, for display purposes.
Custom metadata, such as department or role.
### Response
If the user doesn't already exist, TORTUS creates one and returns it in the response. A newly created user only becomes valid once the launch token has been exchanged.
```json theme={null}
{
"launch_token": "AbCdEfGhIjKlMnOp.QrStUvWxYzAbCdEfGhIjKlMnOpQrStUvWxYz",
"user_id": "tortus|a1b2c3d4e5f6g7h8",
"expires_in": 300
}
```
The token to pass to the SDK. Valid for **5 minutes**.
The TORTUS user ID for this session. Save it to resume the session later via `userId`.
Seconds until the token expires.
## Persisting and resuming users
Each launch either resumes an existing TORTUS user or creates a new one. Which one you get depends entirely on whether you send `userId`.
The rule is simple:
1. **First launch for a person:** call the endpoint without `userId` (optionally with `externalUserId` and `userPayload`). Then use `user_id` from the response and save it against that person in your own system.
2. **Every later launch for the same person:** send that saved value as `userId` to resume them.
Skip step 1's save, or forget to send `userId` in step 2, and you create a duplicate user on every login.
```mermaid theme={null}
sequenceDiagram
participant B as Your backend
participant T as TORTUS launch endpoint
Note over B,T: First launch for a person
B->>T: POST /v1/oauth/launch (no userId)
T-->>B: launch_token + user_id
B->>B: Save user_id against this person
Note over B,T: Every later launch
B->>T: POST /v1/oauth/launch (userId = saved user_id)
T-->>B: launch_token (same user resumed)
```
Here is the same loop in your launch route:
```ts theme={null}
// server-side route, e.g. GET /api/tortus/client-secrets
const credentials = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64');
// Look up the TORTUS user ID you saved for this person, if any.
const savedUserId = await getTortusUserId(currentUser.id);
const response = await fetch('https://api.tortus.ai/v1/oauth/launch', {
method: 'POST',
headers: {
Authorization: `Basic ${credentials}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
// Resume the existing user when you have their ID.
...(savedUserId ? { userId: savedUserId } : {}),
externalUserId: currentUser.id,
userPayload: { email: currentUser.email },
}),
});
const { launch_token, user_id } = await response.json();
// First time round, persist the returned ID so future launches resume this user.
if (!savedUserId) {
await saveTortusUserId(currentUser.id, user_id);
}
// Return { launchToken: launch_token } to your frontend.
```
## Wiring it into the SDK
Return the token from `fetchClientSecrets`. The SDK calls this whenever it needs fresh credentials.
```ts theme={null}
const client = await loadTortus({
publishableKey: 'pk_your_key_here',
container: '#tortus-container',
environment: 'production',
fetchClientSecrets: async () => {
const response = await fetch('/api/tortus/client-secrets');
const data = await response.json();
return { launchToken: data.launchToken };
},
});
```
Because tokens are short-lived, have your backend mint a fresh one on each call rather than
caching it client-side.
# Changelog
Source: https://docs.tortus.ai/changelog
### Added
* **Deleted sections in `structuredNotes`**: A section in `structuredNotes` can now have `deleted: true`, which means the clinician deleted it. A deleted section keeps its `id`, `sectionType`, and `heading`, and its `content` is empty. It does not show in `medicalNotes`. Use the flag to tell a removed section from an empty one. The new optional `deleted` field is on `StructuredNoteSection`. Older SDK versions get a deleted section as a section with empty content.
* **`disableSectionDelete` config option** on `loadTortus({ config })`: when `true`, clinicians cannot delete sections of a structured note. Use it when your mapping needs every section of the template. Defaults to `false`.
### Added
* **`structuredNotes` on `consultation:completed`**: The `result.artifacts` object may now include `structuredNotes`, the medical note in section-shaped form (`templateId` plus an ordered list of sections, each with `id`, `sectionType`, `heading`, and `content`). Key your mapping on the section `id`: ids are stable for a given template, so treat them as opaque strings. `heading` is always present, including for sections the rendered note shows without a visible heading. When present, the content matches `medicalNotes`. The field can be absent for any consultation (for example, the clinician edited the note as free text, or the feature is not enabled for your organisation), so always handle absence. New `StructuredNote` and `StructuredNoteSection` types are exported.
### Added
* **Meeting mode support (`tortus.meetings.start(...)`)**: Start a meeting recording via the SDK. Two modes are supported:
* `LIVE_RECORDING` captures audio in the embedded app (matches the standalone meeting flow). Optional `notes` and `title` may be supplied as the meeting starts.
* `AUDIO_FILE` bypasses the recorder by providing `audio.url` (intended for hosts that already have audio captured or handle audio capturing themselves). Transcription begins immediately on the supplied audio.
* **`TortusMeeting` object**: Returned by `meetings.start(...)` with `setAudio?(audio)`, `setNotes?(notes)`, and `close()` for mid-flight updates and tear-down.
* **Meeting lifecycle events** on the SDK client: `meeting:started`, `meeting:resumed`, `meeting:updated`, `meeting:closed`, `meeting:cancelled`, and `meeting:completed`. The `meeting:completed` payload includes the generated meeting note in both Markdown and HTML and the transcription as plain text.
* **`disableFinishMeetingButton` config option** on `loadTortus({ config })`: when `true`, the embedded meeting view hides the post-generation "Finish meeting" button and exposes the bundled meetings list / "Start new meeting" UI instead. The `meeting:completed` event is not emitted while this flag is set, leaving the host integrator in charge of the meeting's lifecycle UI.
### Deprecated
* **Dictation API**: `client.dictations.start()`, the `TortusDictation` / `DictationOptions` types, and the `dictation:started` / `dictation:closed` events are deprecated. The host application has never implemented the iframe-side handler for these calls; they always reject with `CLIENT_4001: Dictation not yet implemented`. References have been removed from the documentation, and the API surface itself will be removed in a future release. Do not use these in new integrations.
### Added
* **`consultation:resumed` event**: Fired when an existing in-progress consultation is re-opened (e.g., after a client unmount/remount or when the clinician navigates back to the consultation). Provides a `TortusConsultation` object with full consultation data, enabling integrators to track consultation lifecycle across application restarts.
* **`completedAt` field on `TortusConsultationData`**: ISO 8601 timestamp indicating when the consultation was completed. Present in `consultation:completed` event payloads.
* **`createdAt` field in `consultation:completed`**: The consultation's original creation timestamp is now reliably included, even when the client was restarted mid-consultation.
### Fixed
* **Reliable consultation data after client restart**: `consultation:completed` now always returns a fully populated `TortusConsultation` object when a consultation is reopened after a client unmount/remount. Previously, restarting the client mid-consultation could result in incomplete consultation data.
### Added
* **Transcription artifacts in `consultation:completed`**: The `result.artifacts` object now includes a `transcriptions` array containing the consultation transcription as plain text (`contentType: 'text/plain'`). This follows the same array pattern as `medicalNotes` and `letters` for future expansion.
### Added
* **`CUSTOM` integration system**: You can now pass `{ system: 'CUSTOM', label: 'Your System' }` as the integration to display a custom label in the "Approve & Save to ..." button. The `label` field is required when `system` is `'CUSTOM'`. The "EHR Connected" badge is not shown for custom integrations.
* **Optional `label` for known integration systems**: All integration systems (`EMIS`, `SYSTM_ONE`, `MEDICUS`, `CERNER`) now accept an optional `label` field that overrides the "Approve & Save to ..." button text. When omitted, the default system display name is used. The "EHR Connected" badge always shows the system's default name.
* **Branded error screen on initialisation failure**: When `loadTortus()` fails, the loader is replaced with a user-friendly error screen showing the error code for support, instead of silently failing.
* **`EMBED_7007` error code**: New error code for client-secrets fetch failures, making it easier to identify integrator-side issues.
### Changed
* **Increased initialisation timeout**: `APP_READY` timeout increased from 30 s to 120 s, with improved diagnostic messages when handshake times out.
### Added
* **`customFields` support for internal embed client**: Integrator-provided custom fields are now persisted to the encounter record and returned in `consultation:started` and `consultation:completed` events.
* **Custom fields validation**: Only organisations with pre-configured fields may use `customFields`.
* **`EMBED_7006` error code**: New "Invalid embed configuration" error for embed-specific configuration issues (e.g. unconfigured custom fields).
### Fixed
* **`MedicalCode` fields now match FHIR conventions**: Medical codes in `consultation:completed` events now use `code` and `display` as primary fields (aligned with FHIR Coding). The previous `id` and `description` fields are still included for backward compatibility but are deprecated; migrate to `code`/`display` at your convenience.
* **`consultation.close()` reliability**: `consultation:closed` now fires exactly once regardless of whether the close was initiated by the integrator (via `consultation.close()`) or by the clinician within the app. Previously, client-initiated closes could emit the event multiple times.
* **`consultation:closed` payload**: The event now includes full consultation data (via `toJSON()`) instead of just the reference, matching the shape of other lifecycle events.
* **`consultation.close()` always available**: `close()` is now a required method on the `TortusConsultation` interface. Previously it was typed as optional and could appear `undefined`.
* **`consultation:started` event carries live object**: The `consultation:started` event now includes the full `TortusConsultation` instance with `close()` and `toJSON()` methods. Previously, methods were stripped during serialisation.
* **Type resolution for external consumers**: All type declarations are now self-contained; they no longer reference internal monorepo paths that would resolve as `unknown` for integrators.
### Changed
* **New `TortusConsultationData` type**: Consultation data in terminal event payloads (`consultation:closed`, `consultation:cancelled`, `consultation:completed`) is now typed as `TortusConsultationData` (data-only, serialisation-safe) instead of `TortusConsultation`. Use `toJSON()` on the live consultation object to obtain a `TortusConsultationData` snapshot.
* **Unused error types removed**: Removed unused configuration error types (`CONFIG_MISSING_FIELD`, `CONFIG_INVALID_VALUE`, `APPLICATION_CONFIG_CONFLICT`) that were declared but never referenced by any error definition.
### Added
* **`text/html` content type**: Artifact `contentType` now supports `'text/markdown' | 'text/html'`, enabling integrators to receive rich HTML directly from the editor alongside markdown.
### Fixed
* **Incorrect content type on medical notes and letters**: Artifacts were labelled as `text/markdown` but actually contained HTML from the editor. Content type now correctly reflects the actual format (`text/html` for HTML content, `text/markdown` for markdown-converted content).
* **Consultation serialisation**: `consultation` objects in event callbacks (`consultation:started`, `consultation:completed`, etc.) are now plain data; they no longer throw "The object cannot be cloned" when using `structuredClone()` or `postMessage()`.
* **`finish()` status handling**: `finish({ status: 'failed' })` and `finish({ status: 'success' })` now work correctly. Failed status shows retry UI, success confirms the save (previously ignored and always closed).
### Changed
* **Default event adapter**: The PostMessage adapter is now the default for all consumers. The legacy X-On adapter remains available via explicit opt-in but is no longer used by default.
### Added
* **Cerner EHR Integration**: Added `CERNER` as a supported integration system.
* Configure with `integration: { system: 'CERNER' }` when starting consultations.
### Fixed
* Medical codes with measurements now properly include `value`, `unit`, and `evidence` in `consultation:completed` events.
This release removes all deprecated APIs and methods. Applications using deprecated functionality must migrate to the new APIs before upgrading.
### Breaking changes
**Removed deprecated methods**
* **`client.launch(options)`**: Use `client.display('home')` instead.
* **`client.startConsultation(consultation)`**: Use `client.consultations.start(consultation, options)` instead.
**Removed deprecated types**
* **`LaunchOptions`** interface: No longer needed, use the `Screen` type with the `display()` method.
* **`integration.label`** property: Use only the `integration.system` property.
* **`onConsultationComplete`** callback: Use `client.on('consultation:completed', callback)` instead.
* **`onError`** callback: Use `client.on('consultation:error', callback)` instead.
* **`artifacts.transcription`** field: Deprecated field removed from consultation output.
* **`DictationOutput`** type: Removed from public API (dictation completion not yet supported).
* **`dictation:completed`** event: Removed from public API (dictation completion not yet supported).
**Artifacts structure changes**
* **`artifacts.medicalNotes`** changed from `string` to `MedicalNote[]` (array with `content` and `contentType` for multi-format support).
* **`artifacts.letter`** changed to `artifacts.letters` as a `Letter[]` array.
* Currently only `contentType: 'text/markdown'` is supported; the array structure enables future format support (HTML, JSON, etc.).
**Consultation data structure changes**
* **Flattened input structure**: the `encounter` object was removed from `StartConsultationInput`.
* `consultation.encounter.reference` becomes `consultation.reference`
* `consultation.encounter.metadata` becomes `consultation.metadata`
* `consultation.encounter.customFields` becomes `consultation.customFields`
* **Output structure simplification**: metadata removed from `ConsultationOutput`.
* All consultation metadata (reference, customFields, metadata, integration, patient, mode) is available in the `consultation` object in events.
* `ConsultationOutput` now contains only `artifacts` (medicalNotes, letters, medicalCodes).
* **`TortusConsultation` enhanced**:
* Added an `integration` property to match all data previously in `ConsultationOutput`.
* Now serves as the single source for all consultation metadata.
### Added
* Enhanced type safety with removal of deprecated optional properties.
* Cleaner API surface with consistent event-driven architecture.
* Improved documentation without deprecated method references.
* Simplified logging; always uses console when debug is enabled (no custom logger needed).
### Changed
* **Event adapters**: Removed deprecated `launch()` method implementations.
* **Validation**: Streamlined validation schemas without deprecated fields.
* **Tests**: Updated all test suites to use modern API patterns.
* **Documentation**: Removed deprecation notices and updated examples.
* **Error codes**: Consolidated error codes; `CLIENT_4011` (user cancelled) is now `CLIENT_4008` (user cancelled operation).
### Removed
* All `@deprecated` annotations and associated functionality.
* Legacy callback-based error handling patterns.
* Unused imports and type definitions related to deprecated functionality.
* Deprecated test cases and assertions.
* **Custom logger option**: Removed the `logger` parameter from `LoadTortusConfig` (now always uses console when debug is enabled).
### Migration guide
**Method migrations**
```typescript theme={null}
// Before (deprecated)
await client.launch({ mode: 'STANDALONE' });
const consultation = await client.startConsultation({
mode: 'FACE_TO_FACE',
onConsultationComplete: (result, finish) => {
console.log('Completed:', result);
finish();
},
onError: (error) => {
console.error('Error:', error);
}
});
// After (v1.0.0+)
await client.display('home');
const unsubscribe1 = client.on('consultation:completed', ({ result, finish }) => {
console.log('Completed:', result);
finish();
});
const unsubscribe2 = client.on('consultation:error', ({ error }) => {
console.error('Error:', error);
});
const consultation = await client.consultations.start({
mode: 'FACE_TO_FACE'
});
```
**Type migrations**
```typescript theme={null}
// Before (deprecated)
interface MyConsultation extends StartConsultationInput {
integration?: {
system: IntegrationSystem;
label?: string; // Deprecated
};
onConsultationComplete?: (result: ConsultationOutput, finish: () => Promise) => void; // Deprecated
onError?: (error: TortusError) => void; // Deprecated
}
// After (v1.0.0+)
interface MyConsultation extends StartConsultationInput {
integration?: {
system: IntegrationSystem;
};
}
```
**Consultation data access**
```typescript theme={null}
// Before (v0.x with encounter nesting)
const consultation = await client.consultations.start({
mode: 'FACE_TO_FACE',
encounter: {
reference: 'my-ref',
customFields: { myField: 'value' },
metadata: { integrationRef: 'ext-123' }
}
});
console.log('Custom field:', consultation.encounter.customFields.myField);
console.log('Metadata:', consultation.encounter.metadata.integrationRef);
// After (v1.0.0+ with flattened structure)
const consultation = await client.consultations.start({
mode: 'FACE_TO_FACE',
reference: 'my-ref',
customFields: { myField: 'value' },
metadata: { integrationRef: 'ext-123' }
});
console.log('Mode confirmed:', consultation.mode); // NEW: confirms what mode was started
console.log('Custom field:', consultation.customFields.myField);
console.log('Metadata:', consultation.metadata.integrationRef);
```
**Consultation results access**
```typescript theme={null}
// Before (v0.x with metadata in result)
client.on('consultation:completed', ({ result }) => {
console.log('Reference:', result.reference);
console.log('Mode:', result.mode);
console.log('Custom fields:', result.customFields);
console.log('Patient:', result.patient);
console.log('Integration:', result.integration);
console.log('Artifacts:', result.artifacts);
});
// After (v1.0.0+ with metadata in consultation object)
client.on('consultation:completed', ({ consultation, result }) => {
// All metadata is in the consultation object
console.log('Reference:', consultation.reference);
console.log('Mode:', consultation.mode);
console.log('Custom fields:', consultation.customFields);
console.log('Patient:', consultation.patient);
console.log('Integration:', consultation.integration);
// Result now contains only artifacts
console.log('Artifacts:', result.artifacts);
});
```
**Artifact handling**
```typescript theme={null}
// Before (deprecated)
client.on('consultation:completed', ({ result }) => {
console.log('Transcription:', result.artifacts.transcription); // Always empty string
console.log('Notes:', result.artifacts.medicalNotes);
});
// After (v1.0.0+)
client.on('consultation:completed', ({ result }) => {
console.log('Notes:', result.artifacts.medicalNotes);
console.log('Letter:', result.artifacts.letter);
console.log('Codes:', result.artifacts.medicalCodes);
});
```
**Error handling**
```typescript theme={null}
// Before (v0.x)
try {
await consultation.close();
} catch (error) {
if (error.code === 'CLIENT_4011') { // User cancelled
console.log('User cancelled the operation');
}
}
// After (v1.0.0+)
try {
await consultation.close();
} catch (error) {
if (error.code === 'CLIENT_4008') { // User cancelled operation
console.log('User cancelled the operation');
}
// Recommended: Use error type instead of code for better maintainability
if (error.type === 'application_user_cancelled') {
console.log('User cancelled the operation');
}
}
```
**Logger configuration**
```typescript theme={null}
// Before (v0.x) - Custom logger was supported but undocumented
const client = await loadTortus({
publishableKey: 'pk_...',
container: '#tortus-container',
debug: true,
logger: (level, message, data) => {
// Custom logging logic
myLogger.log(`[${level}] ${message}`, data);
}
});
// After (v1.0.0+) - Always uses console when debug is enabled
const client = await loadTortus({
publishableKey: 'pk_...',
container: '#tortus-container',
debug: true // Now always logs to console.log/warn/error
});
```
# How it works
Source: https://docs.tortus.ai/concepts
The mental model behind the embedded TORTUS client.
A few concepts explain almost everything about how the SDK behaves. Once these click, the rest of the API follows naturally.
## The embedded model
The SDK renders TORTUS inside a **sandboxed iframe** in a container you provide. Your application and TORTUS communicate over a secure message channel: you send instructions (start a consultation, navigate a screen) and TORTUS sends events back (started, completed, error).
Your users never leave your application, and patient data is handled inside the protected TORTUS experience.
## Standby mode
The client **always starts in standby mode** after `loadTortus()` resolves. The standby screen waits for instructions until you start a consultation or navigate elsewhere.
```ts theme={null}
const client = await loadTortus({
/* ... */
});
// The client is now showing the standby screen, ready for instructions.
```
You can always return to it with `client.display('standby')`.
## Event-driven architecture
All consultation and meeting handling is **event-based**. You subscribe with `client.on(...)` and react to lifecycle events such as `consultation:started`, `consultation:completed`, and `consultation:error`. Each event carries a `reference` so you can correlate it with your own records.
Completion events hand you a `finish()` callback that you **must call** to acknowledge receipt of the results.
```ts theme={null}
client.on('consultation:completed', async ({ consultation, result, finish }) => {
await handleResult(result);
await finish({ status: 'success' });
});
```
Learn the full lifecycle in [Handling events & results](/guides/events-and-results).
## Screens and navigation
You can move the embedded experience between screens programmatically:
```ts theme={null}
await client.display('home'); // the TORTUS home screen
await client.display('standby'); // back to standby
```
Navigation returns a promise that **rejects if the operation isn't possible** (for example, navigating away mid-consultation).
## Client lifecycle
The client owns the iframe and all its resources. When you're done, for example when unmounting the component that hosts TORTUS, call `destroy()` to clean everything up.
```ts theme={null}
await client.destroy();
```
`destroy()` releases all resources but does **not** remove historical data saved in the browser.
Individual consultations and meetings also expose `close()` for finer-grained teardown.
## Putting it together
How sessions are secured with launch tokens.
Start consultations across every mode.
Subscribe to the lifecycle and read artifacts.
The full method surface.
# Configuration
Source: https://docs.tortus.ai/configuration
Every option the loadTortus() function accepts.
`loadTortus()` initialises the client and immediately displays the standby screen. It accepts a single configuration object.
```ts theme={null}
import { loadTortus } from '@tortus-ai/embed-client';
const client = await loadTortus({
publishableKey: 'pk_your_key_here',
container: '#tortus-container',
environment: 'production',
fetchClientSecrets: async () => ({ launchToken: await getTokenFromBackend() }),
debug: true,
});
```
## Options
Your publishable API key, provided by TORTUS.
DOM selector (or element) for the iframe container.
Which TORTUS environment to connect to. Must be specified explicitly.
Async function that returns secure credentials from your backend. See
[Authentication](/authentication).
Enable debug logging for troubleshooting. Defaults to `false` in production and `true` in
development.
Default EHR integration applied to all consultations. Consultations inherit this unless they
provide their own `integration`. See [EHR integrations](/guides/consultations#ehr-integrations).
When `true`, the embedded meeting view hides the post-generation **Finish meeting** button and
`meeting:completed` is not emitted. The bundled meetings list / **Start new meeting** UI is shown
instead, so your host integration manages the meeting lifecycle. See [Hosting your own meetings
UI](/guides/meetings#hosting-your-own-meetings-ui).
When `true`, the clinician cannot delete a section of a structured note. Use it when your mapping
needs every section of the template. See [Structured notes](/guides/events-and-results#structured-notes).
## The container element
TORTUS renders into the element you point `container` at. Give it an explicit size:
```html theme={null}
```
Set `debug: true` while integrating to surface handshake and event logs in the console, then turn
it off for production.
# Consultations
Source: https://docs.tortus.ai/guides/consultations
Start consultations across audio, face-to-face, and live recording modes.
A consultation is the core unit of work in TORTUS. You start one with `client.consultations.start(consultation, options?)`, choosing a **mode** that matches how the audio is captured. TORTUS then takes over the embedded view and emits events as the consultation progresses.
```ts theme={null}
const consultation = await client.consultations.start(config, options);
console.log('Started:', consultation.reference);
```
The `options` argument is optional and currently supports `returnTo`, the screen to show when the consultation completes:
```ts theme={null}
{
returnTo: 'home';
} // or "standby"
```
## Modes
For in-person consultations recorded live in the embedded app.
```ts theme={null}
const consultation = await client.consultations.start(
{
mode: "FACE_TO_FACE",
patient: { name: "John Doe", dateOfBirth: "1975-08-22" },
integration: { system: "CERNER" },
},
{ returnTo: "standby" },
);
```
Process an existing recording by passing its URL. Transcription begins immediately.
```ts theme={null}
const consultation = await client.consultations.start(
{
mode: "AUDIO_FILE",
audio: { url: "https://your-domain.com/consultation-recording.mp3" },
patient: {
name: "Jane Smith",
dateOfBirth: "1985-03-15",
identifier: { label: "NHS Number", value: "1234567890" },
},
metadata: { consultationId: "cons_123", providerId: "dr_smith" },
integration: { system: "EMIS" },
},
{ returnTo: "home" },
);
```
Capture audio live, optionally setting the audio source during the consultation.
```ts theme={null}
const consultation = await client.consultations.start({
mode: "LIVE_RECORDING",
patient: { name: "Sarah Wilson" },
customFields: { appointmentType: "follow-up", urgency: "routine" },
});
// You can update the audio source mid-consultation
await consultation.setAudio?.({ url: "https://your-domain.com/live-stream-url" });
```
## Patient details
The `patient` object identifies who the consultation is about. `name` is the common field; `dateOfBirth` and a typed `identifier` are optional but recommended.
```ts theme={null}
patient: {
name: "Jane Smith",
dateOfBirth: "1985-03-15",
identifier: { label: "NHS Number", value: "1234567890" },
}
```
You can also attach free-form `metadata` and `customFields` for your own correlation and workflow needs.
## EHR integrations
Set `integration` to tell TORTUS which clinical system the results should be approved and saved to. The chosen system drives the **"Approve & Save to …"** button and the **"EHR Connected"** badge.
| System | Display name | Description |
| - | - | - |
| `EMIS` | EMIS | EMIS Web EHR integration |
| `SYSTM_ONE` | SystmOne | TPP SystmOne EHR integration |
| `MEDICUS` | Medicus | Medicus EHR integration |
| `CERNER` | Cerner | Oracle Cerner EHR integration |
| `CUSTOM` | *(your label)* | Custom integration, requires a `label` field |
Set a default `integration` in [`loadTortus()`](/configuration) and every consultation inherits it
unless it provides its own.
### Custom integration
Use the `CUSTOM` system when your clinical system isn't listed. The `label` you provide is shown in the "Approve & Save to …" button. The "EHR Connected" badge is not shown for custom integrations.
```ts theme={null}
const consultation = await client.consultations.start({
mode: 'AUDIO_FILE',
audio: { url: 'https://your-domain.com/recording.mp3' },
patient: { name: 'Jane Smith' },
integration: {
system: 'CUSTOM',
label: 'Acme Health', // Required: shown in the UI
},
});
```
`label` is **required** when `system` is `'CUSTOM'`.
### Optional label for known systems
You can pass an optional `label` for a known system to customise the "Approve & Save to …" button text only. The "EHR Connected" badge still shows the system's default name.
```ts theme={null}
const consultation = await client.consultations.start({
mode: 'FACE_TO_FACE',
patient: { name: 'John Doe' },
integration: {
system: 'EMIS',
label: 'My GP Surgery', // Optional: overrides button text only
},
});
```
When `label` is omitted, the default system display name is used (e.g. "EMIS", "SystmOne").
## Next step
React to the consultation lifecycle and read the generated artifacts.
# Handling events & results
Source: https://docs.tortus.ai/guides/events-and-results
Subscribe to the consultation lifecycle and read the generated artifacts.
Everything TORTUS produces flows back to your application through **events**. You subscribe with `client.on(event, callback)`, which returns an unsubscribe function.
```ts theme={null}
const unsubscribe = client.on('consultation:started', ({ consultation }) => {
console.log('Started:', consultation.reference);
});
unsubscribe(); // stop listening
```
## Events
| Event | When it fires |
| - | - |
| `consultation:started` | A consultation has started |
| `consultation:resumed` | An in-progress consultation was re-opened (e.g. after client restart) |
| `consultation:completed` | A consultation completed successfully |
| `consultation:error` | A consultation failed with an error |
| `consultation:cancelled` | A consultation was cancelled |
| `consultation:closed` | A consultation was closed |
| Event | When it fires |
| - | - |
| `meeting:started` | A meeting has started |
| `meeting:resumed` | An in-progress meeting was re-opened |
| `meeting:updated` | Meeting metadata (notes/audio) was updated |
| `meeting:completed` | A meeting completed successfully (suppressed when `disableFinishMeetingButton` is `true`) |
| `meeting:cancelled` | A meeting was cancelled |
| `meeting:closed` | A 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.
```ts theme={null}
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
}
});
```
| Status | Meaning |
| - | - |
| `{ 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' })`:
```ts theme={null}
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.
```ts theme={null}
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');
```
### Structured notes
`structuredNotes` is the medical note in section-shaped form, so you can map sections into your own system by id instead of parsing the rendered note. Each structured note carries the `templateId` it was generated from and its `sections` in note order.
```ts theme={null}
result.artifacts.structuredNotes?.forEach((note) => {
console.log(note.templateId);
note.sections.forEach((section) => {
console.log(section.id); // e.g. 'presenting-complaint' — stable per template
console.log(section.heading); // e.g. 'Presenting complaint'
console.log(section.content); // Markdown
});
});
```
* **Key on `id`.** Section ids are stable for a given template, so a mapping you build against a template keeps working across consultations. Treat ids as opaque strings.
* **`heading` is always present**, including for sections the rendered note shows without a visible heading.
* **When present, the content matches `medicalNotes`.** The sections are the same clinician-approved note, not a separate output. The one difference is deleted sections.
* **A deleted section has `deleted: true`.** When the clinician deletes a section, it stays in `sections` with its `id`, `sectionType`, and `heading`, but its `content` is empty. It does not show in `medicalNotes`. Use the flag to tell a removed section from an empty one. To stop clinicians deleting sections, set [`disableSectionDelete`](/configuration).
* **The field can be absent for any consultation, by design** — for example, when the clinician edited the note as free text or the feature is not enabled for your organisation. Always handle absence; fall back to `medicalNotes`, which is present in every completed consultation.
### Medical codes
`medicalCodes` includes diagnoses and observations. Observational codes carry a measurement, and every code carries an `evidence` array (which may be empty).
```ts theme={null}
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
```ts theme={null}
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:
```ts theme={null}
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.
```ts theme={null}
// 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.
# Meetings
Source: https://docs.tortus.ai/guides/meetings
Capture non-patient conversations and generate meeting notes.
Meetings let clinicians (or other users) capture **non-patient conversations** and have TORTUS generate meeting notes. The flow mirrors consultations: start a meeting, let TORTUS process it, and handle the `meeting:completed` event.
Meeting mode is in **early access** and not yet available to all users and partners. To request
access, contact your TORTUS Account Manager or [support@tortus.ai](mailto:support@tortus.ai).
## Starting a meeting
Capture audio in the embedded app, the same flow a clinician sees in the standalone TORTUS meeting view.
```ts theme={null}
const meeting = await client.meetings.start(
{
mode: "LIVE_RECORDING",
reference: "mtg_external_123", // Optional: your own ID for cross-system correlation
title: "Weekly clinical governance",
notes: "Initial agenda: incident review, training updates",
},
{ returnTo: "home" }, // Where to send the user after the meeting closes
);
console.log("Meeting started:", meeting.reference);
```
Update the notes mid-flight, for example as the user types into your host UI:
```ts theme={null}
await meeting.setNotes?.("Updated agenda after attendees joined");
```
Skip the recorder by providing a URL to an existing audio file. Transcription begins immediately.
```ts theme={null}
const meeting = await client.meetings.start({
mode: "AUDIO_FILE",
reference: "mtg_external_456",
audio: { url: "https://your-domain.com/meeting-recording.mp3" },
title: "Quarterly review",
});
```
## Handling completion
When the user clicks **Finish meeting** in the embedded view, the SDK emits `meeting:completed` with the generated note in both Markdown and HTML, plus the transcription as plain text. As with consultations, you must call `finish()` to acknowledge receipt.
```ts theme={null}
client.on('meeting:completed', async ({ meeting, result, finish }) => {
const { meetingNotes, transcriptions } = result.artifacts;
const htmlNote = meetingNotes.find((n) => n.contentType === 'text/html');
const markdownNote = meetingNotes.find((n) => n.contentType === 'text/markdown');
const transcript = transcriptions.find((t) => t.contentType === 'text/plain');
try {
await persistMeetingNote({
reference: meeting.reference,
title: meeting.title,
noteHtml: htmlNote?.content,
noteMarkdown: markdownNote?.content,
transcript: transcript?.content,
});
finish(); // Acknowledge success (defaults to { status: 'success' })
} catch (error) {
finish({ status: 'failed' }); // Lets the user retry from the embedded UI
}
});
```
## Hosting your own meetings UI
To manage the meetings list and the start/finish flow in your **own** host UI, set `disableFinishMeetingButton: true` when calling `loadTortus(...)`:
```ts theme={null}
const client = await loadTortus({
publishableKey: 'pk_live_...',
container: '#tortus-container',
environment: 'production',
fetchClientSecrets: () => fetchTokenFromBackend(),
disableFinishMeetingButton: true,
});
```
When the flag is set:
* The embedded meeting view does **not** render the post-generation **Finish meeting** button.
* The SDK does **not** emit `meeting:completed`; your host drives completion through your own UI.
* The bundled meetings list / **Start new meeting** surface is shown so users can navigate between meetings without your host intervening.
The returned `TortusMeeting` also exposes `setAudio?(audio)` and `close()` for mid-flight updates
and teardown. See the [API reference](/reference/api).
# Introduction
Source: https://docs.tortus.ai/index
Embed AI-powered medical consultations into your healthcare application with the TORTUS Embed SDK.
The **TORTUS Embed SDK** (`@tortus-ai/embed-client`) lets your healthcare application embed TORTUS' AI-powered medical consultations directly into your own workflow. You load TORTUS into a container on your page, start a consultation from a few lines of TypeScript, and receive structured medical outputs (notes, letters, codes, and transcriptions) back through events.
The SDK renders the TORTUS experience inside a sandboxed iframe, so your users never leave your application and patient data stays protected.
## What you can build
Capture face-to-face or live-recorded consultations and turn them into structured clinical
documentation.
Send an existing recording and receive notes, letters, and coded data in return.
Capture non-patient conversations and generate meeting notes (early access).
## Why the SDK
* **Easy integration**: async initialisation with a single `loadTortus()` call.
* **Secure by design**: short-lived launch tokens, iframe sandboxing, and built-in validation for patient data.
* **Type-safe**: full TypeScript definitions for every option, event, and result.
* **Flexible**: multiple consultation modes and EHR integrations, or bring your own host UI.
## Start here
Go from zero to your first consultation in a few minutes.
Configure the registry and install the package.
Understand standby mode, events, and the client lifecycle.
Every method, option, and event in one place.
You'll need credentials from TORTUS to use the SDK: a **publishable key**, and a **client ID +
client secret** for your backend. Contact [support@tortus.ai](mailto:support@tortus.ai) if you
don't have these yet.
# Installation
Source: https://docs.tortus.ai/installation
Configure the registry and install the TORTUS Embed SDK.
The SDK is published to **GitHub Packages** under the `@tortus-ai` scope. Installing it is a two-step process: point the `@tortus-ai` scope at GitHub Packages, then install the package.
## Requirements
* A package manager (`npm`, `pnpm`, or `yarn`).
* A **GitHub Personal Access Token** (or an organisation token) with the `read:packages` permission.
* A build target of **ES2020 or newer** (see [Browser support](/reference/security#browser-support)).
## 1. Configure the registry
Create or update `.npmrc` in your project root so the `@tortus-ai` scope resolves to GitHub Packages:
```ini .npmrc theme={null}
@tortus-ai:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN
```
Don't commit a real token to source control. Use an environment variable or your CI secret store,
for example `//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}`.
## 2. Install the package
```bash npm theme={null}
npm install @tortus-ai/embed-client
```
```bash pnpm theme={null}
pnpm add @tortus-ai/embed-client
```
```bash yarn theme={null}
yarn add @tortus-ai/embed-client
```
## Verify the install
Import the entry point and check your editor picks up the bundled TypeScript types:
```ts theme={null}
import { loadTortus } from '@tortus-ai/embed-client';
```
If you don't have GitHub access, TORTUS can supply an npm-importable package file as an
alternative. Reach out to [support@tortus.ai](mailto:support@tortus.ai). GitHub Packages remains
the recommended path.
## Next step
Set up the launch token flow before you load the client.
# Quickstart
Source: https://docs.tortus.ai/quickstart
Embed TORTUS and run your first consultation in a few minutes.
This guide takes you from an empty page to a working consultation. By the end you'll have TORTUS embedded in your app, a consultation running, and structured results flowing back to your code.
## Prerequisites
Before you begin, make sure you have:
* A **publishable key** (`pk_...`) and a **client ID + client secret** from TORTUS. [Contact us](mailto:support@tortus.ai) if you need them.
* A **modern browser** target (ES2020+) and access to install from GitHub Packages.
* A **secure backend** you control. Launch tokens must never be minted in the browser.
## Steps
Configure the `@tortus-ai` scope to use GitHub Packages, then install the package.
```bash theme={null}
npm install @tortus-ai/embed-client
```
See [Installation](/installation) for the full registry and authentication setup.
The SDK authenticates with a short-lived **launch token** that you fetch from your own server, so your client secret never reaches the browser.
Save the `user_id` TORTUS returns and resend it as `userId` on later launches.
```ts theme={null}
// server-side route, e.g. GET /api/tortus/client-secrets
const credentials = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64");
// The TORTUS user ID you saved for this person on their first launch, if any.
const savedUserId = await getTortusUserId(currentUser.id);
const response = await fetch("https://api.tortus.ai/v1/oauth/launch", {
method: "POST",
headers: {
Authorization: `Basic ${credentials}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
...(savedUserId ? { userId: savedUserId } : {}), // resume an existing user
externalUserId: "your-internal-user-id",
userPayload: { email: "clinician@example.com", firstName: "Ada", lastName: "Lovelace" },
}),
});
const { launch_token, user_id } = await response.json();
// First launch only: persist the ID so future launches resume this user.
if (!savedUserId) {
await saveTortusUserId(currentUser.id, user_id);
}
// return { launchToken: launch_token } to your frontend
```
Read the full flow, including [persisting and resuming users](/authentication#persisting-and-resuming-users), in [Authentication](/authentication).
TORTUS renders into an element you provide. Give it a width and height.
```html theme={null}
```
Initialise the client. It starts in **standby mode**, waiting for instructions.
```ts theme={null}
import { loadTortus } from "@tortus-ai/embed-client";
const client = await loadTortus({
publishableKey: "pk_your_key_here",
container: "#tortus-container",
environment: "sandbox", // use "production" when you go live
fetchClientSecrets: async () => {
const res = await fetch("/api/tortus/client-secrets");
const data = await res.json();
return { launchToken: data.launchToken };
},
});
```
Subscribe to `consultation:completed` and acknowledge receipt with `finish()`.
```ts theme={null}
client.on("consultation:completed", async ({ consultation, result, finish }) => {
const { medicalNotes, letters, medicalCodes, transcriptions } = result.artifacts;
// Filter by contentType, never rely on array order
const note = medicalNotes.find((n) => n.contentType === "text/html");
try {
await saveToEhr({ reference: consultation.reference, note: note?.content, codes: medicalCodes });
await finish({ status: "success" });
} catch {
await finish({ status: "failed" }); // lets the user retry in the TORTUS UI
}
});
```
Kick off a face-to-face consultation. TORTUS takes over the embedded view from here.
```ts theme={null}
await client.consultations.start(
{
mode: "FACE_TO_FACE",
patient: { name: "Jane Smith", dateOfBirth: "1985-03-15" },
integration: { system: "EMIS" },
},
{ returnTo: "home" },
);
```
That's the full loop: load → start → receive results → acknowledge. Everything else builds on
these steps.
## Next steps
Explore audio, face-to-face, and live recording modes plus EHR integrations.
Learn the full event lifecycle and how to read artifacts.
Every option `loadTortus()` accepts.
The mental model behind the embedded client.
# API reference
Source: https://docs.tortus.ai/reference/api
Every method, option, and event in the TORTUS Embed SDK.
The complete public surface of `@tortus-ai/embed-client`. For task-oriented walkthroughs, see the [guides](/guides/consultations).
## loadTortus(config)
Initialises the TORTUS client and displays the standby screen automatically.
| Parameter | Type | Required | Description |
| - | - | - | - |
| `publishableKey` | `string` | Yes | Your TORTUS publishable API key. |
| `container` | `string \| HTMLElement` | Yes | DOM selector or element for the iframe. |
| `environment` | `'production' \| 'sandbox'` | Yes | Which environment to connect to. Must be specified explicitly. |
| `fetchClientSecrets` | `() => Promise<{ launchToken: string }>` | Yes | Async function that returns secure credentials so the app can authenticate. |
| `integration` | `Integration` | No | Default EHR integration, inherited by consultations without their own. |
**Returns** `Promise`
See [Configuration](/configuration) for the full option list.
## client.display(screen)
Navigate to a specific screen.
| Parameter | Type | Required | Description |
| - | - | - | - |
| `screen` | `'home' \| 'standby'` | Yes | The screen to display. |
**Returns** `Promise`: resolves when navigation completes, rejects if it isn't possible.
## client.consultations.start(consultation, options?)
Start a new consultation.
| Parameter | Type | Required | Description |
| - | - | - | - |
| `consultation` | `object` | Yes | Configuration including `mode`, patient data, and encounter details. |
| `options` | `object` | No | Behaviour options (see below). |
**`options`**
| Option | Type | Required | Description |
| - | - | - | - |
| `returnTo` | `'home' \| 'standby'` | No | Screen to return to when complete. |
**Returns** `Promise`
The `TortusConsultation` object contains all consultation data: `reference`, `mode`, `patient`,
`customFields`, `metadata`, and `integration`. When the consultation completes, this same data is
available in the `consultation` parameter of the `consultation:completed` event. The `result`
parameter contains only the generated artifacts.
## client.dictations.start(options?)
Start a new dictation session.
**`options`**
| Option | Type | Required | Description |
| - | - | - | - |
| `returnTo` | `'home' \| 'standby'` | No | Screen to return to when complete. |
**Returns** `Promise`
## client.meetings.start(meeting, options?)
Start a new meeting recording. See [Meetings](/guides/meetings) for full examples.
**`meeting`**
| Field | Type | Required | Description |
| - | - | - | - |
| `mode` | `'LIVE_RECORDING' \| 'AUDIO_FILE'` | Yes | How the meeting audio is captured. |
| `reference` | `string` | No | Cross-system identifier; auto-generated if omitted. |
| `title` | `string` | No | Meeting title. |
| `notes` | `string` | No | Notes, such as an agenda captured live. |
| `audio` | `{ url: string }` | When `mode` is `'AUDIO_FILE'` | Audio source for pre-recorded meetings. |
**`options`**
| Option | Type | Required | Description |
| - | - | - | - |
| `returnTo` | `'home' \| 'standby'` | No | Screen to return to when the meeting closes. |
**Returns** `Promise`
The returned `TortusMeeting` exposes `setNotes?(notes)`, `setAudio?(audio)`, and `close()` for mid-flight updates and teardown.
## client.on(event, callback)
Subscribe to client events. See [Handling events & results](/guides/events-and-results) for payload details.
| Parameter | Type | Required | Description |
| - | - | - | - |
| `event` | `string` | Yes | The event name to subscribe to. |
| `callback` | `function` | Yes | Handler invoked when the event fires. |
**Consultation events**
| Event | When it fires |
| - | - |
| `consultation:started` | A consultation has started. |
| `consultation:resumed` | An in-progress consultation was re-opened (e.g. after client restart). |
| `consultation:completed` | A consultation completed successfully. |
| `consultation:error` | A consultation failed with an error. |
| `consultation:cancelled` | A consultation was cancelled. |
| `consultation:closed` | A consultation was closed. |
**Meeting events**
| Event | When it fires |
| - | - |
| `meeting:started` | A meeting has started. |
| `meeting:resumed` | An in-progress meeting was re-opened. |
| `meeting:updated` | Meeting metadata (notes/audio) was updated. |
| `meeting:completed` | A meeting completed successfully (suppressed when `disableFinishMeetingButton` is `true`). |
| `meeting:cancelled` | A meeting was cancelled. |
| `meeting:closed` | A meeting was closed. |
**Event data**
For completion events, the `consultation` (or `meeting`) parameter contains all metadata, while `result` contains the generated artifacts (`medicalNotes`, `letters`, `medicalCodes`, `transcriptions`, and optionally `structuredNotes`). Filter artifacts by `contentType` rather than relying on array order.
`structuredNotes` (`StructuredNote[]`) is the medical note in section-shaped form: each note carries `templateId` and ordered `sections`, each with `id` (stable per template — treat as an opaque string), `sectionType`, `heading` (always present, including for sections rendered without a visible heading), `content` (Markdown), and `deleted` (`true` when the clinician deleted the section; its `content` is then empty and `medicalNotes` leaves it out). When present, it matches `medicalNotes`, except for deleted sections; it can be absent for any consultation by design, so always handle absence. See [Handling events & results](/guides/events-and-results) for details.
**Returns** a function that unsubscribes from the event.
## client.destroy()
Destroys the client and cleans up all resources.
**Returns** `Promise`
`destroy()` cleans up all resources but won't remove historical data saved in the browser.
# Security & support
Source: https://docs.tortus.ai/reference/security
Security model, browser support, and licensing for the TORTUS Embed SDK.
## Security considerations
The SDK is built for handling sensitive medical data. Keep these principles in mind:
* **Mint secrets server-side.** Always fetch client secrets and launch tokens from a secure backend you control. Never expose your client ID or secret to the frontend. See [Authentication](/authentication).
* **Secure message passing.** All communication between your app and the embedded experience uses secure message passing with origin validation.
* **Patient privacy.** A strict referrer policy and sandboxed iframe permissions protect patient data, and the embedded app stores encrypted data on the device rather than leaking PHI to the host.
Treat launch tokens as short-lived secrets. They expire after 5 minutes and should be requested
fresh from your backend for each session.
## Browser support
The SDK targets modern browsers and relies on:
* **ES2020+** language features.
* The **iframe `postMessage`** API.
* **`async`/`await`** syntax.
## Licence
© 2025 TORTUS AI. All rights reserved.
This library is provided under licence from TORTUS AI. Contact [TORTUS](mailto:support@tortus.ai) for licensing and usage terms.
## Need help?
Reach the TORTUS team for credentials, access, or integration help.
Revisit the fastest path to a working integration.