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

# Transcription & Closed Captions

> Use CometChat Calls SDK v5 transcription on iOS to transcribe calls, show live closed captions, and retrieve transcripts after the call.

<Note>
  **Available since v5.0.4** — transcription and closed captions require CometChat Calls SDK v5.0.4 or later for iOS. See [Setup](/docs/calls/ios/setup) to install or upgrade.
</Note>

Transcribe call sessions in real time and display live closed captions on screen. Transcripts are stored server-side and can be retrieved after the call using `TranscriptsRequest`.

<Warning>
  Transcription must be enabled for your CometChat app. Contact support if you need to enable this feature.
</Warning>

## How It Works

Transcription and closed captions are two related but separate things:

| Concept             | What it does                                                                                                                                                                   |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Transcription**   | Server-side speech-to-text for the session. Starting it brings a transcriber into the call, which produces the transcript that is stored for later retrieval.                  |
| **Closed captions** | The on-screen overlay that renders the live transcript as it arrives. Captions are produced from the running transcription, so they only appear while transcription is active. |

Starting transcription is a prerequisite for captions — turning captions on without an active transcription shows nothing.

## Starting Transcription

### Auto-Start Transcription

Configure transcription to start automatically when the session begins:

<Tabs>
  <Tab title="Swift">
    ```swift theme={null}
    let sessionSettings = CometChatCalls.sessionSettingsBuilder
        .enableAutoStartTranscription(true)
        .build()
    ```
  </Tab>

  <Tab title="Objective-C">
    ```objectivec theme={null}
    SessionSettings *sessionSettings = [[[CometChatCalls sessionSettingsBuilder]
        enableAutoStartTranscription:YES]
        build];
    ```
  </Tab>
</Tabs>

**Default:** `false`

### Manual Transcription Control

#### Start Transcription

Begin transcribing during an active call:

<Tabs>
  <Tab title="Swift">
    ```swift theme={null}
    CallSession.shared.startTranscription()
    ```
  </Tab>

  <Tab title="Objective-C">
    ```objectivec theme={null}
    [[CallSession shared] startTranscription];
    ```
  </Tab>
</Tabs>

#### Stop Transcription

Stop the current transcription. Any captions currently on screen are cleared:

<Tabs>
  <Tab title="Swift">
    ```swift theme={null}
    CallSession.shared.stopTranscription()
    ```
  </Tab>

  <Tab title="Objective-C">
    ```objectivec theme={null}
    [[CallSession shared] stopTranscription];
    ```
  </Tab>
</Tabs>

## Built-in UI Controls

Both transcription controls live in the control panel's **More** menu, and both are hidden by default.

### Transcription Menu Item

To show the transcription item:

<Tabs>
  <Tab title="Swift">
    ```swift theme={null}
    let sessionSettings = CometChatCalls.sessionSettingsBuilder
        .hideTranscriptionButton(false)
        .build()
    ```
  </Tab>

  <Tab title="Objective-C">
    ```objectivec theme={null}
    SessionSettings *sessionSettings = [[[CometChatCalls sessionSettingsBuilder]
        hideTranscriptionButton:NO]
        build];
    ```
  </Tab>
</Tabs>

**Default:** `true`

The item toggles between **Start Transcription** and **Stop Transcription** based on the current state.

### Closed Caption Menu Item

To show the closed-caption item:

<Tabs>
  <Tab title="Swift">
    ```swift theme={null}
    let sessionSettings = CometChatCalls.sessionSettingsBuilder
        .hideClosedCaptionButton(false)
        .build()
    ```
  </Tab>

  <Tab title="Objective-C">
    ```objectivec theme={null}
    SessionSettings *sessionSettings = [[[CometChatCalls sessionSettingsBuilder]
        hideClosedCaptionButton:NO]
        build];
    ```
  </Tab>
</Tabs>

**Default:** `true`

The item toggles between **Show Captions** and **Hide Captions**.

<Note>
  Captions are off by default, so the overlay appears only after the user turns them on **and** transcription is running for the session. Turning captions on before transcription starts shows nothing until the transcript begins arriving.
</Note>

## Caption Language

**Method:** `setCaptionLanguage(_ captionLanguage: String)`

Sets the language used for transcription and captions.

<Tabs>
  <Tab title="Swift">
    ```swift theme={null}
    let sessionSettings = CometChatCalls.sessionSettingsBuilder
        .setCaptionLanguage("en-US")
        .build()
    ```
  </Tab>

  <Tab title="Objective-C">
    ```objectivec theme={null}
    SessionSettings *sessionSettings = [[[CometChatCalls sessionSettingsBuilder]
        setCaptionLanguage:@"en-US"]
        build];
    ```
  </Tab>
</Tabs>

**Default:** `en-US`

<Accordion title="Supported Language Codes">
  | Code    | Language                               |
  | ------- | -------------------------------------- |
  | `en-US` | English (United States)                |
  | `de-DE` | German (Germany)                       |
  | `en-GB` | English (United Kingdom)               |
  | `es-ES` | Spanish (Spain)                        |
  | `fr-FR` | French (France)                        |
  | `hi-IN` | Hindi (India)                          |
  | `hu-HU` | Hungarian (Hungary)                    |
  | `it-IT` | Italian (Italy)                        |
  | `ja-JP` | Japanese (Japan)                       |
  | `ko-KR` | Korean (South Korea)                   |
  | `lt-LT` | Lithuanian (Lithuania)                 |
  | `ms-MY` | Malay (Malaysia)                       |
  | `nl-NL` | Dutch (Netherlands)                    |
  | `pt-PT` | Portuguese (Portugal)                  |
  | `ru-RU` | Russian (Russia)                       |
  | `sv-SE` | Swedish (Sweden)                       |
  | `tr-TR` | Turkish (Turkey)                       |
  | `zh`    | Chinese Mandarin (Simplified, China)   |
  | `zh-TW` | Chinese Mandarin (Traditional, Taiwan) |
</Accordion>

## Retrieving Transcripts

After a call, use `TranscriptsRequest` to list the transcript artifacts for a session. Each record is a **pointer to a downloadable transcript file**, not the transcript text itself.

<Note>
  The SDK must be initialized with `CometChatCalls.init()` and a user must be logged in. The auth token is read from the logged-in user at fetch time, so it automatically tracks re-logins — there is no `set(authToken:)` on this builder.
</Note>

### Building a Request

<Tabs>
  <Tab title="Swift">
    ```swift theme={null}
    let request = TranscriptsRequest.TranscriptsBuilder()
        .set(sessionId: "v1.us.2547167fe69871fd.alice")  // required
        .set(limit: 10)                                  // optional
        .build()

    request.fetchNext(onSuccess: { transcripts in
        for transcript in transcripts {
            print(transcript.tid, transcript.transcriptUrl)
        }
    }, onError: { error in
        print("Error: \(error?.errorDescription ?? "")")
    })
    ```
  </Tab>

  <Tab title="Objective-C">
    ```objectivec theme={null}
    TranscriptsBuilder *builder = [[TranscriptsBuilder alloc] init];
    TranscriptsRequest *request = [[[builder
        setWithSessionId:@"v1.us.2547167fe69871fd.alice"]
        setWithLimit:10]
        build];

    [request fetchNextOnSuccess:^(NSArray<Transcript *> * transcripts) {
        for (Transcript *transcript in transcripts) {
            NSLog(@"%@ %@", transcript.tid, transcript.transcriptUrl);
        }
    } onError:^(CometChatCallException * error) {
        NSLog(@"Error: %@", error.errorDescription);
    }];
    ```
  </Tab>
</Tabs>

| Method                   | Required | Description                                                                                                       |
| ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `set(sessionId: String)` | Yes      | The session ID whose transcripts to fetch. A missing or empty value fails the fetch with `ERROR_SESSION_ID_NILL`. |
| `set(limit: Int)`        | No       | Page size. Defaults to `30` and is clamped to the range `1`–`1000`.                                               |
| `build()`                | Yes      | Returns a `TranscriptsRequest`.                                                                                   |

<Note>
  Callbacks are delivered on the main queue, so it is safe to update UI directly from them.
</Note>

### Paginating

A `TranscriptsRequest` is a stateful cursor. Create one per session ID and drive it with `fetchNext` and `fetchPrevious`:

<Tabs>
  <Tab title="Swift">
    ```swift theme={null}
    let request = TranscriptsRequest.TranscriptsBuilder()
        .set(sessionId: sessionId)
        .set(limit: 10)
        .build()

    // Next page
    request.fetchNext(onSuccess: { transcripts in
        // [Transcript] — empty once the last page has been consumed
    }, onError: { error in
        print("Error: \(error?.errorDescription ?? "")")
    })

    // Previous page
    request.fetchPrevious(onSuccess: { transcripts in
        // [Transcript] — empty on a fresh request, or when already at the first page
    }, onError: { error in
        print("Error: \(error?.errorDescription ?? "")")
    })
    ```
  </Tab>

  <Tab title="Objective-C">
    ```objectivec theme={null}
    // Next page
    [request fetchNextOnSuccess:^(NSArray<Transcript *> * transcripts) {
        // Empty once the last page has been consumed
    } onError:^(CometChatCallException * error) {
        NSLog(@"Error: %@", error.errorDescription);
    }];

    // Previous page
    [request fetchPreviousOnSuccess:^(NSArray<Transcript *> * transcripts) {
        // Empty on a fresh request, or when already at the first page
    } onError:^(CometChatCallException * error) {
        NSLog(@"Error: %@", error.errorDescription);
    }];
    ```
  </Tab>
</Tabs>

* `fetchNext` delivers the next page, or an empty array when there are no more pages. A session with no transcripts delivers an empty array.
* `fetchPrevious` delivers the previous page, or an empty array when already on the first page. It never requests a page below `1`.
* The cursor is committed only from a successful response, so a failed fetch can simply be retried.
* Only one fetch may be in flight per request instance. Calling `fetchNext` or `fetchPrevious` while another is pending fails with `ERROR_REQUEST_IN_PROGRESS`; the original call is unaffected and still completes.

### Transcript Properties

| Property         | Type           | Description                                                            |
| ---------------- | -------------- | ---------------------------------------------------------------------- |
| `tid`            | String         | Transcript ID                                                          |
| `mid`            | String         | Meeting ID                                                             |
| `roomName`       | String         | Room name of the meeting                                               |
| `startTime`      | Int            | Meeting start time, in epoch **seconds**                               |
| `endTime`        | Int            | Meeting end time, in epoch **seconds**                                 |
| `url`            | String         | Meeting URL                                                            |
| `transcriptDate` | String         | Transcript date                                                        |
| `transcriptUrl`  | String         | URL of the downloadable transcript JSON                                |
| `metaData`       | \[String: Any] | The raw server entry, so fields not modelled above are still reachable |

<Note>
  The server omits keys whose value is empty, so sparse records are normal and should not be treated as an error. Missing string fields arrive as `""` and missing numbers as `0`. A single entry that fails to parse is skipped rather than failing the whole page.
</Note>

### Reading the Transcript Content

`transcriptUrl` points at the transcript file. Fetch it yourself to read the actual utterances:

<Tabs>
  <Tab title="Swift">
    ```swift theme={null}
    request.fetchNext(onSuccess: { transcripts in
        guard let transcript = transcripts.first,
              let url = URL(string: transcript.transcriptUrl),
              !transcript.transcriptUrl.isEmpty else { return }

        URLSession.shared.dataTask(with: url) { data, _, _ in
            guard let data = data else { return }
            let content = try? JSONSerialization.jsonObject(with: data)
            print(content ?? "")
        }.resume()
    }, onError: { error in
        print("Error: \(error?.errorDescription ?? "")")
    })
    ```
  </Tab>

  <Tab title="Objective-C">
    ```objectivec theme={null}
    [request fetchNextOnSuccess:^(NSArray<Transcript *> * transcripts) {
        Transcript *transcript = transcripts.firstObject;
        if (transcript.transcriptUrl.length == 0) { return; }

        NSURL *url = [NSURL URLWithString:transcript.transcriptUrl];
        [[[NSURLSession sharedSession] dataTaskWithURL:url
            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                if (data == nil) { return; }
                id content = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
                NSLog(@"%@", content);
            }] resume];
    } onError:^(CometChatCallException * error) {
        NSLog(@"Error: %@", error.errorDescription);
    }];
    ```
  </Tab>
</Tabs>

### Error Handling

Failures are delivered to the `onError` closure as a `CometChatCallException` carrying an `errorCode`:

| Condition                              | `errorCode`                  |
| -------------------------------------- | ---------------------------- |
| `CometChatCalls.init()` was not called | `INIT_NOT_CALLED`            |
| No logged-in user / empty auth token   | `ERROR_NILL_AUTH_TOKEN`      |
| `sessionId` missing or empty           | `ERROR_SESSION_ID_NILL`      |
| A fetch is already in flight           | `ERROR_REQUEST_IN_PROGRESS`  |
| No network                             | `ERROR_INTERNET_UNAVAILABLE` |
| Missing or malformed response          | `ERROR_JSON_EXCEPTION`       |
| Server-side API error                  | The server's own code        |

## Transcripts in Call Logs

Call logs can be filtered to transcribed calls, which also attaches each call's transcripts to the log:

<Tabs>
  <Tab title="Swift">
    ```swift theme={null}
    let callLogRequest = CallLogsRequest.CallLogsBuilder()
        .set(limit: 30)
        .set(hasTranscriptions: true)
        .build()

    callLogRequest.fetchNext(onSuccess: { callLogs in
        for callLog in callLogs {
            for transcript in callLog.transcriptions {
                print(transcript.tid, transcript.transcriptUrl)
            }
        }
    }, onError: { error in
        print("Error: \(error?.errorDescription ?? "")")
    })
    ```
  </Tab>

  <Tab title="Objective-C">
    ```objectivec theme={null}
    CallLogsBuilder *builder = [[CallLogsBuilder alloc] init];
    CallLogsRequest *callLogRequest = [[[builder
        setWithLimit:30]
        setWithHasTranscriptions:YES]
        build];

    [callLogRequest fetchNextOnSuccess:^(NSArray<CallLog *> * callLogs) {
        for (CallLog *callLog in callLogs) {
            for (Transcript *transcript in callLog.transcriptions) {
                NSLog(@"%@ %@", transcript.tid, transcript.transcriptUrl);
            }
        }
    } onError:^(CometChatCallException * error) {
        NSLog(@"Error: %@", error.errorDescription);
    }];
    ```
  </Tab>
</Tabs>

`transcriptions` is an empty array when the server omitted transcripts, so it never needs a nil check. Passing `false` leaves the list unfiltered, exactly as if the filter had never been set — and the server then omits the transcripts.

## Complete Example

<Tabs>
  <Tab title="Swift">
    ```swift theme={null}
    // 1. Join a session with transcription enabled
    let sessionSettings = CometChatCalls.sessionSettingsBuilder
        .enableAutoStartTranscription(true)
        .hideTranscriptionButton(false)
        .hideClosedCaptionButton(false)
        .setCaptionLanguage("en-US")
        .build()

    // 2. Control transcription during the call
    CallSession.shared.startTranscription()
    CallSession.shared.stopTranscription()

    // 3. Retrieve transcripts after the call
    let request = TranscriptsRequest.TranscriptsBuilder()
        .set(sessionId: sessionId)
        .set(limit: 10)
        .build()

    request.fetchNext(onSuccess: { transcripts in
        for transcript in transcripts {
            print(transcript.transcriptUrl)
        }
    }, onError: { error in
        print("Error: \(error?.errorDescription ?? "")")
    })
    ```
  </Tab>

  <Tab title="Objective-C">
    ```objectivec theme={null}
    // 1. Join a session with transcription enabled
    SessionSettings *sessionSettings = [[[[[[CometChatCalls sessionSettingsBuilder]
        enableAutoStartTranscription:YES]
        hideTranscriptionButton:NO]
        hideClosedCaptionButton:NO]
        setCaptionLanguage:@"en-US"]
        build];

    // 2. Control transcription during the call
    [[CallSession shared] startTranscription];
    [[CallSession shared] stopTranscription];

    // 3. Retrieve transcripts after the call
    TranscriptsBuilder *builder = [[TranscriptsBuilder alloc] init];
    TranscriptsRequest *request = [[[builder
        setWithSessionId:sessionId]
        setWithLimit:10]
        build];

    [request fetchNextOnSuccess:^(NSArray<Transcript *> * transcripts) {
        for (Transcript *transcript in transcripts) {
            NSLog(@"%@", transcript.transcriptUrl);
        }
    } onError:^(CometChatCallException * error) {
        NSLog(@"Error: %@", error.errorDescription);
    }];
    ```
  </Tab>
</Tabs>

## Related Documentation

* [SessionSettingsBuilder](/docs/calls/ios/session-settings)
* [Call Actions](/docs/calls/ios/actions)
* [Call Logs](/docs/calls/ios/call-logs)
