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.
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.
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).
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.
Never put your Tavus API key in a browser. The interaction POST doesn’t need
it, and nothing client-side ever should.
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.
This endpoint accepts only the component ids above.
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.
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.
Every recorded interaction is delivered to your conversation webhook as a 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.
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.
skip and dismiss events are delivered as well as submit; count them
if you track completion rates.
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.
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_ids must be unique per logical interaction and reused on every retry of that interaction. The Tavus client uses ci_{tool_call_id}_{type}_{uuid}.