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. 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.chart, canvas.alert.
This endpoint accepts only the component ids above.

Interaction Types

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

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_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}.