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

# Conversation Recordings

> Store conversation recordings in your own S3, GCS, or Azure Blob storage. Federated identity - no secrets shared with Tavus.

The `recording_storage` config field works for **Amazon S3, Google Cloud Storage, and Azure Blob Storage** - pick a provider, configure a one-time trust relationship on your side, and pass us the resulting non-secret identifiers.

<Note>
  **No customer secrets are stored at Tavus.** Every supported path uses provider-native federated identity (IAM role assumption, GCP Workload Identity Federation, or Entra ID Federated Credentials). You configure trust on your side; we receive short-lived tokens at runtime.
</Note>

Recordings are typically available in your bucket within seconds to a few minutes after the call ends, depending on call length and provider. Once the recording lands, Tavus fires `application.recording_ready` (with `storage_provider` and a fully-qualified `storage_uri`) to your `callback_url`. See [Webhooks and Callbacks](/sections/webhooks-and-callbacks#application-callbacks).

## Set up your storage

<Tabs>
  <Tab title="Amazon S3">
    S3 is the fastest path - recordings are written directly into your bucket as they finalize. Works in every AWS region.

    <Note>
      **Scope your trust to your account.** The recording service presents the same ExternalId (`tavus`) for every Tavus account, so the trust policy alone can't tell your role which Tavus account a recording belongs to. The setup below isolates your account through the object key instead: Tavus writes every recording under a prefix that contains your Tavus Workspace ID, and your role only allows writes under that prefix. Click your user profile on the [PAL Maker](https://maker.tavus.io/dev) to find your Workspace ID.
    </Note>

    <Steps>
      <Step title="Create an IAM role in your AWS account">
        Configure the role's trust relationship with all three of the following - every field is **mandatory**:

        * **Trusted AWS principal:** AWS account ID `291871421005`.
        * **ExternalId:** `tavus`.
        * **Max session duration: 12 hours (43200 seconds).** AWS roles default to 1 hour, but the recording service requests 12-hour sessions when assuming the role. A role with the default duration will fail validation at room creation with `unable to assume role with given parameters`.

        <Note>
          **About the trusted AWS account.** Tavus's recording infrastructure is operated through Daily.co; AWS account ID `291871421005` belongs to them. The same account ID is documented in [Daily's S3 setup guide](https://docs.daily.co/docs/guides/features/recording/custom-s3-storage) for customers running their own security review. The ExternalId `tavus` identifies Tavus's recording integration with Daily. It is shared by all Tavus accounts, which is why the permissions policy in the next step, not the trust policy, is what scopes the role to your account.
        </Note>
      </Step>

      <Step title="Attach a permissions policy scoped to your Workspace ID">
        Replace `your-bucket-name` and `<your_workspace_id>`:

        ```json theme={null}
        {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Sid": "BucketLevel",
              "Effect": "Allow",
              "Action": [
                "s3:ListBucket",
                "s3:ListBucketMultipartUploads",
                "s3:ListBucketVersions"
              ],
              "Resource": "arn:aws:s3:::your-bucket-name"
            },
            {
              "Sid": "AccountPrefixAndValidationFile",
              "Effect": "Allow",
              "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:GetObjectVersion",
                "s3:AbortMultipartUpload",
                "s3:ListMultipartUploadParts"
              ],
              "Resource": [
                "arn:aws:s3:::your-bucket-name/tavus/<your_workspace_id>/*",
                "arn:aws:s3:::your-bucket-name/daily-co-test-upload.txt"
              ]
            }
          ]
        }
        ```

        Two things this policy does on purpose:

        * **The list actions stay bucket-wide.** They only exist at bucket level, and the recording service needs `s3:ListBucketMultipartUploads` to finish an upload after a dropped call. They don't grant access to object contents.
        * **Object access is limited to your prefix plus one fixed file.** When a conversation is created, the recording service uploads a small test file, `daily-co-test-upload.txt`, at the bucket root to confirm it can reach the bucket. It contains no conversation data and is safe to delete or expire with a lifecycle rule. Every other key outside `tavus/<your_workspace_id>/` is denied.

        <Warning>
          **Don't add wildcard denies on top of this policy.** An explicit `Deny s3:Get*` or `Deny s3:List*` overrides the allows above and breaks multipart upload completion, leaving recordings unfinished in your bucket. To go write-only, remove `s3:GetObject` and `s3:GetObjectVersion` from the allow list instead; this also disables download links generated by the recording service.
        </Warning>
      </Step>

      <Step title="Pass the role and a key_template on every conversation">
        ```shell cURL {7-14} theme={null}
        curl --request POST \
          --url https://tavusapi.com/v2/conversations \
          --header 'Content-Type: application/json' \
          --header 'x-api-key: <api_key>' \
          --data '{
            "properties": {
              "auto_start_recording": true,
              "recording_storage": {
                "provider": "s3",
                "bucket_name": "your-bucket-name",
                "bucket_region": "us-east-1",
                "assume_role_arn": "arn:aws:iam::123456789012:role/TavusRecordingWriter",
                "key_template": "recordings/{conversation_id}/{epoch_ms}.mp4"
              }
            },
            "face_id": "r5f0577fc829"
          }'
        ```

        `key_template` is what turns on the account-scoped layout. Tavus prepends `tavus/<your_workspace_id>/` to whatever you set, so the request above writes to `tavus/<your_workspace_id>/recordings/<conversation_id>/<epoch_ms>.mp4`. You never include the prefix yourself, and nothing in the request can change it.

        <Warning>
          **Set `key_template` on every S3 conversation.** A conversation without it falls back to the default layout `tavus/<conversation_id>/<epoch_ms>`, which a policy scoped to your prefix rejects, and that recording is not delivered.
        </Warning>
      </Step>
    </Steps>

    #### Customize the object key

    Everything after the `tavus/<your_workspace_id>/` prefix is yours to shape. Tokens: `{conversation_id}` (Tavus conversation UUID) and `{epoch_ms}` (epoch-milliseconds timestamp assigned when the recording starts, unique per recording). Allowed literal characters: `[0-9A-Za-z./_-]`. Max 512 characters. No leading slash, no `//`, no `..`, no escaped braces. On S3, `{epoch_ms}` is **required**. Invalid templates are rejected with a `400` when the conversation is created.

    | Goal | `key_template` | Resulting key |
    | - | - | - |
    | Per-conversation folders | `recordings/{conversation_id}/{epoch_ms}.mp4` | `tavus/<workspace_id>/recordings/<conv>/<epoch>.mp4` |
    | Flat, one folder | `{conversation_id}-{epoch_ms}.mp4` | `tavus/<workspace_id>/<conv>-<epoch>.mp4` |
    | Minimal | `{epoch_ms}` | `tavus/<workspace_id>/<epoch>` |

    <Accordion title="Legacy: bucket-wide policy and flat S3 fields (existing setups)">
      If you set up S3 recording before the account-scoped layout existed, your role most likely has a bucket-wide permissions policy and your requests don't set `key_template`. **That setup continues to work unchanged.** Recordings land at `tavus/<conversation_id>/<epoch_ms>`, and the `application.recording_ready` webhook reports that key.

      Bucket-wide permissions policy (legacy):

      ```json theme={null}
      {
        "Version": "2012-10-17",
        "Statement": [{
          "Effect": "Allow",
          "Action": [
            "s3:PutObject",
            "s3:GetObject",
            "s3:ListBucketMultipartUploads",
            "s3:AbortMultipartUpload",
            "s3:ListBucketVersions",
            "s3:ListBucket",
            "s3:GetObjectVersion",
            "s3:ListMultipartUploadParts"
          ],
          "Resource": [
            "arn:aws:s3:::your-bucket-name",
            "arn:aws:s3:::your-bucket-name/*"
          ]
        }]
      }
      ```

      The original flat fields on `properties` (without the `recording_storage` object) also continue to work and map internally to `provider: "s3"`:

      ```shell cURL {7-9} theme={null}
      curl --request POST \
        --url https://tavusapi.com/v2/conversations \
        --header 'Content-Type: application/json' \
        --header 'x-api-key: <api_key>' \
        --data '{
          "properties": {
            "enable_recording": true,
            "recording_s3_bucket_name": "your-bucket-name",
            "recording_s3_bucket_region": "us-east-1",
            "aws_assume_role_arn": "arn:aws:iam::123456789012:role/TavusRecordingWriter"
          },
          "face_id": "r5f0577fc829"
        }'
      ```

      **Moving to the account-scoped layout.** Do it in this order to avoid a window where recordings are rejected:

      1. Add `key_template` to every S3 conversation you create (this requires `recording_storage`; the flat fields don't support it). Recordings start landing under `tavus/<your_workspace_id>/…`.
      2. Update anything downstream that parses object keys or `application.recording_ready` payloads for the new prefix.
      3. Replace the bucket-wide permissions policy with the scoped one above.
    </Accordion>

    <Accordion title="Terraform setup for S3">
      ```hcl theme={null}
      variable "tavus_workspace_id" {
        description = "Your Tavus Workspace ID (PAL Maker - click your user profile)"
        type        = string
      }

      resource "aws_s3_bucket" "recordings" {
        bucket = "your-recording-bucket"
      }

      resource "aws_iam_role" "tavus_writer" {
        name = "TavusRecordingWriter"

        # The recording service requests 12-hour sessions; default 3600s will fail.
        max_session_duration = 43200

        assume_role_policy = jsonencode({
          Version = "2012-10-17"
          Statement = [{
            Effect    = "Allow"
            Principal = { AWS = "arn:aws:iam::291871421005:root" }
            Action    = "sts:AssumeRole"
            Condition = {
              StringEquals = { "sts:ExternalId" = "tavus" }
            }
          }]
        })
      }

      resource "aws_iam_role_policy" "writer" {
        name = "TavusRecordingWriter-s3-write"
        role = aws_iam_role.tavus_writer.id
        policy = jsonencode({
          Version = "2012-10-17"
          Statement = [
            {
              Sid    = "BucketLevel"
              Effect = "Allow"
              Action = [
                "s3:ListBucket",
                "s3:ListBucketMultipartUploads",
                "s3:ListBucketVersions",
              ]
              Resource = aws_s3_bucket.recordings.arn
            },
            {
              Sid    = "AccountPrefixAndValidationFile"
              Effect = "Allow"
              Action = [
                "s3:PutObject",
                "s3:GetObject",
                "s3:GetObjectVersion",
                "s3:AbortMultipartUpload",
                "s3:ListMultipartUploadParts",
              ]
              Resource = [
                "${aws_s3_bucket.recordings.arn}/tavus/${var.tavus_workspace_id}/*",
                "${aws_s3_bucket.recordings.arn}/daily-co-test-upload.txt",
              ]
            },
          ]
        })
      }
      ```
    </Accordion>
  </Tab>

  <Tab title="Google Cloud Storage">
    GCS uses Workload Identity Federation. Tavus exposes an OIDC issuer at `https://recording-copy.tavus.io`; you configure your GCP project to trust that issuer and bind it to a service account that has write access to your bucket.

    <Note>
      **Scope your trust to your account.** The steps below bind trust using your Tavus Workspace ID as an attribute condition (`attribute.customer_id`). This ensures only recordings belonging to your account can authenticate to your GCP resources. Click your user profile on the [PAL Maker](https://maker.tavus.io/dev) to find your Workspace ID.
    </Note>

    <Steps>
      <Step title="Create a Workload Identity Pool + Provider trusting Tavus">
        ```bash theme={null}
        PROJECT_ID="your-gcp-project"

        gcloud iam workload-identity-pools create tavus-recording-pool \
          --project="$PROJECT_ID" \
          --location="global" \
          --display-name="Tavus Recording Storage"

        gcloud iam workload-identity-pools providers create-oidc tavus-worker \
          --project="$PROJECT_ID" \
          --location="global" \
          --workload-identity-pool="tavus-recording-pool" \
          --display-name="Tavus Worker OIDC" \
          --issuer-uri="https://recording-copy.tavus.io" \
          --attribute-mapping="google.subject=assertion.sub,attribute.customer_id=assertion.customer_id" \
          --attribute-condition="attribute.customer_id == '<your_workspace_id>'"
        ```
      </Step>

      <Step title="Create a service account and grant it write access to your bucket">
        ```bash theme={null}
        BUCKET="your-recording-bucket"
        SA_EMAIL="tavus-recording-writer@${PROJECT_ID}.iam.gserviceaccount.com"

        gcloud iam service-accounts create tavus-recording-writer \
          --project="$PROJECT_ID" \
          --display-name="Tavus Recording Writer"

        gsutil iam ch "serviceAccount:${SA_EMAIL}:objectCreator" "gs://${BUCKET}"
        ```
      </Step>

      <Step title="Allow the federated identity to impersonate the service account">
        ```bash theme={null}
        PROJECT_NUMBER=$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')
        # Replace <your_workspace_id> with your Tavus Workspace ID (find in PAL Maker - click your user profile)
        PRINCIPAL="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/tavus-recording-pool/attribute.customer_id/<your_workspace_id>"

        gcloud iam service-accounts add-iam-policy-binding "$SA_EMAIL" \
          --project="$PROJECT_ID" \
          --role="roles/iam.workloadIdentityUser" \
          --member="$PRINCIPAL"
        ```
      </Step>

      <Step title="Pass the federation identifiers on conversation creation">
        ```shell cURL {7-14} theme={null}
        curl --request POST \
          --url https://tavusapi.com/v2/conversations \
          --header 'Content-Type: application/json' \
          --header 'x-api-key: <api_key>' \
          --data '{
          "face_id": "rc9cff32ceba",
          "properties": {
            "auto_start_recording": true,
            "recording_storage": {
              "provider": "gcs",
              "bucket_name": "your-recording-bucket",
              "project_id": "your-gcp-project",
              "workload_identity_provider": "projects/123456/locations/global/workloadIdentityPools/tavus-recording-pool/providers/tavus-worker",
              "service_account_email": "tavus-recording-writer@your-gcp-project.iam.gserviceaccount.com"
            }
          }
        }'
        ```

        <Note>
          `workload_identity_provider` is the resource name **without** the `//iam.googleapis.com/` prefix - Tavus prepends it.
        </Note>
      </Step>
    </Steps>

    <Accordion title="Terraform setup for GCP">
      ```hcl theme={null}
      resource "google_iam_workload_identity_pool" "tavus" {
        workload_identity_pool_id = "tavus-recording-pool"
        display_name              = "Tavus Recording Storage"
      }

      resource "google_iam_workload_identity_pool_provider" "tavus_worker" {
        workload_identity_pool_id          = google_iam_workload_identity_pool.tavus.workload_identity_pool_id
        workload_identity_pool_provider_id = "tavus-worker"
        attribute_mapping = {
          "google.subject"        = "assertion.sub"
          "attribute.customer_id" = "assertion.customer_id"
        }
        # Replace with your Tavus Workspace ID (find in PAL Maker - click your user profile)
        attribute_condition = "attribute.customer_id == '<your_workspace_id>'"
        oidc {
          issuer_uri = "https://recording-copy.tavus.io"
        }
      }

      resource "google_service_account" "tavus_writer" {
        account_id   = "tavus-recording-writer"
        display_name = "Tavus Recording Writer"
      }

      resource "google_storage_bucket_iam_member" "writer" {
        bucket = "your-recording-bucket"
        role   = "roles/storage.objectCreator"
        member = "serviceAccount:${google_service_account.tavus_writer.email}"
      }

      resource "google_service_account_iam_member" "wif_user" {
        service_account_id = google_service_account.tavus_writer.name
        role               = "roles/iam.workloadIdentityUser"
        # Replace with your Tavus Workspace ID (find in PAL Maker - click your user profile)
        member             = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.tavus.name}/attribute.customer_id/<your_workspace_id>"
      }
      ```
    </Accordion>

    #### Customize the object key (optional)

    By default, recordings land at `tavus/<conversation_id>/<epoch_ms>` (no file extension) in your bucket. Add a `key_template` field to your `recording_storage` config to override the destination key shape:

    ```json theme={null}
    {
      "recording_storage": {
        "provider": "gcs",
        "bucket_name": "your-bucket",
        "workload_identity_provider": "...",
        "service_account_email": "...",
        "key_template": "recordings/{conversation_id}/{epoch_ms}.mp4"
      }
    }
    ```

    Tokens substituted at copy time: `{conversation_id}` (Tavus conversation UUID) and `{epoch_ms}` (Daily epoch-ms timestamp, unique per recording instance). Allowed literal characters: `[0-9A-Za-z./_-{}]`. Max 512 characters. No leading slash, no `//`, no `..`. Invalid templates are rejected when your config is saved.

    Common shapes:

    | Goal | `key_template` | Resulting key |
    | - | - | - |
    | Default (today's behavior) | (omit field) | `tavus/<conversation_id>/<epoch_ms>` |
    | Add `.mp4` extension | `tavus/{conversation_id}/{epoch_ms}.mp4` | `tavus/<conv>/<epoch>.mp4` |
    | Custom prefix | `my-org/recs/{conversation_id}/{epoch_ms}.mp4` | `my-org/recs/<conv>/<epoch>.mp4` |
    | Flat layout (one file per conversation) | `my-org/recs/{conversation_id}.mp4` | `my-org/recs/<conv>.mp4` |

    <Warning>
      **Overwrite behavior for flat layouts.** A template without `{epoch_ms}` produces the same key for every recording instance on a given conversation. In normal Tavus CVI usage one conversation produces exactly one recording, so this is safe. If your integration calls `startRecording` / `stopRecording` multiple times on the same conversation, or you implement your own recording-error retry, later recordings will overwrite earlier ones in your bucket. Include `{epoch_ms}` in your template for a zero-collision guarantee.
    </Warning>

    <Note>
      **Permission scope.** If you scoped the service-account permission to a specific path prefix (rather than the whole bucket), update it to cover the prefix you choose in `key_template` before applying. Otherwise the recording will fail to deliver with `DESTINATION_AUTH_FAILED`.
    </Note>
  </Tab>

  <Tab title="Azure Blob Storage">
    Azure Blob uses Entra ID Federated Credentials. Tavus exposes an OIDC issuer at `https://recording-copy.tavus.io`; you create an App Registration on your side that trusts JWTs from this issuer.

    <Note>
      **Scope your trust to your account.** The steps below set the federated credential `subject` to your Tavus Workspace ID. This ensures only recordings belonging to your account can authenticate to your Azure resources. Click your user profile on the [PAL Maker](https://maker.tavus.io/dev) to find your Workspace ID.
    </Note>

    <Steps>
      <Step title="Create an App Registration with a federated credential">
        ```bash theme={null}
        TENANT_ID="<your-tenant-uuid>"
        SUBSCRIPTION="<your-subscription-uuid>"
        RESOURCE_GROUP="your-rg"
        STORAGE_ACCOUNT="yourrecordingsaccount"
        CONTAINER="conversation-recordings"

        # 1. Create an App Registration
        az ad app create --display-name "Tavus Recording Storage"
        APP_ID=$(az ad app list --display-name "Tavus Recording Storage" --query "[0].appId" -o tsv)

        # 2. Add the federated credential (this trusts JWTs from Tavus)
        cat > federation.json <<EOF
        {
          "name": "tavus-recording-copy",
          "issuer": "https://recording-copy.tavus.io",
          "subject": "<your_workspace_id>",
          "audiences": ["api://AzureADTokenExchange"]
        }
        EOF
        # Replace <your_workspace_id> with your Tavus Workspace ID (find in PAL Maker - click your user profile)
        az ad app federated-credential create --id "$APP_ID" --parameters federation.json

        # 3. Create a service principal for the app
        az ad sp create --id "$APP_ID"
        SP_ID=$(az ad sp show --id "$APP_ID" --query id -o tsv)
        ```
      </Step>

      <Step title="Grant the App Registration write access to the container">
        ```bash theme={null}
        SCOPE="/subscriptions/${SUBSCRIPTION}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.Storage/storageAccounts/${STORAGE_ACCOUNT}/blobServices/default/containers/${CONTAINER}"

        az role assignment create \
          --assignee-object-id "$SP_ID" \
          --assignee-principal-type ServicePrincipal \
          --role "Storage Blob Data Contributor" \
          --scope "$SCOPE"
        ```
      </Step>

      <Step title="Pass the federation identifiers on conversation creation">
        ```shell cURL {7-14} theme={null}
        curl --request POST \
          --url https://tavusapi.com/v2/conversations \
          --header 'Content-Type: application/json' \
          --header 'x-api-key: <api_key>' \
          --data '{
            "properties": {
              "auto_start_recording": true,
              "recording_storage": {
                "provider": "azure_blob",
                "storage_account": "yourrecordingsaccount",
                "container": "conversation-recordings",
                "tenant_id": "11111111-2222-3333-4444-555555555555",
                "client_id": "66666666-7777-8888-9999-000000000000"
              }
            },
            "face_id": "r3f4182ef554"
          }'
        ```
      </Step>
    </Steps>

    <Accordion title="Terraform setup for Azure">
      <Note>
        **Subscription ID** - the `azurerm` provider needs an explicit subscription ID. Either set `export ARM_SUBSCRIPTION_ID=<your-sub-id>` before `terraform apply`, or set `subscription_id` in your `provider "azurerm"` block. Without this, `terraform plan` hangs without a clear error.
      </Note>

      ```hcl theme={null}
      resource "azuread_application" "tavus" {
        display_name = "Tavus Recording Storage"
      }

      resource "azuread_service_principal" "tavus" {
        client_id = azuread_application.tavus.client_id
      }

      resource "azuread_application_federated_identity_credential" "tavus" {
        application_id = azuread_application.tavus.id
        display_name   = "tavus-recording-copy"
        description    = "Tavus recording delivery"
        audiences      = ["api://AzureADTokenExchange"]
        issuer         = "https://recording-copy.tavus.io"
        # Replace with your Tavus Workspace ID (find in PAL Maker - click your user profile)
        subject        = "<your_workspace_id>"
      }

      resource "azurerm_role_assignment" "writer" {
        scope                = azurerm_storage_container.recordings.resource_manager_id
        role_definition_name = "Storage Blob Data Contributor"
        principal_id         = azuread_service_principal.tavus.object_id
      }
      ```
    </Accordion>

    #### Customize the object key (optional)

    By default, recordings land at `tavus/<conversation_id>/<epoch_ms>` (no file extension) in your container. Add a `key_template` field to your `recording_storage` config to override the destination key shape:

    ```json theme={null}
    {
      "recording_storage": {
        "provider": "azure_blob",
        "storage_account": "your-account",
        "container": "your-container",
        "tenant_id": "...",
        "client_id": "...",
        "key_template": "recordings/{conversation_id}/{epoch_ms}.mp4"
      }
    }
    ```

    Tokens substituted at copy time: `{conversation_id}` (Tavus conversation UUID) and `{epoch_ms}` (Daily epoch-ms timestamp, unique per recording instance). Allowed literal characters: `[0-9A-Za-z./_-{}]`. Max 512 characters. No leading slash, no `//`, no `..`. Invalid templates are rejected when your config is saved.

    Common shapes:

    | Goal | `key_template` | Resulting blob name |
    | - | - | - |
    | Default (today's behavior) | (omit field) | `tavus/<conversation_id>/<epoch_ms>` |
    | Add `.mp4` extension | `tavus/{conversation_id}/{epoch_ms}.mp4` | `tavus/<conv>/<epoch>.mp4` |
    | Custom prefix | `my-org/recs/{conversation_id}/{epoch_ms}.mp4` | `my-org/recs/<conv>/<epoch>.mp4` |
    | Flat layout (one file per conversation) | `my-org/recs/{conversation_id}.mp4` | `my-org/recs/<conv>.mp4` |

    <Warning>
      **Overwrite behavior for flat layouts.** A template without `{epoch_ms}` produces the same blob name for every recording instance on a given conversation. In normal Tavus CVI usage one conversation produces exactly one recording, so this is safe. If your integration calls `startRecording` / `stopRecording` multiple times on the same conversation, or you implement your own recording-error retry, later recordings will overwrite earlier ones in your container. Include `{epoch_ms}` in your template for a zero-collision guarantee.
    </Warning>

    <Note>
      **Permission scope.** If you scoped the RBAC role assignment to a specific blob prefix (rather than the whole container), update it to cover the prefix you choose in `key_template` before applying. Otherwise the recording will fail to deliver with `DESTINATION_AUTH_FAILED`.
    </Note>
  </Tab>
</Tabs>

## Start recording

You can either start the recording yourself from your frontend, or have Tavus start it for you.

### Start it from your client

By default, recording does not start on its own - trigger it after the participant joins:

```javascript theme={null}
const call = Daily.createCallObject();

call.on('joined-meeting', () => {
  call.startRecording();
});
```

### Start recording automatically

Set `auto_start_recording` and Tavus begins the recording as soon as the pal joins the call - no client-side code, and nothing to coordinate in your frontend:

```shell cURL {7} theme={null}
curl --request POST \
  --url https://tavusapi.com/v2/conversations \
  --header 'Content-Type: application/json' \
  --header 'x-api-key: <api_key>' \
  --data '{
    "properties": {
      "auto_start_recording": true,
      "recording_storage": {
        "provider": "s3",
        "bucket_name": "your-bucket-name",
        "bucket_region": "us-east-1",
        "assume_role_arn": "arn:aws:iam::123456789012:role/TavusRecordingWriter"
      }
    },
    "face_id": "r3f4182ef554"
  }'
```

<Warning>
  **Use one or the other, not both.** With `auto_start_recording` set, the recording is already running by the time your client joins - calling `startRecording()` as well fails with `RecordingAlreadyExists` and surfaces a `recording-error` in your app.
</Warning>

**Requirements.** The request is rejected with a `400` if either of these isn't met:

* Pass `recording_storage`, so the recording has a destination to be delivered to.
* The conversation must use a Tavus-hosted room. Not supported alongside `daily_room`, `meeting_url`, or LiveKit - in those cases Tavus doesn't control the room, so start the recording from your own client.

<Note>
  **Recording begins about a second after the pal joins**, so it usually opens with a short lead-in before your user arrives. If the user never joins at all, you still receive a short recording and an `application.recording_ready` webhook for that conversation.
</Note>

<Note>
  **Using echo mode?** Start sending audio once you receive Daily's `recording-started` event.
</Note>

## Receive the recording

Once the recording lands in your destination, Tavus fires `application.recording_ready` to your `callback_url`:

```json theme={null}
{
  "properties": {
    "bucket_name": "<your-bucket>",
    "s3_key": "<object-key>",
    "duration": 1234,
    "storage_provider": "gcs",
    "storage_uri": "gs://<your-bucket>/<object-key>"
  },
  "conversation_id": "<conversation_id>",
  "event_type": "application.recording_ready",
  "message_type": "application",
  "timestamp": "2026-04-30T22:11:14Z"
}
```

By default the `s3_key` / `storage_uri` object key follows the pattern `tavus/<conversation_id>/<epoch_ms>` - a fixed `tavus/` prefix, the conversation UUID, and a Unix epoch-milliseconds timestamp assigned by the recording service when the recording starts. The key has no file extension by default; recordings are MP4 files regardless. The same key is reused across delivery retries for a given recording, so it is stable per recording.

<Note>
  Every provider accepts an optional `key_template` on `recording_storage` to shape the destination key. On S3 it also switches the key to the account-scoped layout `tavus/<your_workspace_id>/<your template>`, and the webhook reports that full key. See the [Amazon S3](#amazon-s3), [Google Cloud Storage](#google-cloud-storage), or [Azure Blob Storage](#azure-blob-storage) setup section.
</Note>

For GCS and Azure Blob, if delivery to your bucket exhausts retries (typically due to a misconfigured trust policy on your side), Tavus instead fires `application.recording_copy_failed` with `error_code` and `error_message`. The recording is retained in Tavus's recording infrastructure for \~30 days as a manual recovery window. See [event reference](/sections/webhooks-and-callbacks#application-callbacks).

## Verify your setup

After your first recording, check:

1. **`application.recording_ready` arrives** at your callback URL - typically within \~1 minute for an average call, longer for multi-hour recordings.
2. The `storage_uri` resolves - try opening it (or fetching it) from your cloud's CLI.
3. If you see `application.recording_copy_failed` instead, the `error_code` is your starting point: `DESTINATION_AUTH_FAILED` is almost always a trust-policy issue (verify the issuer URI, subject claim, or assume-role principal).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.