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

# Canvas interactions, webhooks, and history

> Record Canvas interactions, receive canvas.interaction webhooks, and fetch the full interaction history.

**Canvas interactions** record what a user does on a Canvas card: tap, submit, skip, or dismiss. Each interaction is recorded with a POST, delivered to your `callback_url` as a `canvas.interaction` webhook, and queryable with the history GET. All endpoints live under `https://tavusapi.com`.

## When Magic Canvas is active

No per-conversation configuration is required. A PAL gets Canvas actions when both are true:

* The PAL has the **Magic Canvas skill** attached: `PUT /v2/pals/{pal_id}/skills/magic_canvas`, covered in [Configuring your PAL](/sections/conversational-video-interface/magic-canvas/api/configuration).
* The conversation has a rendering surface. Audio-only conversations, chat conversations, and conversations that join an external meeting via `meeting_url` (Zoom, Teams, Google Meet) get no Canvas actions.

When both are true, the PAL gets one action per enabled component, the control actions `canvas_clear` and `update_component`, and system-prompt guidance on when to show each component. This applies to every conversation start path, including hosted deployments (`POST /v2/deployments/{deployment_id}/start`).

## Record an Interaction

[`POST /v2/conversations/{conversation_id}/canvas/interactions`](/api-reference/canvas-interactions/record-canvas-interaction)

The Tavus-hosted embed and `@tavus/cvi-ui` post interactions for you; call this endpoint directly only if you build your own renderer.

```bash theme={null}
curl -X POST https://tavusapi.com/v2/conversations/{conversation_id}/canvas/interactions \
  -H "Content-Type: application/json" \
  -d '{
    "interaction_id": "ci_call_8f2d41_submit_5e0b7c2a",
    "tool_call_id": "call_8f2d41",
    "component": "canvas.question",
    "component_version": "v1",
    "type": "submit",
    "value": { "selected_option_ids": ["opt_2"], "skipped": false },
    "metadata": { "client": "kiosk-web" }
  }'
```

A successful request returns `200`:

```json theme={null}
{ "success": true }
```

An identical retry returns the same `200`; see [Idempotency and delivery guarantees](#idempotency-and-delivery-guarantees).

### Authentication

No API key or token is required while the conversation is active. Once the conversation ends, every POST is rejected with a 400.

### Rate limiting

The interaction POST is rate-limited at **120 requests per 60-second window** per `(client IP, conversation_id)`. The limit is checked before schema validation or any database work.

When you exceed the limit, the API returns **HTTP 429** with a `Retry-After` header (seconds until the current window resets) and a body of `{ "error": "Too many requests" }`. Back off and retry after that interval.

Custom renderers that post `heartbeat` interactions count toward this limit. Space heartbeats accordingly; the Tavus-hosted embed and `@tavus/cvi-ui` stay within the budget for normal sessions.

<Warning>
  Never put your Tavus API key in a browser. The interaction POST doesn't need
  it, and nothing client-side ever should.
</Warning>

### Request Body

| Field               | Type   | Required | Description                                                                                                                                                  |
| ------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `interaction_id`    | string | ✅        | 1–128 chars. Your idempotency key: unique per logical interaction, reused verbatim on retries. The Tavus client generates `ci_{tool_call_id}_{type}_{uuid}`. |
| `tool_call_id`      | string | ✅        | 1–128 chars. The id of the Canvas invocation that showed the card. Ties the interaction to a specific card instance.                                         |
| `component`         | string | ✅        | One of the component ids below.                                                                                                                              |
| `component_version` | string | ✅        | The component's contract version. `v1` for all current components.                                                                                           |
| `type`              | string | ✅        | One of `submit`, `skip`, `dismiss`, `clear`, `error`, `heartbeat`. The component must also allow it (see below).                                             |
| `value`             | object | ✅        | The interaction payload. At most 16 KB serialized. Shape depends on `component` and `type`.                                                                  |
| `metadata`          | object | ❌        | Your own annotations, at most 4 KB serialized. Defaults to `{}`. Stored and delivered as-is; never validated against the component contract.                 |

Unknown top-level fields are rejected with a 400.

Component ids: `canvas.question`, `canvas.input`, `canvas.calendar`, `canvas.scheduling_embed`, `canvas.text`, `canvas.chart`, `canvas.alert`.

<Note>
  This endpoint accepts only the component ids above.
</Note>

### Interaction Types

| `type`      | Sent when                                                                                     | Allowed on                                          |
| ----------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| `submit`    | The user completed the card: picked an answer, typed a value, chose a slot, booked a meeting. | `question`, `input`, `calendar`, `scheduling_embed` |
| `skip`      | The user explicitly skipped the card without answering.                                       | `question`, `input`, `calendar`, `scheduling_embed` |
| `dismiss`   | The user closed the card.                                                                     | all components                                      |
| `clear`     | The card was cleared from the canvas.                                                         | all components                                      |
| `error`     | The card hit a render or runtime error.                                                       | all components                                      |
| `heartbeat` | A liveness ping from a long-lived card.                                                       | all components                                      |

Display-only components (`text`, `chart`, `alert`) allow only the four lifecycle types; sending `submit` to one returns a 400.

### `value` Rules per Component

For `submit` and `skip`, `value` is validated against the component's contract. The lifecycle types (`dismiss`, `clear`, `error`, `heartbeat`) pass `value` through without component-specific validation.

| Component                 | `skip` rules                                                                                    | `submit` rules                                                                                                                                                                                                                                                                                                                                                                           |
| ------------------------- | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `canvas.question`         | `skipped: true` and no answer payload.                                                          | `skipped: false` with at least one selected option or a non-empty `custom_text`. Allowed keys are exactly `selected_option_ids` (list of strings, required), `skipped` (boolean, required), `custom_text` (max 4,000 chars), and `option_texts` (maps option ids to display strings of max 2,000 chars; every key must also appear in `selected_option_ids`). Unknown keys are rejected. |
| `canvas.input`            | `{ "skipped": true }`                                                                           | Requires `input_type` (one of `text`, `email`, `number`, `tel`) and `value`. For `number` the value must be numeric; otherwise it must be a string.                                                                                                                                                                                                                                      |
| `canvas.calendar`         | `{ "skipped": true }`                                                                           | Requires exactly one of: `selected_date` (string), `selected_slot` (`{ "id", "start", "end" }`), `selected_slots` (non-empty list of slots), or `selected_range` (`{ "start", "end" }`).                                                                                                                                                                                                 |
| `canvas.scheduling_embed` | Not validated: only `submit` is checked against the contract; other types pass `value` through. | Allowed keys are exactly `provider`, `scheduled`, `event_uri`, `invitee_uri`. `provider` must be `"calendly"` and `scheduled` must be `true`. The URI fields are optional but must be `https` Calendly URLs.                                                                                                                                                                             |

Example `value` payloads:

```json submit payloads by component [expandable] theme={null}
// canvas.question
{ "selected_option_ids": ["opt_2"], "skipped": false, "option_texts": { "opt_2": "2–10 people" } }

// canvas.input
{ "input_type": "email", "value": "ada@example.com" }

// canvas.calendar
{ "selected_slot": { "id": "slot_tue_10", "start": "2026-06-16T10:00:00Z", "end": "2026-06-16T10:30:00Z" } }

// canvas.scheduling_embed
{ "provider": "calendly", "scheduled": true, "event_uri": "https://api.calendly.com/scheduled_events/AAAA" }
```

### Errors

Schema and contract validation failures return an `{ "error", "fields" }` envelope. Conversation-state errors (400) and conflicts (409) return a `{ "message": "<string>" }` envelope.

| Status | Body                                                                                            | Meaning                                                                                                                                                                                                                                               |
| ------ | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `{ "error": "Invalid canvas interaction payload.", "fields": ["<field>", "_schema", ...] }`     | The body failed schema or contract validation. `fields` lists the offending top-level field names only (max 10, each truncated to 64 chars); cross-field failures surface under the single key `_schema`. No marshmallow detail strings are returned. |
| 400    | `{ "message": "Invalid conversation_id" }`                                                      | No conversation exists with this id. Note this is a 400, not a 404.                                                                                                                                                                                   |
| 400    | `{ "message": "Canvas interactions can only be recorded for active conversations." }`           | The conversation has ended (or hasn't started).                                                                                                                                                                                                       |
| 409    | `{ "message": "Interaction does not match the issued canvas instance for this tool_call_id." }` | The `component` or `component_version` doesn't match the card Tavus issued for this `tool_call_id`.                                                                                                                                                   |
| 409    | `{ "message": "interaction_id was already recorded with a different payload." }`                | This `interaction_id` was already used with different contents. Retries must be byte-identical; new interactions need a new id.                                                                                                                       |
| 429    | `{ "error": "Too many requests" }`                                                              | Rate limit exceeded (120 POSTs per 60-second window per client IP and conversation). Response includes a `Retry-After` header. Back off and retry.                                                                                                    |

The following checks run internally and all surface through the `{ "error", "fields" }` envelope above; their internal messages (component, component\_version, interaction-type, and size rules) are not returned to the client:

* An unsupported component id.
* A `component_version` that doesn't match the component's current version (`v1`).
* An interaction type the component doesn't allow (for example, `submit` on `canvas.text`).
* `value` over 16 KB or `metadata` over 4 KB serialized.
* A `value` that breaks the component's `submit` or `skip` rules; see the per-component rules above.

The size caps apply to the serialized JSON of `value` and `metadata` individually, not to the raw request body.

## The `canvas.interaction` Webhook

Every recorded interaction is delivered to your conversation webhook as a [`canvas.interaction`](/sections/event-schemas/canvas-interaction) event, on the same callback pipeline as all other conversation events. Set `callback_url` when creating the conversation and route on `event_type === "canvas.interaction"`; `properties` carries the interaction exactly as recorded.

```json theme={null}
{
  "message_type": "canvas",
  "event_type": "canvas.interaction",
  "conversation_id": "c123456",
  "timestamp": "2026-06-09T21:14:03Z",
  "properties": {
    "conversation_id": "c123456",
    "interaction_id": "ci_call_8f2d41_submit_5e0b7c2a",
    "tool_call_id": "call_8f2d41",
    "component": "canvas.question",
    "component_version": "v1",
    "type": "submit",
    "value": { "selected_option_ids": ["opt_2"], "skipped": false },
    "metadata": { "client": "kiosk-web" },
    "created_at": "2026-06-09T21:14:03.518923"
  }
}
```

`properties` fields:

| Field               | Type           | Required | Description                                                                                                                                                                                                                                          |
| ------------------- | -------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `conversation_id`   | string         | ✅        | The conversation the interaction belongs to.                                                                                                                                                                                                         |
| `interaction_id`    | string         | ✅        | The client's idempotency key for this interaction.                                                                                                                                                                                                   |
| `tool_call_id`      | string         | ✅        | The Canvas invocation that showed the card.                                                                                                                                                                                                          |
| `component`         | string         | ✅        | Component id, e.g. `canvas.question`.                                                                                                                                                                                                                |
| `component_version` | string         | ✅        | Component contract version, e.g. `v1`.                                                                                                                                                                                                               |
| `type`              | string         | ✅        | One of `submit`, `skip`, `dismiss`, `clear`, `error`, `heartbeat`.                                                                                                                                                                                   |
| `value`             | object         | ✅        | The interaction payload, exactly as validated and stored.                                                                                                                                                                                            |
| `metadata`          | object         | ✅        | The client's annotations, passed through unvalidated.                                                                                                                                                                                                |
| `created_at`        | string or null | ✅        | When the interaction was recorded. A naive ISO-8601 timestamp with microseconds and **no** timezone suffix (no trailing `Z`). The value is UTC; parse it accordingly. The top-level `timestamp` on the webhook envelope, by contrast, carries a `Z`. |

<Note>
  `skip` and `dismiss` events are delivered as well as `submit`; count them
  if you track completion rates.
</Note>

## Fetch History

[`GET /v2/conversations/{conversation_id}/canvas/interactions`](/api-reference/canvas-interactions/list-canvas-interactions)

Requires your API key; you must own the conversation. Call it from your backend, not the browser.

```bash theme={null}
curl https://tavusapi.com/v2/conversations/{conversation_id}/canvas/interactions \
  -H "x-api-key: <your-api-key>"
```

```json theme={null}
{
  "data": [
    {
      "conversation_id": "c123456",
      "interaction_id": "ci_call_8f2d41_submit_5e0b7c2a",
      "tool_call_id": "call_8f2d41",
      "component": "canvas.question",
      "component_version": "v1",
      "type": "submit",
      "value": { "selected_option_ids": ["opt_2"], "skipped": false },
      "metadata": { "client": "kiosk-web" },
      "created_at": "2026-06-09T21:14:03.518923"
    }
  ]
}
```

* Items use the same fields as the webhook's `properties` object.
* Ordered oldest first (`created_at`, then insertion order as a tiebreaker).
* Readable during and after the conversation: writes stop when the call ends, reads don't. Use it for post-call processing and to reconcile against your webhook log.

## Idempotency and Delivery Guarantees

* **The POST is idempotent on `(conversation_id, interaction_id)`.** An identical retry returns `200`, stores nothing new, and fires no second webhook. Safe to retry on timeouts.
* **"Identical" means `tool_call_id`, `component`, `component_version`, `type`, and `value` all match.** `metadata` is excluded; the first recorded `metadata` is kept and delivered.
* **A reused `interaction_id` with a different payload is a 409**, never a silent overwrite.
* **The webhook fires once, when the interaction is first recorded.** Replays, retries, and concurrent duplicates never re-fire it.
* **The interaction store is the source of truth.** In rare failure cases an event may not reach your endpoint even though the interaction was stored. If completeness matters, reconcile with the history GET after the call.
* **Self-generated `interaction_id`s** must be unique per logical interaction and reused on every retry of that interaction. The Tavus client uses `ci_{tool_call_id}_{type}_{uuid}`.
