Skip to main content
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.

Set up your storage

S3 is the fastest path - recordings are written directly into your bucket as they finalize. Works in every AWS region.
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 to find your Workspace ID.
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 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.
2

Attach a permissions policy scoped to your Workspace ID

Replace your-bucket-name and <your_workspace_id>:
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.
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.
3

Pass the role and a key_template on every conversation

cURL
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.
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.

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.
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):
The original flat fields on properties (without the recording_storage object) also continue to work and map internally to provider: "s3":
cURL
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.

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:

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:
cURL
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.
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.
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.
Using echo mode? Start sending audio once you receive Daily’s recording-started event.

Receive the recording

Once the recording lands in your destination, Tavus fires application.recording_ready to your callback_url:
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.
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, 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.

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).