1. Tutorials
  2. Stream Meeting Transcripts with Recall.ai

Tutorials

Stream Meeting Transcripts with Recall.ai

This tutorial walks through connecting the Recall desktop plugin to a lightweight web interface so your users can see meeting transcripts in real time. You will install the @todesktop/plugin-recall plugin inside ToDesktop Builder, add the @todesktop/client-recall package to your frontend, and implement the minimal backend endpoint Recall needs to start recording.

Architecture diagram of the Recall plugin, ToDesktop Builder, backend, and web view
INFO

📢 Prerequisites

  • Latest version of ToDesktop Builder.
  • Node.js installed on your development machine.
  • Basic familiarity with bundling a small web app (Vite, webpack, or similar).
  • An account on Recall.ai and your API key. You may also want to set up an account with assemblyai for transcription.

Step 1: Create or open your ToDesktop Builder project

  1. Launch ToDesktop Builder.
  2. Create a new project (for example, choose Simple Window) or open an existing one.
  3. Set the Window URL to where you will host your frontend (during development this might be something like http://localhost:5173).
ToDesktop Builder's create new project screen

Step 2: Install the Recall plugin

  1. In ToDesktop Builder, navigate to Plugins.
  2. Search for Recall and click Install to add @todesktop/plugin-recall to your project.
  3. Keep the default configuration—no manual setup is required once the plugin is installed.

Installing from the Builder UI bundles the Recall capabilities (meeting detection, audio capture, transcript streaming etc.) inside your packaged desktop app.

ToDesktop Builder's plugins screen with the Recall plugin installed
TIP

đź§© Example source code

If you want to skip ahead, you can find a github repository with the full example source code below:

Step 3: Provide an upload-token endpoint

The Recall plugin calls your frontend, which in turn requests an upload token from your backend. This token authorises the Recall service to stream transcript events to the SDK.

Implement a POST endpoint at http://localhost:4000/api/create-sdk-upload (or any URL you configure). The endpoint must accept payloads similar to the one below and respond with JSON that contains an upload_token string.

        POST /api/create-sdk-upload
Content-Type: application/json
{
  "recording_config": {
    "transcript": { "provider": { "assembly_ai_v3_streaming": {} } },
    "realtime_endpoints": [
      { "type": "desktop-sdk-callback", "events": ["transcript.data", "participant_events.join"] }
    ]
  }
}

      

Check out the Creating an SDK upload on Recall's docs for more details on the payload structure.

Here's a very minimal Express implementation. You will need to add CORS support and error handling to get it production ready. Also, you may want to add a more robust authentication mechanism.

        import express from 'express';
import axios from 'axios';

const recallApiBase = process.env.RECALL_API_BASE || 'https://us-west-2.recall.ai';
const recallApiToken = process.env.RECALL_API_TOKEN;

const app = express();
app.use(express.json());

app.post('/api/create-sdk-upload', async (req, res) => {
  const response = await axios.post(`${recallApiBase}/api/v1/sdk_upload/`, req.body, {
    headers: {
      Authorization: `Token ${recallApiToken}`
    }
  });

  const { id, upload_token, recording_id } = response.data || {};
  res.json({ id, upload_token, recording_id });
});

app.listen(4000);

      

Tip: Log every request with a clear prefix (the example repo uses [RecallExample]) so you can troubleshoot quickly if the SDK reports an error.

Step 4: Scaffold your frontend project

Any bundler that supports TypeScript works. The snippets below assume Vite:

        npm create vite@latest recall-transcript-demo -- --template vanilla-ts
cd recall-transcript-demo
npm install
npm install @todesktop/client-recall

      

Update vite.config.ts to emit modern bundles (ES2020) if desired.

Step 5: Build the Recall UI

First create a src/index.html file with the following structure. You probably want to add some styling to make it look nice but we'll keep it unstyle for this tutorial:

        <html>
  <body>
    <main>
      <h1>Recall Transcript Example</h1>
      <p id="status">Initialising…</p>
      <p id="emptyState">Say something in the meeting to see transcript lines.</p>
      <ul id="transcript" aria-live="polite"></ul>
    </main>
    <script type="module" src="./index.js"></script>
  </body>
</html>

      

Reference frontend: todesktop-recall/packages/example.

Create src/main.ts with the following structure:

        import './style.css';
import { recallDesktop, type MeetingWindow } from '@todesktop/client-recall';

const status = document.querySelector<HTMLParagraphElement>('#status');
const list = document.querySelector<HTMLUListElement>('#transcript');
const emptyState = document.querySelector<HTMLParagraphElement>('#emptyState');

const setStatus = (text: string) => {
  status!.textContent = text;
  console.log('[RecallDemo]', text);
};

const toggleEmpty = (visible: boolean) => {
  emptyState!.style.display = visible ? 'block' : 'none';
};

toggleEmpty(true);

const appendTranscript = (speaker: string, text: string, meeting: MeetingWindow) => {
  const item = document.createElement('li');
  item.innerHTML = `<strong>${speaker} · ${
    meeting.platform ?? 'unknown'
  }</strong><span>${text}</span>`;
  list!.appendChild(item);
  item.scrollIntoView({ behavior: 'smooth', block: 'end' });
  toggleEmpty(false);
};

const handleRealtimeEvent = (payload: any) => {
  if (payload.event !== 'transcript.data') {
    return;
  }
  const words = payload.data?.data?.words?.map((w: any) => w.text?.trim()).filter(Boolean) ?? [];
  if (words.length === 0) {
    return;
  }
  const speaker = payload.data?.data?.participant?.name?.trim() || 'Unknown participant';
  appendTranscript(speaker, words.join(' '), payload.window);
};

const handleMeetingDetected = async ({ window }: { window: MeetingWindow }) => {
  setStatus(`Meeting detected for ${window.platform ?? 'unknown'} — fetching upload token…`);
  const response = await fetch('http://localhost:4000/api/create-sdk-upload', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      /* same payload as Step 3 */
    })
  });

  const { upload_token: uploadToken } = await response.json();
  const result = await recallDesktop.startRecording(window.id, uploadToken);

  if (!result.success) {
    throw new Error(result.message ?? 'Unable to start recording');
  }

  setStatus(`Recording started (${window.platform ?? 'unknown'}). Waiting for transcript data…`);
};

const initialise = async () => {
  setStatus('Initialising Recall SDK…');
  if (!recallDesktop.isAvailable()) {
    setStatus('Open this page inside your ToDesktop app to access the Recall SDK.');
    return;
  }

  const initResult = await recallDesktop.initSdk();
  if (!initResult.success) {
    setStatus(`Failed to initialise Recall SDK: ${initResult.message}`);
    return;
  }

  recallDesktop.addEventListener('realtime-event', handleRealtimeEvent);
  recallDesktop.addEventListener('meeting-detected', handleMeetingDetected);
  recallDesktop.addEventListener('recording-started', ({ window }) => {
    setStatus(`Recording confirmed for ${window.platform ?? 'unknown'}.`);
  });
  recallDesktop.addEventListener('recording-ended', () => {
    setStatus('Recording ended. Waiting for the next meeting…');
    toggleEmpty(true);
  });

  setStatus('Waiting for meeting detection…');
};

void initialise();

      

Key pieces borrowed from the example repo ( todesktop-recall/packages/example):

  • Logging helpers keep console output readable ([RecallExample] → [RecallDemo]).
  • handleMeetingDetected requests an upload token and starts recording with recallDesktop.startRecording.
  • handleRealtimeEvent filters for transcript.data events and renders the transcript text.
  • onRecordingEnded resets the empty state so the UI is ready for the next meeting.

Add matching HTML (e.g., index.html) with elements #status, #emptyState, and #transcript. Styling is optional; the example repo uses a centered card layout but plain markup works.

Step 6: Run everything together

  1. Start your upload-token backend (node server.js, etc.).
  2. Run the frontend dev server (npm run dev if using Vite).
  3. In ToDesktop Builder, press Play to open the window pointing to your local URL.
  4. Join a meeting. Watch the status message transition from Initialising… → Waiting for meeting detection… → Recording started… while transcript lines stream into the list.
Window of the Recall app with transcripts

Troubleshooting

  • SDK unavailable: Ensure you open the page inside ToDesktop, not a standalone browser tab. recallDesktop.isAvailable() returns false outside the desktop shell.
  • Token errors: Confirm the backend endpoint returns 200 with { "upload_token": "..." }. Missing or malformed tokens prevent recording.
  • No transcripts: Check meeting permissions and make sure the plugin is installed. Inspect DevTools for [RecallDemo] logs.
  • Duplicate recordings: Track the last active window ID (the example repo uses a setActiveRecordingWindowId helper) to avoid restarting for the same meeting.