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.
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.
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.
S3 is the fastest path — recordings are written directly into your bucket as they finalize. Works in every AWS region.
1
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.
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 for customers running their own security review. The ExternalId tavus is Tavus’s identifier with Daily, gating cross-account sts:AssumeRole per the confused-deputy AWS pattern.
The original setup using flat properties on properties (without the recording_storage object) continues to work. Existing integrations don’t need to change.
These map internally to provider: "s3". New integrations should use recording_storage — it’s the only way to access GCS, Azure, and unsupported S3 regions, and it’s where new fields will be added.
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.
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 Tavus platform to find your Workspace ID.
1
Create a Workload Identity Pool + Provider trusting Tavus
Create a service account and grant it write access to your bucket
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}"
3
Allow the federated identity to impersonate the service account
PROJECT_NUMBER=$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')# Replace <your_workspace_id> with your Tavus Workspace ID (find in Tavus platform — 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"
4
Pass the federation identifiers on conversation creation
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:
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
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.
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.
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.
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 Tavus platform to find your Workspace ID.
1
Create an App Registration with a federated credential
TENANT_ID="<your-tenant-uuid>"SUBSCRIPTION="<your-subscription-uuid>"RESOURCE_GROUP="your-rg"STORAGE_ACCOUNT="yourrecordingsaccount"CONTAINER="conversation-recordings"# 1. Create an App Registrationaz 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 Tavus platform — click your user profile)az ad app federated-credential create --id "$APP_ID" --parameters federation.json# 3. Create a service principal for the appaz ad sp create --id "$APP_ID"SP_ID=$(az ad sp show --id "$APP_ID" --query id -o tsv)
2
Grant the App Registration write access to the container
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"
3
Pass the federation identifiers on conversation creation
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.
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 Tavus platform — 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}
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:
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
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.
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.
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.
GCS and Azure Blob accept an optional key_template on recording_storage to override the destination key shape — see the Google Cloud Storage or Azure Blob Storage setup section.
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.
application.recording_ready arrives at your callback URL — typically within ~1 minute for an average call, longer for multi-hour recordings.
The storage_uri resolves — try opening it (or fetching it) from your cloud’s CLI.
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).