# Composite / Web

Composite recording (also known as web recording or headless browser recording) combines all participants into a single video file with a customizable layout. This is the easiest way to get started with call recording—you get a ready-to-share file with no post-processing required.

## Quickstart

<tabs groupId="examples">

<tabs-item value="js" label="JavaScript">

```js
// Start composite recording
// Triggers webhook event: call.recording_started
await call.startRecording("composite");

// Stop composite recording
// Triggers webhook event: call.recording_stopped
// When processing completes: call.recording_ready (contains recording URL)
await call.stopRecording("composite");

// List all recordings for this call
const response = await call.listRecordings();

// Download using the URL from response.recordings or the call.recording_ready webhook payload
// response.recordings[0].url
```

</tabs-item>

<tabs-item value="py" label="Python">

```py
# Start composite recording
# Triggers webhook event: call.recording_started
call.start_recording("composite")

# Stop composite recording
# Triggers webhook event: call.recording_stopped
# When processing completes: call.recording_ready (contains recording URL)
call.stop_recording("composite")

# List all recordings for this call
response = call.list_recordings()

# Download using the URL from response.data.recordings or the call.recording_ready webhook payload
# response.data.recordings[0].url
```

</tabs-item>

<tabs-item value="go" label="Golang">

```go
// Start composite recording
// Triggers webhook event: call.recording_started
call.StartRecording(ctx, "composite", &getstream.StartRecordingRequest{})

// Stop composite recording
// Triggers webhook event: call.recording_stopped
// When processing completes: call.recording_ready (contains recording URL)
call.StopRecording(ctx, "composite", &getstream.StopRecordingRequest{})

// List all recordings for this call
response, err := call.ListRecordings(ctx, &getstream.ListRecordingsRequest{})

// Download using the URL from response.Data.Recordings or the call.recording_ready webhook payload
// response.Data.Recordings[0].Url
```

</tabs-item>

<tabs-item value="curl" label="cURL">

```bash
# Start composite recording
# Triggers webhook event: call.recording_started
curl -X POST "https://video.stream-io-api.com/api/v2/video/call/${CALL_TYPE}/${CALL_ID}/recordings/composite/start?api_key=${API_KEY}" \
    -H "Authorization: ${TOKEN}" \
    -H "stream-auth-type: jwt"

# Stop composite recording
# Triggers webhook event: call.recording_stopped
# When processing completes: call.recording_ready (contains recording URL)
curl -X POST "https://video.stream-io-api.com/api/v2/video/call/${CALL_TYPE}/${CALL_ID}/recordings/composite/stop?api_key=${API_KEY}" \
    -H "Authorization: ${TOKEN}" \
    -H "stream-auth-type: jwt"

# List all recordings for this call
# Download using the URL from response.recordings or the call.recording_ready webhook payload
curl "https://video.stream-io-api.com/api/v2/video/call/${CALL_TYPE}/${CALL_ID}/recordings?api_key=${API_KEY}" \
    -H "Authorization: ${TOKEN}" \
    -H "stream-auth-type: jwt"
```

</tabs-item>

</tabs>

## How it works

When you start a composite recording, a hidden participant joins the call running a headless browser. This browser renders the video layout in real-time (grid, spotlight, or single participant) and the display output is captured and encoded into a single MP4 file. The recording is processed server-side and stored on AWS S3 (Stream-managed by default, or your own bucket).

## When to use composite recording

**Advantages:**

- Easy to get started—single file output ready for immediate playback
- Customizable layouts and branding (logos, colors, participant labels)
- No post-processing infrastructure needed
- Audio-only option available for lighter storage

**Limitations:**

- Higher cost compared to raw or individual recording (due to real-time rendering)
- Layout is fixed at recording time—you cannot reframe or re-layout after the fact
- Resolution and orientation are determined when recording starts and do not adapt during the call (e.g., if a participant switches from landscape to portrait, the recording won't adjust)

For use cases requiring post-production flexibility or per-participant tracks, consider [individual recording](/video/docs/api/recording/individual-track/) or [raw recording](/video/docs/api/recording/raw/).

## Events

These events are sent to users connected to the call and your webhook/SQS, each event contains a new `recording_type` field that refers to the type of recording (`composite` in this case):

- [`call.recording_started`](/video/docs/api/webhooks/events/#CallRecordingStartedEvent) when the call recording has started
- [`call.recording_stopped`](/video/docs/api/webhooks/events/#CallRecordingStoppedEvent) when the call recording has stopped
- [`call.recording_ready`](/video/docs/api/webhooks/events/#CallRecordingReadyEvent) when the recording is available for download
- [`call.recording_failed`](/video/docs/api/webhooks/events/#CallRecordingFailedEvent) when recording fails for any reason

## User Permissions

The following permissions are checked when users interact with the call recording API.

- `StartRecording` required to start the recording
- `StopRecording` required to stop the recording

For listing and deleting recordings, see [Recording Management](/video/docs/api/recording/manage/).

## Enabling / Disabling call recording

Recording can be configured from the Dashboard (see call type screen) or directly via the API. It is also possible to change the recording settings for a call and override the default settings coming from the its call type.

<tabs groupId="examples">

<tabs-item value="js" label="JavaScript">

```js
// Disable on call level
call.update({
  settings_override: {
    recording: {
      mode: "disabled",
    },
  },
});

// Disable on call type level
client.video.updateCallType({
  name: "<call type name>",
  settings: {
    recording: {
      mode: "disabled",
    },
  },
});

// Automatically record calls
client.video.updateCallType({
  name: "<call type name>",
  settings: {
    recording: {
      mode: "auto-on",
      quality: "720p",
    },
  },
});

// Enable
call.update({
  settings_override: {
    recording: {
      mode: "available",
    },
  },
});

// Other settings
call.update({
  settings_override: {
    recording: {
      mode: "available",
      audio_only: false,
      quality: "1080p",
    },
  },
});
```

</tabs-item>

<tabs-item value="py" label="Python">

```py
from getstream.models import RecordSettingsRequest

# Disable on call level
call.update(
    settings_override=CallSettingsRequest(
        recording=RecordSettingsRequest(
            mode='disabled',
        ),
    ),
)

# Disable on call type level
call_type_name = 'default'
client.video.update_call_type(call_type_name,
    settings=CallSettingsRequest(
        recording=RecordSettingsRequest(
            mode='disabled',
        ),
    ),
)

# Automatically record calls
client.video.update_call_type(call_type_name,
    settings=CallSettingsRequest(
        recording=RecordSettingsRequest(
            mode="auto-on",
            quality="720p",
        ),
    ),
)

# Enable recording feature for a specific call
call.update(
    settings_override=CallSettingsRequest(
        recording=RecordSettingsRequest(
            mode="available",
        ),
    ),
)

# Other settings
call.update(
    settings_override=CallSettingsRequest(
        recording=RecordSettingsRequest(
            mode="available",
            quality="1080p",
        ),
    ),
)
```

</tabs-item>

<tabs-item value="go" label="Golang">

```go
// Disable on call level
call.Update(ctx, &getstream.UpdateCallRequest{
  SettingsOverride: &getstream.CallSettingsRequest{
    Recording: &getstream.RecordSettingsRequest{
      Mode: "disabled",
    },
  },
})

// Disable on call type level
call_type_name := "default"
client.Video().UpdateCallType(ctx, call_type_name, &getstream.UpdateCallTypeRequest{
  Settings: &getstream.CallSettingsRequest{
	  Recording: &getstream.RecordSettingsRequest{
      Mode: "disabled",
    },
  },
})

// Automatically record calls
client.Video().UpdateCallType(ctx, call_type_name, &getstream.UpdateCallTypeRequest{
  Settings: &getstream.CallSettingsRequest{
	  Recording: &getstream.RecordSettingsRequest{
      Mode:    "auto-on",
      Quality: getstream.PtrTo("720p"),
    },
  },
})

// Enable recording feature for a specific call
call.Update(ctx, &getstream.UpdateCallRequest{
  SettingsOverride: &getstream.CallSettingsRequest{
	  Recording: &getstream.RecordSettingsRequest{
      Mode: "available",
    },
  },
})

// Other settings
call.Update(ctx, &getstream.UpdateCallRequest{
  SettingsOverride: &getstream.CallSettingsRequest{
    Recording: &getstream.RecordSettingsRequest{
      Mode:    "available",
      Quality: getstream.PtrTo("1080p"),
    },
  },
})
```

</tabs-item>

<tabs-item value="curl" label="cURL">

```bash
# Disable on call level
curl -X PATCH "https://video.stream-io-api.com/api/v2/video/call/default/${CALL_ID}?api_key=${API_KEY}" \
  -H "Authorization: ${TOKEN}" \
  -H "stream-auth-type: jwt" \
  -H "Content-Type: application/json" \
  -d '{
    "settings_override": {
      "recording": {
        "mode": "disabled"
      }
    }
  }'

# Enable on call level
curl -X PATCH "https://video.stream-io-api.com/api/v2/video/call/default/${CALL_ID}?api_key=${API_KEY}" \
  -H "Authorization: ${TOKEN}" \
  -H "stream-auth-type: jwt" \
  -H "Content-Type: application/json" \
  -d '{
    "settings_override": {
      "recording": {
        "mode": "available"
      }
    }
  }'

# Other settings
curl -X PATCH "https://video.stream-io-api.com/api/v2/video/call/default/${CALL_ID}?api_key=${API_KEY}" \
  -H "Authorization: ${TOKEN}" \
  -H "stream-auth-type: jwt" \
  -H "Content-Type: application/json" \
  -d '{
  "settings_override": {
    "recording": {
      "mode": "available",
      "audio_only": false,
      "quality": "1080p"
    }
  }
}'

# Enable/disable on call type level
curl -X PUT "https://video.stream-io-api.com/api/v2/video/calltypes/${CALL_TYPE_NAME}?api_key=${API_KEY}" \
  -H "Authorization: ${TOKEN}" \
  -H "stream-auth-type: jwt" \
  -H "Content-Type: application/json" \
  -d '{
    "settings": {
      "recording": {
        "mode": "disabled"
      }
    }
  }'
```

</tabs-item>

</tabs>

## Audio only recording

You can configure your calls to only record the audio tracks and exclude the video. This produces a single MP3 file with all participants mixed together. You can enable this from the dashboard (Call Types section) or set it for individual calls.

<tabs groupId="examples">

<tabs-item value="js" label="JavaScript">

```js
// Enable
call.update({
  settings_override: {
    recording: {
      mode: "available",
      audio_only: true,
    },
  },
});
```

</tabs-item>

<tabs-item value="py" label="Python">

```py
# Set recording only for audio
call.update(
    settings_override=CallSettingsRequest(
        recording=RecordSettingsRequest(
            mode='available',
            audio_only=True
        ),
    ),
)
```

</tabs-item>

<tabs-item value="go" label="Golang">

```go
// Set recording only for audio
call.Update(ctx, &getstream.UpdateCallRequest{
  SettingsOverride: &getstream.CallSettingsRequest{
    Recording: &getstream.RecordSettingsRequest{
      Mode:     "available",
      AudioOnly: getstream.PtrTo(true),
    },
  },
})
```

</tabs-item>

<tabs-item value="curl" label="cURL">

```bash
curl -X PATCH "https://video.stream-io-api.com/api/v2/video/call/default/${CALL_ID}?api_key=${API_KEY}" \
  -H "Authorization: ${TOKEN}" \
  -H "stream-auth-type: jwt" \
  -H "Content-Type: application/json" \
  -d '{
  "settings_override": {
    "recording": {
      "mode": "available",
      "audio_only": true
    }
  }
}'
```

</tabs-item>

</tabs>

## Recording layouts

Recording can be customized in several ways:

- You can pick one of the built-in layouts and pass some options to it
- You can further customize the style of the call by providing your own CSS file
- You can use your own recording application

There are three available layouts you can use for your calls: `"single-participant"`, `"grid"` and `"spotlight"`

### Single Participant

This layout shows only one participant video at a time, other video tracks are hidden.

![Layout Single Participant](@video/api/_assets/layout_single-participant.png)

The visible video is selected based on this priority:

- Participant is pinned
- Participant is screen-sharing
- Participant is the dominant speaker
- Participant has a video track

### Grid

This layout shows a configurable number of tracks in an equally sized grid.

![Layout Grid](@video/api/_assets/layout_grid.png)

### Spotlight

This layout shows a video in a spotlight and the rest of the participants in a separate list or grid.

![Layout Spotlight](@video/api/_assets/layout_spotlight.png)

## Layout options

Each layout has a number of options that you can configure. Here is an example:

![Layout Custom Options](@video/api/_assets/layout_custom_options.png)

<tabs groupId="examples">

<tabs-item value="js" label="JavaScript">

```js
const layoutOptions = {
  "logo.image_url":
    "https://theme.zdassets.com/theme_assets/9442057/efc3820e436f9150bc8cf34267fff4df052a1f9c.png",
  "logo.horizontal_position": "center",
  "title.text": "Building Stream Video Q&A",
  "title.horizontal_position": "center",
  "title.color": "black",
  "participant_label.border_radius": "0px",
  "participant.border_radius": "0px",
  "layout.spotlight.participants_bar_position": "top",
  "layout.background_color": "#f2f2f2",
  "participant.placeholder_background_color": "#1f1f1f",
  "layout.single-participant.padding_inline": "20%",
  "participant_label.background_color": "transparent",
};

client.video.updateCallType({
  name: callTypeName,
  settings: {
    recording: {
      mode: "available",
      audio_only: false,
      quality: "1080p",
      layout: {
        name: "spotlight",
        options: layoutOptions,
      },
    },
  },
});
```

</tabs-item>

<tabs-item value="py" label="Python">

```py
from getstream.models import CallSettingsRequest, LayoutSettingsRequest, RecordSettingsRequest

layout_options = {
    "logo.image_url": "https://theme.zdassets.com/theme_assets/9442057/efc3820e436f9150bc8cf34267fff4df052a1f9c.png",
    "logo.horizontal_position": "center",
    "title.text": "Building Stream Video Q&A",
    "title.horizontal_position": "center",
    "title.color": "black",
    "participant_label.border_radius": "0px",
    "participant.border_radius": "0px",
    "layout.spotlight.participants_bar_position": "top",
    "layout.background_color": "#f2f2f2",
    "participant.placeholder_background_color": "#1f1f1f",
    "layout.single-participant.padding_inline": "20%",
    "participant_label.background_color": "transparent",
}

client.video.update_call_type(
    "default",
    settings=CallSettingsRequest(
        recording=RecordSettingsRequest(
            mode="available",
            audio_only=False,
            quality="1080p",
            layout=LayoutSettingsRequest(
                name="spotlight",
                options=layout_options,
            ),
        ),
    ),
)
```

</tabs-item>

<tabs-item value="go" label="Golang">

```go
layoutOptions := map[string]any{
  "logo.image_url":                             "https://theme.zdassets.com/theme_assets/9442057/efc3820e436f9150bc8cf34267fff4df052a1f9c.png",
  "logo.horizontal_position":                   "center",
  "title.text":                                 "Building Stream Video Q&A",
  "title.horizontal_position":                  "center",
  "title.color":                                "black",
  "participant_label.border_radius":            "0px",
  "participant.border_radius":                  "0px",
  "layout.spotlight.participants_bar_position": "top",
  "layout.background_color":                    "#f2f2f2",
  "participant.placeholder_background_color":   "#1f1f1f",
  "layout.single-participant.padding_inline":   "20%",
  "participant_label.background_color":         "transparent",
}

client.Video().UpdateCallType(ctx, "callTypeName", &getstream.UpdateCallTypeRequest{
  Settings: &getstream.CallSettingsRequest{
    Recording: &getstream.RecordSettingsRequest{
      Mode:      "available",
      AudioOnly: getstream.PtrTo(false),
      Quality:   getstream.PtrTo("1080p"),
      Layout: &getstream.LayoutSettingsRequest{
        Name:    "spotlight",
        Options: &layoutOptions,
      },
    },
  },
})
```

</tabs-item>

<tabs-item value="curl" label="cURL">

```bash
curl -X PUT "https://video.stream-io-api.com/api/v2/video/calltypes/${CALL_TYPE_NAME}?api_key=${API_KEY}" \
  -H "Authorization: ${TOKEN}" \
  -H "stream-auth-type: jwt" \
  -H "Content-Type: application/json" \
  -d '{
    "settings": {
      "recording": {
        "mode": "available",
        "audio_only": false,
        "quality": "1080p",
        "layout": {
          "name": "spotlight",
          "options": {
            "logo.image_url": "https://theme.zdassets.com/theme_assets/9442057/efc3820e436f9150bc8cf34267fff4df052a1f9c.png",
            "logo.horizontal_position": "center",
            "title.text": "Building Stream Video Q&A",
            "title.horizontal_position": "center",
            "title.color": "black",
            "participant_label.border_radius": "0px",
            "participant.border_radius": "0px",
            "layout.spotlight.participants_bar_position": "top",
            "layout.background_color": "#f2f2f2",
            "participant.placeholder_background_color": "#1f1f1f",
            "layout.single-participant.padding_inline": "20%",
            "participant_label.background_color": "transparent"
          }
        }
      }
    }
  }'
```

</tabs-item>

</tabs>

Options are passed as a flat key/value map under `layout.options`. The tables below list every option the default layout app supports.

### Common options (all layouts)

These options apply to the `single-participant`, `grid`, and `spotlight` layouts.

| Option                                     | Type          | Default         | Allowed Values                                        | Description                                                                                                         |
| ------------------------------------------ | ------------- | --------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `video.background_color`                   | color         |                 |                                                       | Background color of the video element                                                                               |
| `video.scale_mode`                         | string        | browser default | `fit`, `fill`                                         | How camera video fills its box when the aspect ratio differs. `fill` crops to cover, `fit` letterboxes to contain   |
| `video.screenshare_scale_mode`             | string        | `fit`           | `fit`, `fill`                                         | Same as `video.scale_mode` but for screen share tracks                                                              |
| `logo.image_url`                           | string        |                 |                                                       | URL of a logo image to overlay on the layout                                                                        |
| `logo.width`                               | string        | `initial`       | CSS size                                              | Logo width                                                                                                          |
| `logo.height`                              | string        | `40px`          | CSS size                                              | Logo height                                                                                                         |
| `logo.horizontal_position`                 | string        | `right`         | `center`, `left`, `right`                             | Horizontal position of the logo                                                                                     |
| `logo.vertical_position`                   | string        | `bottom`        | `center`, `top`, `bottom`                             | Vertical position of the logo                                                                                       |
| `logo.margin_inline`                       | string        | `.875rem`       | CSS size                                              | Horizontal margin around the logo                                                                                   |
| `logo.margin_block`                        | string        | `.875rem`       | CSS size                                              | Vertical margin around the logo                                                                                     |
| `title.text`                               | string        |                 |                                                       | Title text to overlay on the layout                                                                                 |
| `title.font_size`                          | string        | `30px`          | CSS size                                              | Title font size                                                                                                     |
| `title.color`                              | color         | `white`         |                                                       | Title text color                                                                                                    |
| `title.horizontal_position`                | string        | `left`          | `center`, `left`, `right`                             | Horizontal position of the title                                                                                    |
| `title.vertical_position`                  | string        | `top`           | `center`, `top`, `bottom`                             | Vertical position of the title                                                                                      |
| `title.margin_inline`                      | string/number | `.875rem`       | CSS size                                              | Horizontal margin around the title                                                                                  |
| `title.margin_block`                       | string/number | `.875rem`       | CSS size                                              | Vertical margin around the title                                                                                    |
| `participant.aspect_ratio`                 | string        |                 | e.g. `"16 / 9"`, `"4 / 3"`, `"1 / 1"`, `"9 / 16"`     | Aspect ratio of each participant tile (ignored while screen sharing)                                                |
| `participant.border_radius`                | string/number |                 | CSS size                                              | Corner radius of the participant tile                                                                               |
| `participant.outline_color`                | color         | `#005fff`       |                                                       | Outline color applied to the currently speaking participant                                                         |
| `participant.outline_width`                | string        | `2px`           | CSS size                                              | Outline width applied to the currently speaking participant                                                         |
| `participant.placeholder_background_color` | color         |                 |                                                       | Background color of the placeholder shown when a participant has no video                                           |
| `participant.filter`                       | object        |                 | See [Filtering participants](#filtering-participants) | Optional filter to determine which participants are displayed                                                       |
| `participant_label.display`                | boolean       | `true`          |                                                       | Show the participant label                                                                                          |
| `participant_label.text_color`             | color         |                 |                                                       | Text color of the participant label                                                                                 |
| `participant_label.background_color`       | color         |                 |                                                       | Background color of the participant label                                                                           |
| `participant_label.border_width`           | string        | `0`             | CSS size                                              | Border width of the participant label                                                                               |
| `participant_label.border_color`           | color         | `rgba(0,0,0,0)` |                                                       | Border color of the participant label                                                                               |
| `participant_label.border_radius`          | string/number |                 | CSS size                                              | Corner radius of the participant label                                                                              |
| `participant_label.horizontal_position`    | string        | `left`          | `center`, `left`, `right`                             | Horizontal position of the participant label                                                                        |
| `participant_label.vertical_position`      | string        | `bottom`        | `center`, `top`, `bottom`                             | Vertical position of the participant label                                                                          |
| `participant_label.margin_inline`          | string        | `0`             | CSS size                                              | Horizontal margin around the participant label                                                                      |
| `participant_label.margin_block`           | string        | `0`             | CSS size                                              | Vertical margin around the participant label                                                                        |
| `layout.background_color`                  | color         |                 |                                                       | Background color of the layout                                                                                      |
| `layout.background_image`                  | string        |                 | CSS `background-image`                                | Background image of the layout, e.g. `url(https://...)`                                                             |
| `layout.background_size`                   | string        |                 | CSS `background-size`                                 | Background image sizing, e.g. `cover`                                                                               |
| `layout.background_position`               | string        |                 | CSS `background-position`                             | Background image position                                                                                           |
| `layout.background_repeat`                 | string        |                 | CSS `background-repeat`                               | Background image repeat behavior                                                                                    |
| `layout.forceMirrorParticipants`           | boolean       |                 | `true`, `false`                                       | Forces participant camera videos to be mirrored or unmirrored. When omitted, the default mirroring behavior applies |
| `custom_actions`                           | array         |                 | See [Custom actions](#custom-actions)                 | Optional array of custom actions executed when a defined condition is met                                           |
| `debug.show_timestamp`                     | boolean       | `false`         | `true`, `false`                                       | Renders a high-precision timestamp overlay over the video, useful for debugging frame timing                        |

### Single Participant

In addition to the [common options](#common-options-all-layouts), the `single-participant` layout supports:

| Option                                        | Type          | Default   | Allowed Values       | Description                                                            |
| --------------------------------------------- | ------------- | --------- | -------------------- | ---------------------------------------------------------------------- |
| `layout.single-participant.mode`              | string        | `default` | `default`, `shuffle` | When set to `shuffle`, periodically rotates which participant is shown |
| `layout.single-participant.shuffle_delay`     | number        |           |                      | Delay (in seconds) between shuffles when `mode` is `shuffle`           |
| `layout.single-participant.padding_inline`    | string        | `0px`     | CSS size             | Horizontal padding around the spotlighted participant                  |
| `layout.single-participant.padding_block`     | string/number | `0px`     | CSS size             | Vertical padding around the spotlighted participant                    |
| `layout.single-participant.presenter_visible` | boolean       | `true`    | `true`, `false`      | Show the presenter's camera video during screen sharing                |

### Spotlight

In addition to the [common options](#common-options-all-layouts), the `spotlight` layout supports:

| Option                                       | Type          | Default   | Allowed Values                   | Description                                                                          |
| -------------------------------------------- | ------------- | --------- | -------------------------------- | ------------------------------------------------------------------------------------ |
| `layout.spotlight.participants_bar_position` | string        | `bottom`  | `top`, `bottom`, `left`, `right` | Position of the participants bar relative to the spotlight                           |
| `layout.spotlight.participants_bar_limit`    | number/string | `dynamic` | `dynamic` or a number            | Max number of participants in the bar. `dynamic` fits as many as the available space |

### Grid

In addition to the [common options](#common-options-all-layouts), the `grid` layout supports:

| Option                     | Type    | Default | Allowed Values  | Description                                                                                              |
| -------------------------- | ------- | ------- | --------------- | -------------------------------------------------------------------------------------------------------- |
| `layout.grid.page_size`    | number  | `20`    |                 | Maximum number of participant tiles shown per page                                                       |
| `layout.grid.expand_tiles` | boolean | `false` | `true`, `false` | When enabled, participant tiles expand to fill the available space while maintaining a 16:9 aspect ratio |

<admonition type="info">

Screen share layout is configured separately from `layout.options` (via the call type's screen share layout setting). The supported screen share layouts are `single-participant`, `spotlight`, and `dominant-speaker`.

</admonition>

## Recording resolution and portrait mode

Calls can be recorded in different resolutions and modes (landscape and portrait). On the dashboard, you can configure the default settings for all calls of a specific call type.

![Recording Resolution And Orientation](@video/api/_assets/recording-resolution-and-orientation.png)

While this can be configured from the dashboard, you can also set it for individual calls:

<tabs groupId="examples">

<tabs-item value="js" label="JavaScript">

```js
client.video.updateCallType({
  name: callTypeName,
  settings: {
    recording: {
      mode: "available",
      quality: "portrait-1080x1920",
    },
  },
});
```

</tabs-item>

<tabs-item value="py" label="Python">

```py
from getstream.models import CallSettingsRequest, RecordSettingsRequest

client.video.update_call_type(
    "default",
    settings=CallSettingsRequest(
        recording=RecordSettingsRequest(
            mode="available",
            quality="portrait-1080x1920",
        ),
    ),
)
```

</tabs-item>

<tabs-item value="curl" label="cURL">

```bash
curl -X PUT "https://video.stream-io-api.com/api/v2/video/calltypes/${CALL_TYPE_NAME}?api_key=${API_KEY}" \
  -H "Authorization: ${TOKEN}" \
  -H "stream-auth-type: jwt" \
  -H "Content-Type: application/json" \
  -d '{
    "settings": {
      "recording": {
        "mode": "available",
        "quality": "portrait-1080x1920"
      }
    }
  }'
```

</tabs-item>

</tabs>

## Filtering participants

The `participant.filter` option allows you to choose which participants are visible in the recording.
Its value is a special filter object.

The following properties are allowed to be used in the filter object:

| Property            | Type     | Description                                                                                                           |
| ------------------- | -------- | --------------------------------------------------------------------------------------------------------------------- |
| `userId`            | string   | Participant's user id                                                                                                 |
| `isSpeaking`        | boolean  | Indicates wheather the participant is currently speaking                                                              |
| `isDominantSpeaker` | boolean  | Indicates wheather the participant is a dominant speaker (only one participant can be a dominant speaker at the time) |
| `name`              | string   | Participant's user name                                                                                               |
| `roles`             | string[] | List of participant's roles in the current call                                                                       |
| `isPinned`          | boolean  | Indicates wheather the participant is pinned                                                                          |
| `hasVideo`          | boolean  | Indicates whether the participant has video                                                                           |
| `hasAudio`          | boolean  | Indicates whether the participant has audio                                                                           |
| `hasScreenShare`    | boolean  | Indicates whether the participant has screen share video                                                              |

For example, to include only pinned participants in the recording, you can provide the following filter:

```json
{
  "participant.filter": {
    "isPinned": true
  }
}
```

If you want to filter participants based on their role, keep in mind that a participant can have more than one role within a call.
Because the `roles` property is an array, you must use the `$contains` operator to build your filter. For example, this filter
will only match participants with the role `"admin"`:

```json
{
  "participant.filter": {
    "roles": { "$contains": "admin" }
  }
}
```

When recording livestreams, including only participants with video is an easy way to exclude viewers from the recording:

```json
{
  "participant.filter": {
    "hasVideo": true
  }
}
```

Other operators you can use are `$neq` ("not equal to") and `$in` ("equal to one of the listed values"). For example, this filter
will only match participants with one of the three names:

```json
{
  "participant.filter": {
    "name": { "$in": ["Moe", "Larry", "Curly"] }
  }
}
```

You can also use the `$eq` ("equals") operator, but its effect is the same as not using any operator at all, as in the first example.

You can combine multiple conditions using the `$and`, `$or`, and `$not` operators:

```json
// Hide participants with the role "guest", unless they have been pinned:
{
  "participant.filter": {
    "$or": [
      { "$not": { "roles": { "$contains": "guest" } } },
      { "isPinned": true }
    ]
  }
}

// Show participants that either have video or screen share
{
  "participant.filter": {
    "$or": [
      { "hasVideo": true },
      { "hasScreenShare": true }
    ]
  }
}
```

## Custom actions

Custom actions let you dynamically adjust the recording setup during the runtime based on the conditions you define under `options.custom_actions` configuration option. The function that evaluates the conditions is the same as in [Filtering participants](#filtering-participants) (`participant.filter` option) and allows using the same operators.

### Available actions

#### layout_override

Override the current layout when a condition matches. The first matching `layout_override` action wins (checked top-to-bottom).

| Property           | Type              | Allowed Values                                         | Description                                                                                                 |
| ------------------ | ----------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| action_type        | `layout_override` |                                                        |                                                                                                             |
| layout             | string            | `[grid spotlight single-participant dominant-speaker]` | One of the supported layouts                                                                                |
| ignore_screenshare | boolean           |                                                        | When `true`, keeps the override even during screen sharing; otherwise `screenshare_layout` takes precedence |
| condition          | object            |                                                        |                                                                                                             |

#### options_override

Override any supported `options.*` values when a condition matches. All matching `options_override` actions are merged in order; later actions overwrite earlier ones for the same option keys.

| Property    | Type               | Allowed Values                                                                                      | Description                                                                                                           |
| ----------- | ------------------ | --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| action_type | `options_override` |                                                                                                     |                                                                                                                       |
| options     | object             | Refer to [Single Participant](#single-participant-1), [Spotlight](#spotlight-1) and [Grid](#grid-1) | A partial of the supported `options` keys (anything you normally set under `options.*`), `custom_actions` are omitted |
| condition   | object             |                                                                                                     |                                                                                                                       |

### Target values & supported operators

You can target these values in conditions:

| Property                   | Type   | Description                                                       |
| -------------------------- | ------ | ----------------------------------------------------------------- |
| `participant_count`        | number | Number of participants in the call                                |
| `pinned_participant_count` | number | Number of pinned participants in the call (evaluated client-side) |

Logical operators:

- `$and`, `$or`, `$not`

Scalar operators:

- `$eq`, `$neq`, `$in`, `$gt`, `$gte`, `$lt`, `$lte`

<admonition type="info">

The number of target values is currently limited but if you need other properties available feel free to reach out to our [support](https://getstream.io/contact/support/).

</admonition>

### Examples

Switch to a `dominant-spearker` layout when `pinned_participant_count` is greater or equal to one:

```json
{
  // other options
  "custom_actions": [
    {
      "action_type": "layout_override",
      "condition": {
        "pinned_participant_count": { "$gte": 1 }
      },
      "layout": "dominant-speaker"
    }
  ]
}
```

Apply different background color to the recording setup when second participant joins the call:

```json
{
  // other options
  "custom_actions": [
    {
      "action_type": "options_override",
      "condition": {
        "participant_count": { "$gt": 1 }
      },
      "options": {
        "layout.background_color": "hotpink"
      }
    }
  ]
}
```

## Custom recording styling using external CSS

You can customize how recorded calls look by providing an external CSS file. The CSS file needs to be publicly available and ideally hosted on a CDN to ensure the best performance.
The best way to find the right CSS setup is by running the layout app directly. The application is [publicly available on Github here](https://github.com/GetStream/stream-video-js/tree/main/sample-apps/react/egress-composite) and contains instructions on how to be used.

<tabs groupId="examples">

<tabs-item value="js" label="JavaScript">

```js
client.video.updateCallType({
  name: callTypeName,
  settings: {
    recording: {
      mode: "available",
      audio_only: false,
      quality: "1080p",
      layout: {
        name: "spotlight",
        external_css_url: "https://path/to/custom.css",
      },
    },
  },
});
```

</tabs-item>

<tabs-item value="py" label="Python">

```py
from getstream.models import CallSettingsRequest, LayoutSettingsRequest, RecordSettingsRequest

client.video.update_call_type(
    "default",
    settings=CallSettingsRequest(
        recording=RecordSettingsRequest(
            mode="available",
            audio_only=False,
            quality="1080p",
            layout=LayoutSettingsRequest(
                name="spotlight",
                external_css_url="https://path/to/custom.css",
            ),
        ),
    ),
)
```

</tabs-item>

<tabs-item value="go" label="Golang">

```go
client.Video().UpdateCallType(ctx, "callTypeName", &getstream.UpdateCallTypeRequest{
  Settings: &getstream.CallSettingsRequest{
    Recording: &getstream.RecordSettingsRequest{
      Mode:      "available",
      AudioOnly: getstream.PtrTo(false),
	    Quality:   getstream.PtrTo("1080p"),
      Layout: &getstream.LayoutSettingsRequest{
        Name:           "spotlight",
        ExternalCssUrl: getstream.PtrTo("https://path/to/custom.css"),
      },
    },
  },
})
```

</tabs-item>

<tabs-item value="curl" label="cURL">

```bash
curl -X PUT "https://video.stream-io-api.com/api/v2/video/calltypes/${CALL_TYPE_NAME}?api_key=${API_KEY}" \
  -H "Authorization: ${TOKEN}" \
  -H "stream-auth-type: jwt" \
  -H "Content-Type: application/json" \
  -d '{
    "settings": {
      "recording": {
        "mode": "available",
        "audio_only": false,
        "quality": "1080p",
        "layout": {
          "name": "spotlight",
          "external_css_url": "https://path/to/custom.css"
        }
      }
    }
  }'
```

</tabs-item>

</tabs>

## Advanced - record calls using a custom web application

If needed, you can use your own custom application to record a call. This is the most flexible and complex approach to record calls, make sure to reach out to our customer support before going with this approach.

The layout app used to record calls is available on [GitHub](https://github.com/GetStream/stream-video-js/tree/main/sample-apps/react/egress-composite) and is a good starting point. The repository also includes information on how to build your own.

<tabs groupId="examples">

<tabs-item value="js" label="JavaScript">

```js
client.video.updateCallType({
  name: callTypeName,
  settings: {
    recording: {
      mode: "available",
      audio_only: false,
      quality: "1080p",
      layout: {
        name: "custom",
        external_app_url: "https://path/to/layout/app",
      },
    },
  },
});
```

</tabs-item>

<tabs-item value="py" label="Python">

```py
from getstream.models import CallSettingsRequest, LayoutSettingsRequest, RecordSettingsRequest

client.video.update_call_type(
    "default",
    settings=CallSettingsRequest(
        recording=RecordSettingsRequest(
            mode="available",
            audio_only=False,
            quality="1080p",
            layout=LayoutSettingsRequest(
                name="custom",
                external_app_url="https://path/to/layout/app",
            ),
        ),
    ),
)
```

</tabs-item>

<tabs-item value="go" label="Golang">

```go

client.Video().UpdateCallType(ctx, "callTypeName", &getstream.UpdateCallTypeRequest{
  Settings: &getstream.CallSettingsRequest{
    Recording: &getstream.RecordSettingsRequest{
      Mode:      "available",
      AudioOnly: getstream.PtrTo(false),
      Quality:   getstream.PtrTo("1080p"),
      Layout: &getstream.LayoutSettingsRequest{
        Name:           "custom",
        ExternalAppUrl: getstream.PtrTo("https://path/to/layout/app"),
      },
    },
  },
})
```

</tabs-item>

<tabs-item value="curl" label="cURL">

```bash
curl -X PUT "https://video.stream-io-api.com/api/v2/video/calltypes/${CALL_TYPE_NAME}?api_key=${API_KEY}" \
  -H "Authorization: ${TOKEN}" \
  -H "stream-auth-type: jwt" \
  -H "Content-Type: application/json" \
  -d '{
    "settings": {
      "recording": {
        "mode": "available",
        "audio_only": false,
        "quality": "1080p",
        "layout": {
          "name": "custom",
          "external_app_url": "https://path/to/layout/app"
        }
      }
    }
  }'
```

</tabs-item>

</tabs>

## Client-side recording

Unfortunately, there is no direct support for client-side recording at the moment. Call recording at the moment is done by Stream server-side. If client-side recording is important for you please make sure to follow the conversation [here](https://github.com/GetStream/protocol/discussions/249).


---

This page was last updated at 2026-07-03T16:22:49.353Z.

For the most recent version of this documentation, visit [https://getstream.io/video/docs/javascript/recording/composite/](https://getstream.io/video/docs/javascript/recording/composite/).