Let the user pick a date, a time slot, or a date range without leaving the conversation.
The calendar component displays a date or time picker mid-conversation. The user’s selection is delivered to your webhook as structured data: a YYYY-MM-DD string or an ISO-8601 slot.
The calendar shows only the dates and slots in the invocation; it does not fetch availability. For live booking against a Calendly scheduling page, use scheduling_embed.
calendar is enabled whenever the Magic Canvas skill is attached and has no component-specific configuration; see Enabling and configuring components.
The PAL is prompted to invoke canvas_show_calendar when the user should pick a date or time slot, inferring mode, multi_select, date_range, initial_month, and initial_year from the conversation. You don’t trigger the action directly.Slot times come from the PAL’s context. Provide availability through your system prompt, conversational context, or a tool from your Tools library.
The same fields appear in update_component patches; dates use YYYY-MM-DD.
Field
Type
Required
Description
title
string
❌
Heading on the card. 1–120 characters.
timezone
string
❌
IANA timezone name (e.g. America/Los_Angeles) used to present times. 1–120 characters.
mode
string
❌
date_picker, slot_picker, date_time, or range. If omitted: slots present → slot_picker, otherwise date_picker.
multi_select
boolean
❌
Slot-bearing modes only (slot_picker, date_time): let the user pick more than one slot. Defaults to false.
slots
array
✅ when mode is slot_picker or date_time
1–24 entries. Each slot: id (required), start (required, ISO-8601), end (required, ISO-8601), label (optional display text).
presets
array
❌
Up to 6 quick-pick buttons for date_picker. Each: label (required, ≤40 chars) and date (required, YYYY-MM-DD).
initial_date
string
❌
Pre-selected date, YYYY-MM-DD.
initial_month
string
❌
Month the calendar opens on, YYYY-MM.
initial_year
integer
❌
Year the calendar opens on, 1–9999.
date_range
object
❌
{ "start", "end" }, both YYYY-MM-DD, inclusive. Limits which dates are selectable.
highlighted_dates
array of strings
❌
Up to 62 YYYY-MM-DD dates to visually highlight.
allow_skip
boolean
❌
Shows a skip button so the user can decline to pick. Defaults to false.
layout
object
❌
Placement control available on every component action: { "preferred_slot": "safe-area-right" | "safe-area-left" }. Calendar cards default to safe-area-right.
display_mode
string
❌
"inline" (the only supported value). Available on every component action.
calendar produces six interaction types: submit, skip, dismiss, clear, error, and heartbeat. Each is delivered to your conversation webhook as a canvas.interaction event and is also available from GET https://tavusapi.com/v2/conversations/{conversation_id}/canvas/interactions.
Exactly one selection field is present and non-null, and skipped is false. The field depends on the card’s mode:
Mode
Selection field
date_picker
selected_date, a YYYY-MM-DD string
slot_picker / date_time
selected_slot, one slot object
slot_picker / date_time with multi_select
selected_slots, an array of one or more slot objects
range
selected_range, { "start", "end" }
Slot objects echo back exactly what the PAL offered: id, start, and end always; label when the invocation included one. Selection fields for other modes are null.
Branch on type first, then on whichever selection field is non-null:
app.post("/tavus-webhook", (req, res) => { const { event_type, properties } = req.body; if ( event_type !== "canvas.interaction" || properties.component !== "canvas.calendar" ) { return res.sendStatus(200); } const { type, value } = properties; if (type === "submit") { if (value.selected_date) { bookDate(value.selected_date); } else if (value.selected_slot) { bookSlot(value.selected_slot.start, value.selected_slot.end); } else if (value.selected_slots) { value.selected_slots.forEach((s) => bookSlot(s.start, s.end)); } else if (value.selected_range) { bookRange(value.selected_range.start, value.selected_range.end); } } else if (type === "skip" || type === "dismiss") { markDeclined(properties.conversation_id); } res.sendStatus(200);});
Tavus records each interaction once: a retried POST with the same interaction_id and identical payload never fires your webhook twice. Key non-reversible handlers on properties.interaction_id anyway.