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.
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.
200:
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.
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_versionthat doesn’t match the component’s current version (v1). - An interaction type the component doesn’t allow (for example,
submitoncanvas.text). valueover 16 KB ormetadataover 4 KB serialized.- A NUL character (
U+0000) in any string or object key insidevalueormetadata. - A
valuethat breaks the component’ssubmitorskiprules; see the per-component rules above.
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_idandinteraction_idmatch, and do not apply the second one again. Aninteraction_idis 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
propertiesobject. - 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, the409 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 returns200, stores nothing new, and fires no second webhook. Safe to retry on timeouts. - “Identical” means
tool_call_id,component,component_version,type, andvalueall match.metadatais excluded; the first recordedmetadatais kept and delivered. - A reused
interaction_idwith 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 a409on 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_idandinteraction_idmatch, and do not apply the second one again. Aninteraction_idis 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 usesci_{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.interactionwebhook carries each interaction in full, includingvalueandmetadata. The end-of-callapplication.transcription_readytranscript 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 nointeraction_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_idreturns200and is delivered again, even when the payload differs. The409for a reusedinteraction_idwith a different payload doesn’t apply. A payload that does not match the card Tavus issued is still a409. The Tavus-hosted embed and@tavus/cvi-uiretry a POST that fails or times out, so expect repeats. Treat two events as the same interaction whenconversation_idandinteraction_idmatch, and do not apply the second one again. created_atis the accept time. It’s when Tavus accepted the POST, in the same format as a recorded interaction, so each repeat carries its owncreated_at.- A
callback_urlis required to receive answers. The webhook and the transcript both go to the conversation’scallback_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 nocallback_url, so to receive answers under ZDR, create the conversation through the API (POST /v2/conversationswith acallback_url) and render it with@tavus/cvi-ui.
409 apply as for any conversation.
