Skip to main content
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. Conversations on Zero Data Retention (ZDR) are the exception: Tavus delivers each interaction and records none. All endpoints live under https://tavusapi.com.
These are card taps on Magic Canvas, not Interaction Events (the Daily app-message protocol for controlling a live call).

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.
  • 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 action canvas_clear, and guidance on when to show each component. See When cards appear for how usage_guidance, the system prompt, and conversational context fit together. 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 The Tavus-hosted embed and @tavus/cvi-ui post interactions for you; call this endpoint directly only if you build your own renderer.
A successful request returns 200:
An identical retry returns the same 200; see 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.
Never put your Tavus API key in a browser. The interaction POST doesn’t need it, and nothing client-side ever should.

Request Body

Unknown top-level fields are rejected with a 400. Component ids: canvas.question, canvas.input, canvas.calendar, canvas.scheduling_embed, canvas.text, canvas.image, canvas.chart, canvas.alert.
This endpoint accepts only the component ids above.

Interaction Types

Display-only components (text, image, 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. Example value payloads:
submit payloads by component

Errors

Schema and contract validation failures return an { "error", "fields" } envelope. Conversation-state errors (400) and conflicts (409) return a { "message": "<string>" } envelope. 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 NUL character (U+0000) in any string or object key inside value or metadata.
  • 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 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. Under ZDR, each accepted interaction is delivered the same way, though none is recorded. Tavus sends this event only for conversations with Magic Canvas enabled, and only when the conversation has a callback_url.
  • Treat it as untrusted input. The end user’s browser reports each interaction through the public POST. Tavus checks the payload’s shape, but the values are whatever the client sent, so validate them before you act on them.
  • The same interaction can be sent more than once. Treat two events as the same interaction when conversation_id and interaction_id match, and do not apply the second one again. An interaction_id is unique only within its conversation.
properties fields:
skip and dismiss events are delivered as well as submit; count them if you track completion rates.

Fetch History

GET /v2/conversations/{conversation_id}/canvas/interactions Requires your API key; you must own the conversation. Call it from your backend, not the browser.
  • 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.
  • Empty for conversations on ZDR, which record no interactions.

Idempotency and Delivery Guarantees

Conversations on ZDR record no interactions, so the guarantees here that rely on a stored record do not apply to them. That includes idempotent replays, the 409 for a reused interaction_id, and the history GET. Under ZDR there is no saved interaction, so a client retry is delivered again.
  • 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. This check does not run under ZDR. A payload that does not match the card Tavus issued is still a 409 on every conversation, including ZDR.
  • A retry of a saved interaction does not send another webhook. The webhook is sent when the interaction is first recorded. Replays, retries, and concurrent duplicates of that saved interaction do not send it again.
  • The same webhook can still arrive twice. In rare cases your endpoint receives the same event more than once. Treat two events as the same interaction when conversation_id and interaction_id match, and do not apply the second one again. An interaction_id is unique only within its conversation.
  • 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}.

Zero Data Retention

Zero Data Retention (ZDR) is an account-level setting that your Tavus account team turns on. For conversations that run under ZDR, Tavus delivers each interaction to you and writes no interaction record:
  • No interaction history. Nothing is stored for the interaction, so the history GET returns { "data": [] }.
  • Delivery by webhook and transcript. The canvas.interaction webhook carries each interaction in full, including value and metadata. The end-of-call application.transcription_ready transcript includes the text form of a submit, the words the renderer sends to the PAL for that answer, when it sends one. Only submits have a text form, and the transcript carries no interaction_id, so treat the webhook as your record of each interaction.
  • One delivery attempt. Tavus makes one attempt to deliver each event and doesn’t retry it. If delivery fails, for example because your endpoint is down, Tavus can’t resend the interaction or return it from the history GET; only its text form may still reach you in the transcript.
  • No reused-id check. With nothing stored, Tavus can’t tell a retry from a new interaction. A repeat under the same interaction_id returns 200 and is delivered again, even when the payload differs. The 409 for a reused interaction_id with a different payload doesn’t apply. A payload that does not match the card Tavus issued is still a 409. The Tavus-hosted embed and @tavus/cvi-ui retry a POST that fails or times out, so expect repeats. Treat two events as the same interaction when conversation_id and interaction_id match, and do not apply the second one again.
  • created_at is the accept time. It’s when Tavus accepted the POST, in the same format as a recorded interaction, so each repeat carries its own created_at.
  • A callback_url is required to receive answers. The webhook and the transcript both go to the conversation’s callback_url. Without one, the answers reach no one: Tavus delivers none of them and keeps no copy. Conversations started by the Tavus-hosted embed or widget have no callback_url, so to receive answers under ZDR, create the conversation through the API (POST /v2/conversations with a callback_url) and render it with @tavus/cvi-ui.
Everything else about the POST is unchanged: schema validation, rate limiting, the active-conversation check, and the issued-card 409 apply as for any conversation.