> ## Documentation Index
> Fetch the complete documentation index at: https://docs.daily.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Real-Time Transcription

> Transcribe speech in Daily calls in real time using Deepgram, with the useTranscription hook for React apps.

<Badge color="blue">Paid plans only</Badge>

Daily's transcription feature streams speech-to-text output to all participants in a call. It is powered by [Deepgram](https://deepgram.com) and supports a wide range of languages, models, and configuration options.

In Daily React, the [`useTranscription`](/reference/daily-react/use-transcription) hook gives you reactive state and helper functions for managing transcription — without writing manual event listener boilerplate.

## Transcription permissions

Only meeting owners and participants with `canAdmin: 'transcription'` can start or stop transcription. The permission can be granted via a [meeting token](/reference/rest-api/meeting-tokens/create-meeting-token#body-properties-permissions) at join time or dynamically via `updateParticipant()` during the call. See the [Permissions guide](/docs/daily-react/docs/permissions) for the full details.

Use [`usePermissions`](/reference/daily-react/use-permissions) to gate your transcription controls on the local participant's permission:

```jsx theme={null}
import { usePermissions, useTranscription } from '@daily-co/daily-react';

function TranscriptionControls() {
  const { canAdminTranscription } = usePermissions();
  const { isTranscribing, startTranscription, stopTranscription } = useTranscription();

  if (!canAdminTranscription) return null;

  return (
    <button onClick={() => isTranscribing ? stopTranscription() : startTranscription()}>
      {isTranscribing ? 'Stop captions' : 'Start captions'}
    </button>
  );
}
```

## Starting transcription

Destructure `startTranscription` from [`useTranscription`](/reference/daily-react/use-transcription) and call it when you are ready to begin:

```jsx theme={null}
import { useTranscription } from '@daily-co/daily-react';

function TranscriptionControls() {
  const { startTranscription, isTranscribing } = useTranscription();
  const { canAdminTranscription } = usePermissions();

  const handleStart = () => {
    startTranscription({
      language: 'en',
      model: 'nova-2',
      punctuate: true,
      profanity_filter: false,
    });
  };

  return (
    <button onClick={handleStart} disabled={isTranscribing || !canAdminTranscription}>
      Start captions
    </button>
  );
}
```

`startTranscription` is a thin wrapper around the daily-js [`startTranscription()`](/reference/daily-js/instance-methods/start-transcription) method, so it accepts the same `DailyTranscriptionDeepgramOptions` object described below.

### Start Options

**`DailyTranscriptionDeepgramOptions`**

<ParamField path="language" type="string" default="'en'">
  BCP-47 language tag. Examples: `'en'`, `'es'`, `'fr'`, `'de'`, `'ja'`, `'pt-BR'`. The available languages depend on the Deepgram model tier in use.
</ParamField>

<ParamField path="model" type="string">
  Deepgram model name. Examples: `'nova-2'`, `'nova'`, `'enhanced'`, `'base'`. Higher-tier models offer better accuracy at higher cost.
</ParamField>

<ParamField path="tier" type="string" deprecated>
  Deepgram tier. Deprecated in favor of specifying `model` directly, but still accepted for backwards compatibility.
</ParamField>

<ParamField path="profanity_filter" type="boolean">
  When `true`, Deepgram replaces profane words with asterisks.
</ParamField>

<ParamField path="redact" type="Array<string> | Array<boolean> | boolean">
  Entities to redact from the transcript. Pass `true` to redact all supported entities, or an array of entity type strings (e.g. `['pci', 'ssn']`).
</ParamField>

<ParamField path="endpointing" type="number | boolean">
  Controls how long Deepgram waits for silence before ending an utterance. Pass a number (milliseconds) or `true`/`false` to enable/disable.
</ParamField>

<ParamField path="punctuate" type="boolean">
  When `true`, Deepgram adds punctuation to the transcript.
</ParamField>

<ParamField path="extra" type="Record<string, any>">
  Arbitrary key-value pairs forwarded directly to the Deepgram API. Use this to pass advanced Deepgram options not exposed directly.
</ParamField>

<ParamField path="includeRawResponse" type="boolean">
  When `true`, each transcription message includes the full raw Deepgram response object in the `rawResponse` field of the event passed to `onTranscriptionMessage`.
</ParamField>

<ParamField path="instanceId" type="string">
  Identifier for this transcription instance. Required when running multiple simultaneous transcriptions.
</ParamField>

<ParamField path="participants" type="string[]">
  Array of session IDs. When provided, only those participants' audio is transcribed. Omit to transcribe everyone.
</ParamField>

### Language and model selection

<Tip>
  Check out [Deepgram's language support documentation](https://developers.deepgram.com/docs/models-languages-overview) for the `model`/`language` combination that works best for your use case.
</Tip>

## Handling transcription messages

Pass an `onTranscriptionMessage` callback to [`useTranscription`](/reference/daily-react/use-transcription) to receive transcript segments in real time. This is the React equivalent of listening to the `transcription-message` event on the call object.

```jsx theme={null}
import { useCallback, useState } from 'react';
import { useTranscription } from '@daily-co/daily-react';

function LiveCaptions() {
  const [lines, setLines] = useState([]);

  const onTranscriptionMessage = useCallback((event) => {
    const { participantId, text, timestamp, trackType, instanceId, rawResponse } = event;

    setLines((prev) => [
      ...prev,
      { participantId, text, timestamp },
    ]);

    if (rawResponse) {
      // Full Deepgram response — available when includeRawResponse: true
      console.log('raw Deepgram response:', rawResponse);
    }
  }, []);

  useTranscription({ onTranscriptionMessage });

  return (
    <div className="captions">
      {lines.map(({ participantId, text, timestamp }, i) => (
        <p key={i}>
          <span className="speaker">{participantId.slice(0, 8)}</span>: {text}
        </p>
      ))}
    </div>
  );
}
```

### Message Format

**`DailyEventObjectTranscriptionMessage`**

| Field           | Type                  | Description                                                                                               |
| --------------- | --------------------- | --------------------------------------------------------------------------------------------------------- |
| `participantId` | `string`              | Session ID of the speaking participant                                                                    |
| `text`          | `string`              | Transcribed text segment                                                                                  |
| `timestamp`     | `Date`                | When the segment was produced                                                                             |
| `rawResponse`   | `Record<string, any>` | Full Deepgram response (when `includeRawResponse: true`)                                                  |
| `trackType`     | `string`              | Which audio track was transcribed (`'cam-audio'`, `'screen-audio'`, `'rmpAudio'`, or a custom track name) |
| `instanceId`    | `string \| undefined` | The transcription instance that produced this message                                                     |

## Updating transcription

To change which participants are being transcribed while transcription is already running, use [`useDaily`](/reference/daily-react/use-daily) to access the call object directly and call `updateTranscription`:

```jsx theme={null}
import { useDaily } from '@daily-co/daily-react';

function TranscriptionFilter({ presenterSessionId }) {
  const daily = useDaily();

  const transcribePresenterOnly = () => {
    daily.updateTranscription({
      participants: [presenterSessionId],
    });
  };

  const transcribeEveryone = () => {
    daily.updateTranscription({
      participants: [], // empty array = all participants
    });
  };

  const transcribeSpecificInstance = () => {
    daily.updateTranscription({
      instanceId: 'primary',
      participants: [presenterSessionId],
    });
  };

  return (
    <div>
      <button onClick={transcribePresenterOnly}>Presenter only</button>
      <button onClick={transcribeEveryone}>Everyone</button>
    </div>
  );
}
```

<ParamField path="participants" type="string[]" required>
  Array of session IDs to transcribe going forward. Pass an empty array to
  transcribe all participants. This field is required.
</ParamField>

<ParamField path="instanceId" type="string">
  The instance to update. Omit to update the default instance.
</ParamField>

## Stopping transcription

Destructure `stopTranscription` from [`useTranscription`](/reference/daily-react/use-transcription):

```jsx theme={null}
import { useTranscription } from '@daily-co/daily-react';

function LeaveButton({ onLeave }) {
  const { stopTranscription } = useTranscription();

  const handleLeave = () => {
    stopTranscription();
    onLeave();
  };

  return <button onClick={handleLeave}>Leave</button>;
}
```

To stop a named instance, pass the call object directly via [`useDaily`](/reference/daily-react/use-daily):

```jsx theme={null}
const daily = useDaily();
daily.stopTranscription({ instanceId: 'primary' });
```

## Events

Pass event callbacks directly to [`useTranscription`](/reference/daily-react/use-transcription). The hook handles subscribing and unsubscribing automatically as your component mounts and unmounts.

### `onTranscriptionStarted`

Called for all participants when transcription begins. Use it to show a "live captions" indicator in your UI. The event object mirrors [`DailyEventObjectTranscriptionStarted`](/reference/daily-js/events/transcription-events#transcription-started) and includes fields such as `language`, `model`, `startedBy`, and `instanceId`.

```jsx theme={null}
const onTranscriptionStarted = useCallback((event) => {
  console.log('Transcription started:', event.instanceId);
  console.log('  Language:', event.language);
  console.log('  Model:', event.model);
  console.log('  Started by:', event.startedBy); // session ID
  setIsTranscribing(true);
}, []);

useTranscription({ onTranscriptionStarted });
```

### `onTranscriptionStopped`

Called for all participants when transcription ends. Use it to hide the captions UI and clear any status text.

```jsx theme={null}
const onTranscriptionStopped = useCallback((event) => {
  console.log('Transcription stopped. Instance:', event.instanceId);
  console.log('Stopped by:', event.updatedBy); // session ID
  setIsTranscribing(false);
}, []);

useTranscription({ onTranscriptionStopped });
```

### `onTranscriptionError`

Called when the transcription service encounters an error. The event includes `instanceId` and `errorMsg`.

```jsx theme={null}
const onTranscriptionError = useCallback((event) => {
  console.error('Transcription error on instance', event.instanceId, ':', event.errorMsg);
  setTranscriptionError(event.errorMsg);
}, []);

useTranscription({ onTranscriptionError });
```

You can pass all callbacks together in a single `useTranscription` call:

```jsx theme={null}
useTranscription({
  onTranscriptionStarted,
  onTranscriptionStopped,
  onTranscriptionError,
  onTranscriptionMessage,
});
```

## Complete example

<Steps>
  <Step title="Set up useTranscription with callbacks">
    ```jsx theme={null}
    import { useCallback, useState } from 'react';
    import { useTranscription } from '@daily-co/daily-react';

    function useLiveCaptions() {
      const [isActive, setIsActive] = useState(false);
      const [statusText, setStatusText] = useState('');
      const [captions, setCaptions] = useState([]);
      const [error, setError] = useState(null);

      const onTranscriptionStarted = useCallback(({ model, language }) => {
        setIsActive(true);
        setStatusText(`Live captions on (${model}/${language})`);
        setError(null);
      }, []);

      const onTranscriptionStopped = useCallback(() => {
        setIsActive(false);
        setStatusText('');
      }, []);

      const onTranscriptionError = useCallback(({ errorMsg }) => {
        setError(errorMsg);
        console.error('Transcription error:', errorMsg);
      }, []);

      const onTranscriptionMessage = useCallback(({ participantId, text, timestamp }) => {
        setCaptions((prev) => [...prev, { participantId, text, timestamp }]);
      }, []);

      const { startTranscription, stopTranscription } = useTranscription({
        onTranscriptionStarted,
        onTranscriptionStopped,
        onTranscriptionError,
        onTranscriptionMessage,
      });

      return { isActive, statusText, captions, error, startTranscription, stopTranscription };
    }
    ```
  </Step>

  <Step title="Display captions from onTranscriptionMessage">
    ```jsx theme={null}
    function CaptionsDisplay({ captions }) {
      return (
        <div
          className="captions-box"
          style={{ height: 200, overflowY: 'auto' }}
        >
          {captions.map(({ participantId, text, timestamp }, i) => (
            <p key={i}>
              <strong>{participantId.slice(0, 8)}</strong>: {text}
            </p>
          ))}
        </div>
      );
    }
    ```
  </Step>

  <Step title="Show status from onTranscriptionStarted/Stopped">
    ```jsx theme={null}
    function CaptionsStatus({ statusText, error }) {
      if (error) {
        return <p className="error">Captions error: {error}</p>;
      }
      return statusText ? <p className="status">{statusText}</p> : null;
    }
    ```
  </Step>

  <Step title="Start/stop button">
    ```jsx theme={null}
    import { usePermissions } from '@daily-co/daily-react';

    function LiveCaptionsFeature() {
      const { canAdminTranscription } = usePermissions();
      const {
        isActive,
        statusText,
        captions,
        error,
        startTranscription,
        stopTranscription,
      } = useLiveCaptions();

      const handleToggle = () => {
        if (isActive) {
          stopTranscription();
        } else {
          startTranscription({
            language: 'en',
            model: 'nova-2',
            punctuate: true,
            includeRawResponse: false,
          });
        }
      };

      return (
        <div>
          <CaptionsStatus statusText={statusText} error={error} />
          <CaptionsDisplay captions={captions} />
          {canAdminTranscription && (
            <button onClick={handleToggle}>
              {isActive ? 'Stop captions' : 'Start captions'}
            </button>
          )}
        </div>
      );
    }
    ```
  </Step>
</Steps>
