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

# Webhooks and Callbacks

> Set up a webhook server to generate a callback URL that receives event notifications from Tavus API.

Tavus sends JSON payloads to the `callback_url` you configure on each resource. On your server, read the body and branch on **`event_type`** (and `message_type` where noted). This page covers five callback areas: **conversation** events (below), **guardrails**, **objectives**, **face training**, and **video generation** - payload shapes differ, and most event-specific fields live under `properties`.

## Conversation Callbacks

If a `callback_url` is provided when you call **`POST https://tavusapi.com/v2/conversations`** ([Create Conversation](/api-reference/conversations/create-conversation)), callbacks will provide insight into the conversation's state. These can be system-related (e.g. PAL joins and room shutdowns) or application-related (e.g. final transcription parsing and recording-ready webhooks). Additional webhooks coming soon.

### Structure

All Conversation callbacks share the following basic structure. Differences will occur in the `properties` object.

```json theme={null}
{
    "properties": {
    "face_id": "<face_id>"
    },
    "conversation_id": "<conversation_id>",
    "webhook_url": "<webhook_url>",
    "event_type": "<event_type>",
    "message_type": "<system/application>",
    "timestamp": "<timestamp>"
}
```

### Types

Our callbacks are split into two main categories:

#### System Callbacks

These callbacks are to provide insight into system-related events in a conversation. They are:

* **system.pal\_joined**: Fired when the PAL is ready for a conversation. Use this in new integrations.
* **system.replica\_joined**: **Legacy duplicate** of `system.pal_joined` with an identical payload. Still sent so existing integrations keep working; prefer `system.pal_joined` for new code.
* **system.shutdown**: This is fired when the room shuts down, for any of the following reasons:
  * `max_call_duration reached`
  * `participant_left_timeout reached`
  * `participant_absent_timeout reached`
  * `bot_could_not_join_meeting_it_was_probably_ended`
  * `daily_room_has_been_deleted`
  * `exception_encountered_during_conversation_startup`
  * `end_conversation_endpoint_hit`
  * `internal error occurred at step x`

**Examples:**

<CodeGroup>
  ```json system.pal_joined theme={null}
  {
    "properties": {
      "face_id": "<face_id>"
    },
    "conversation_id": "<conversation_id>",
    "webhook_url": "<webhook_url>",
    "event_type": "system.pal_joined",
    "message_type": "system",
    "timestamp": "2025-07-11T06:45:47.472000Z"
  }
  ```

  ```json system.replica_joined (legacy) theme={null}
  {
    "properties": {
      "face_id": "<face_id>"
    },
    "conversation_id": "<conversation_id>",
    "webhook_url": "<webhook_url>",
    "event_type": "system.replica_joined",
    "message_type": "system",
    "timestamp": "2025-07-11T06:45:47.472000Z"
  }
  ```

  ```json system.shutdown theme={null}
  {
    "properties": {
      "face_id": "<face_id>",
      "shutdown_reason": "participant_left_timeout"
    },
    "conversation_id": "<conversation_id>",
    "webhook_url": "<webhook_url>",
    "event_type": "system.shutdown",
    "message_type": "system",
    "timestamp": "2025-07-11T06:48:37.564961Z"
  }
  ```
</CodeGroup>

#### Application Callbacks

These callbacks are to inform developers about logical events that take place. They are:

* **application.transcription\_ready**: This is fired after ending a conversation, where the chat history is saved and returned. Each transcript entry includes `role`, `content`, `timestamp` (Unix epoch float, seconds, when the turn began - same field name as live interaction events), `seconds_from_start`, `duration` (seconds, float - same field name as the `duration` on `conversation.stopped_speaking`), and `inference_id` (on assistant turns). The same payload is also available on the verbose [GET conversation](/api-reference/conversations/get-conversation) response under `events` (look for the event with `event_type: "application.transcription_ready"`).
* **application.recording\_ready**: This is fired once your recording is durably written to your storage destination. Includes `storage_provider` (`s3` / `gcs` / `azure_blob`) and a fully-qualified `storage_uri` so the same handler works across providers. See [Recording Storage](/sections/conversational-video-interface/quickstart/conversation-recordings) to set up GCS, Azure, or S3-in-any-region destinations.
* **application.recording\_copy\_failed**: This is fired only on the worker-copy path (GCS, Azure, or S3 in regions Daily doesn't support natively) when Tavus is unable to deliver the recording into your bucket after retries. Daily's default storage retains the recording for \~30 days as a manual recovery window. Common causes: customer IAM trust policy mismatch, federated credential drift, bucket region typo. Use this as the canary for misconfiguration in your end.
* **application.perception\_analysis**: This is fired after ending a conversation, when the PAL has finished summarizing the visual artifacts that were detected throughout the call. This is a feature that is only available when the PAL has `raven-1` specified in the [Perception Layer](/sections/conversational-video-interface/pal/perception).
* **application.post\_call\_action\_executed**: This is fired once for each [post-call action](/sections/conversational-video-interface/pal/post-call-tool) Tavus runs after the conversation ends, recording the outcome (`tool_name`, `status`, and the `request` / `response`). The same payload is also available on the verbose [GET conversation](/api-reference/conversations/get-conversation) response under `events`.

**Examples:**

<CodeGroup>
  ```json application.transcription_ready theme={null}
  {
    "properties": {
      "face_id": "<face_id>",
      "transcript": [
        {
          "role": "user",
          "content": "Hi.",
          "timestamp": 1779475660.12,
          "seconds_from_start": 4.43,
          "duration": 0.42
        },
        {
          "role": "assistant",
          "content": "Hello! How can I help?",
          "timestamp": 1779475661.04,
          "seconds_from_start": 5.35,
          "duration": 1.62,
          "inference_id": "inf_abc123"
        },
        {
          "role": "user",
          "content": "Quick question about my order.",
          "timestamp": 1779475665.21,
          "seconds_from_start": 9.52,
          "duration": 2.18
        },
        {
          "role": "assistant",
          "content": "Sure - what's the order number?",
          "timestamp": 1779475668.30,
          "seconds_from_start": 12.61,
          "duration": 2.05,
          "inference_id": "inf_def456"
        }
      ]
    },
    "conversation_id": "<conversation_id>",
    "webhook_url": "<webhook_url>",
    "event_type": "application.transcription_ready",
    "message_type": "application",
    "timestamp": "2025-07-11T06:48:37.566057Z"
  }
  ```

  ```json application.recording_ready theme={null}
  {
    "properties": {
      "bucket_name": "<bucket_name>",
      "s3_key": "<object_key>",
      "duration": 14,
      "storage_provider": "s3",
      "storage_uri": "s3://<bucket_name>/<object_key>"
    },
    "conversation_id": "<conversation_id>",
    "webhook_url": "<webhook_url>",
    "event_type": "application.recording_ready",
    "message_type": "application",
    "timestamp": "2025-06-19T06:55:18.137386Z"
  }
  ```

  ```json application.recording_copy_failed theme={null}
  {
    "properties": {
      "recording_id": "<daily_recording_id>",
      "s3_key": "<intended_object_key>",
      "duration": 14,
      "storage_provider": "gcs",
      "error_code": "DESTINATION_AUTH_FAILED",
      "error_message": "GCS token exchange failed: invalid Workload Identity Provider"
    },
    "conversation_id": "<conversation_id>",
    "webhook_url": "<webhook_url>",
    "event_type": "application.recording_copy_failed",
    "message_type": "application",
    "timestamp": "2026-04-30T22:11:14Z"
  }
  ```

  ```json application.perception_analysis theme={null}
  {
    "properties": {
      "analysis": "Example summary: participant visible at a desk, neutral lighting, engaged tone. (Real payloads can be much longer.)"
    },
    "conversation_id": "<conversation_id>",
    "webhook_url": "<webhook_url>",
    "message_type": "application",
    "event_type": "application.perception_analysis",
    "timestamp": "2025-07-11T06:51:37.591677Z"
  }
  ```

  ```json application.post_call_action_executed theme={null}
  {
    "properties": {
      "tool_id": "<tool_id>",
      "tool_name": "post_call_summary",
      "status": "success",
      "request": {
        "url": "https://your-app.example.com/hooks/call-summary",
        "method": "POST",
        "body": "{\"channel\":\"C0SUPPORT\",\"text\":\"Caller asked about order status; resolved.\"}"
      },
      "response": { "http_status": 200, "body": "ok" }
    },
    "conversation_id": "<conversation_id>",
    "webhook_url": "<webhook_url>",
    "event_type": "application.post_call_action_executed",
    "message_type": "application",
    "timestamp": "2026-04-29T03:47:05Z"
  }
  ```
</CodeGroup>

## Guardrail Callbacks

If a `callback_url` is provided on a [guardrail](/sections/conversational-video-interface/guardrails), a callback is sent when that guardrail is triggered during a conversation.

```json theme={null}
{
  "conversation_id": "<conversation_id>",
  "properties": {
    "guardrail": "<guardrails_name>",
    "guardrail_uuid": "<guardrail_uuid>"
  }
}
```

## Objective Callbacks

If a `callback_url` is provided on an [objective](/sections/conversational-video-interface/pal/objectives), a callback is sent when that objective is completed during a conversation.

```json theme={null}
{
  "conversation_id": "<conversation_id>",
  "objective_name": "<objective_name>",
  "output_variables": {
    "<variable_name>": "<value>"
  }
}
```

## Face Training Callbacks

If a `callback_url` is provided in the <a href="/api-reference/faces/create-face" target="_blank">`POST /faces`</a> call, you will receive a callback on face training completion or on face training error.

<Note>
  **Dual delivery during the `replica` → `face` rename.** While the API transitions from `replica` to `face`, each training event triggers **two** POSTs to the same `callback_url`: first the legacy payload with `replica_id` (for backward compatibility), then a second payload with `face_id`. Both POSTs share the same UUID value, the same `status`, and, on error, the same `error_message` - only the id field name differs.

  To handle this cleanly:

  * Distinguish the two payloads by which id field is present (`replica_id` vs. `face_id`).
  * De-duplicate on your side by treating `replica_id` and `face_id` as the same identifier for a given training event.
  * New integrations should key off `face_id`; the `replica_id` twin will be removed once the legacy field is retired.
</Note>

<Tabs>
  <Tab title="Face Training Completed">
    ```json Legacy payload (replica_id) theme={null}
    {
        "replica_id": "rxxxxxxxxx",
        "status": "ready"
    }
    ```

    ```json New payload (face_id) theme={null}
    {
        "face_id": "rxxxxxxxxx",
        "status": "ready"
    }
    ```
  </Tab>

  <Tab title="Face Training Error">
    On error, the `error_message` parameter will contain the error message. You can learn more about [API Errors and Status Details here](/sections/errors-and-status-details)

    ```json Legacy payload (replica_id) theme={null}
    {
        "replica_id": "rxxxxxxxxx",
        "status": "error",
        "error_message": "There was an issue processing your training video. The video provided does not meet the minimum duration requirement for training"
    }
    ```

    ```json New payload (face_id) theme={null}
    {
        "face_id": "rxxxxxxxxx",
        "status": "error",
        "error_message": "There was an issue processing your training video. The video provided does not meet the minimum duration requirement for training"
    }
    ```
  </Tab>
</Tabs>

## Video Generation Callbacks

If a `callback_url` is provided in the <a href="/api-reference/video-request/create-video" target="_blank">`POST /videos`</a> call, you will receive callbacks on video generation completed and on video error.

<Tabs>
  <Tab title="Video Generation Completed">
    ```json theme={null}
    {
        "created_at": "2024-08-28 15:27:40.824457",
        "data": {
        "script": "Hello this is a test to give examples of callbacks"
        },
        "download_url": "https://stream.mux.com/H5H029h02tY7XDpNj9JFDbLleTyUpsJr5npddO8gRsKqY/high.mp4?download=1e30440cf9",
        "generation_progress": "100/100",
        "hosted_url": "https://videos.tavus.io/video/1e30440cf9",
        "face_id": "r90bbd427f71",
        "status": "ready",
        "status_details": "Your request has processed successfully!",
        "stream_url": "https://stream.mux.com/H5H029h02tY7XDpNj9JFDbLleTyUpsJr5npddO8gRsKqY.m3u8",
        "updated_at": "2024-08-28 15:29:19.802670",
        "video_id": "1e30440cf9",
        "video_name": "face_id: r90bbd427f71 - August 28, 2024 - video: 1e30440cf9"
    }
    ```
  </Tab>

  <Tab title="Video Generation Error">
    On error, the `status_details` parameter will contain the error message. You can learn more about [API Errors and Status Details here](/sections/errors-and-status-details)

    ```json theme={null}
    {
        "created_at": "2024-08-28 15:32:53.058894",
        "data": {
        "script": "This is a test script to show how videos error"
        },
        "download_url": null,
        "error_details": null,
        "generation_progress": "0/100",
        "hosted_url": "https://videos.tavus.io/video/c9b85a6d36",
        "face_id": "r90bbd427f71",
        "status": "error",
        "status_details": "An error occurred while generating this request. Please check your inputs or try your request again.",
        "stream_url": null,
        "updated_at": "2024-08-28 15:35:03.762392",
        "video_id": "c9b85a6d36",
        "video_name": "face_id: r90bbd427f71 - August 28, 2024 - video: c9b85a6d36"
    }
    ```
  </Tab>
</Tabs>

## Sample Webhook Setup

Create a sample webhook endpoint using Python Flask, and expose it publicly with ngrok.

### Prerequisites

* <a href="https://www.python.org/downloads/" target="_blank">Python</a>

* <a href="https://ngrok.com/downloads/" target="_blank">Ngrok</a>

<Steps>
  <Step title="Step 1: Install Python Dependencies">
    Install the Python dependencies needed to create the server.

    ```sh theme={null}
    pip install flask requests
    ```
  </Step>

  <Step title="Step 2: Make a Webhook Server">
    Set up a webhook server and save it as `server.py`.

    ```py [expandable] theme={null}
    import requests
    from flask import Flask, request, jsonify

    app = Flask(__name__)

    # Store transcripts (in production, use a proper database)
    transcripts = {}

    @app.route('/webhook', methods=['POST'])
    def handle_tavus_callback():
        data = request.json
        event_type = data.get('event_type')
        conversation_id = data.get('conversation_id')
        
        print(f"Received callback: {event_type} for conversation {conversation_id}")
        
        if event_type in ('system.pal_joined', 'system.replica_joined'):
            print("✅ PAL has joined the conversation")
            
        elif event_type == 'system.shutdown':
            shutdown_reason = data['properties'].get('shutdown_reason')
            print(f"🔚 Conversation ended: {shutdown_reason}")
        
        elif event_type == 'application.recording_ready':
            s3_key = data['properties'].get('s3_key')
            print(f"s3_key : {s3_key}")

        elif event_type == 'application.perception_analysis':
            analysis = data['properties'].get('analysis')
            print(f"analysis : {analysis}")
            
        elif event_type == 'application.transcription_ready':
            print("📝 Transcript is ready!")
            transcript = data['properties']['transcript']
            transcripts[conversation_id] = transcript
            
            # Process the transcript
            analyze_conversation(conversation_id, transcript)
            
        return jsonify({"status": "success"}), 200

    def analyze_conversation(conversation_id, transcript):
        """Analyze the conversation transcript"""
        user_turns = len([msg for msg in transcript if msg['role'] == 'user'])
        assistant_turns = len([msg for msg in transcript if msg['role'] == 'assistant'])
        
        print(f"Conversation {conversation_id} analysis:")
        print(f"- User turns: {user_turns}")
        print(f"- Assistant turns: {assistant_turns}")
        print(f"- Total messages: {len(transcript)}")

        print("Conversation : ")

        for msg in transcript:
            print(f"{msg['role']} : {msg['content']}")

    if __name__ == '__main__':
        app.run(port=5000, debug=True)
    ```

    The server will receive and process webhook callbacks from Tavus, handle different event types, store transcripts in memory, and analyze conversation data for each session.
  </Step>

  <Step title="Step 3: Run the Server">
    Run the app using the following command in the terminal:

    ```sh theme={null}
    python server.py
    ```

    The server should run on port `5000`.
  </Step>

  <Step title="Step 4: Forward the Port Using Ngrok">
    With [ngrok](https://ngrok.com/downloads/) installed and on your PATH, forward the port from a terminal (on Windows you can run `ngrok.exe` from the install folder instead).

    ```sh theme={null}
    ngrok http 5000
    ```

    The command will generate a forwarding link (e.g., [https://1234567890.ngrok-free.app](https://1234567890.ngrok-free.app)), which can be used as the callback URL.
  </Step>

  <Step title="Step 5: Use the Callback URL">
    Include the callback URL in your request to Tavus by appending `/webhook` to the forwarding link and setting it in the `callback_url` field.

    ```sh Create conversation with callback_url {6} theme={null}
    curl --request POST \
      --url https://tavusapi.com/v2/conversations \
      --header 'Content-Type: application/json' \
      --header 'x-api-key: <api-key>' \
      --data '{
      "callback_url": "https://1234567890.ngrok-free.app/webhook",
      "face_id": "<face_id>",
      "pal_id": "<pal_id>"
    }'
    ```

    <Note>
      * Replace `<api-key>` with your actual API key. You can generate one in the <a href="https://maker.tavus.io/dev/api-keys" target="_blank">PAL Maker</a>.
      * Replace `<face_id>` with the Face ID you want to use.
      * Replace `<pal_id>` with the PAL ID you want to use.
    </Note>
  </Step>
</Steps>
