A widget is an interactive component embedded directly in a bot message: a promo code with a copy button, a countdown timer, an order status tracker, a quiz, or a calendar for selecting a date and time. When a customer interacts with a widget, the bot updates the same message in place — without sending new messages or displaying an "edited" label.

There are two ways to send a widget:
- Ready-made template — a single line in the Calculator. The layout and functionality are already implemented.
- Custom widget — write your own HTML code (or ask Mavi to create it) and process interactions using bot flow blocks.
Where Widgets Work
| Messenger | Telegram only |
| Chat type | Private chats between a customer and a bot only |
| Not supported | Groups, channels, Telegram Business |
| Telegram app | Requires a recent version of Telegram. Older versions display an "Unsupported message" notice and the caption |
| Forwarding | Widgets cannot be forwarded or copied |
Widgets do not support text input fields or keyboards. Customers can only interact by tapping. If you need customers to enter text, use a regular bot question or a Web App.
Widgets are not displayed in the bot testing window in the dashboard because it is not Telegram. Test widgets directly in Telegram by messaging your bot.
Ready-Made Templates
All templates are sent using a single function:
tg_send_widget(platform_id, template, params, caption)
| Parameter | Description |
|---|---|
platform_id |
Recipient ID. For the current customer, use the platform_id variable |
template |
Template name: promo, timer, order_status, faq, quiz, calendar
|
params |
Dictionary containing template settings |
caption |
Text displayed below the widget. Customers also see it in their chat list, notifications, and older Telegram versions, so it should be meaningful on its own |
The function returns the Telegram API response. The sent message ID is available at response['result']['message_id']. Save it if you plan to update the widget later.
If the function returns text instead of a Telegram response, an error has occurred: an unknown template, an invalid or missing parameter, or a widget exceeding the size limit. The error text explains what went wrong.
How to Build Parameters
Create a dictionary and assign each parameter on a separate line:
p = {}
p["code"] = "WIDGET10"
p["title"] = "Your promo code"
p["note"] = "10% off any order"
w = tg_send_widget(platform_id, "promo", p, "Your promo code WIDGET10 — 10% discount")
Enter the code in the block's Calculator field. The screenshot shows a Start Condition block that sends an order status widget when the customer sends the phrase "order widget".

In the block that sends the widget, you would typically set the response field to #{none}. The block does not send a separate text message because the widget caption serves as the message text.
Language
Templates include built-in interface text, such as button labels, month and weekday names, "Time's up", and other messages.
The default language in Mavibot is English (en).
To use another language, specify the lang parameter:
p["lang"] = "de"
Supported languages:
ru, en, de, es, fr, pt, tr, uk, kk, ms, th, ar.
Texts you provide yourself — titles, questions, steps, and captions — are displayed exactly as entered and are not automatically translated.
Promo Code — promo
Displays a promo code with a button that copies it with one tap.

| Parameter | Description |
|---|---|
code |
Promo code. Required |
title |
Text displayed above the code |
note |
Text displayed below the code, such as an expiration date |
button |
Button label. Default: "Copy promo code" |
used_text |
Text displayed when the code is marked as used |
p = {}
p["code"] = "WIDGET10"
p["title"] = "Your promo code"
p["note"] = "10% off any order"
p["button"] = "Copy code"
w = tg_send_widget(platform_id, "promo", p, "Your promo code WIDGET10 — 10% discount")
client.promo_widget_id = w['result']['message_id']
To mark the code as used, which strikes through the code and removes the button:
p = {}
p["used"] = 1
t = tg_edit_widget_data(platform_id, client.promo_widget_id, p)
Countdown Timer — timer
Displays a countdown to a specified date and time.
The countdown runs on the customer's device. The bot is not involved in updating the timer.

| Parameter | Description |
|---|---|
until |
Target date and time in "DD.MM.YYYY HH:MM" format, using the project's time zone |
seconds |
Alternatively, the number of seconds from the moment the widget is sent |
title |
Timer heading. Default: "Offer ends in" |
done |
Text displayed when the countdown ends |
text |
Text displayed below the countdown |
button, url
|
Button label and destination URL (https:// only) |
hide_button |
Set to 1 to hide the button when the countdown ends |
You must provide either until or seconds.
p = {}
p["seconds"] = 7200
p["title"] = "Offer ends in"
p["button"] = "Visit website"
p["url"] = "https://salebot.pro"
w = tg_send_widget(platform_id, "timer", p, "The offer ends in 2 hours")
When the countdown reaches zero, no message or callback is sent to the bot. If the bot needs to perform an action when the offer expires, use a regular timer connection in the bot flow.
Order Status — order_status
Displays a list of order stages with the current stage highlighted.
The bot can update the stage in place, so the customer always sees the same message.

| Parameter | Description |
|---|---|
title |
Widget title |
steps |
List of stages, from 1 to 12. Default: accepted, preparing, handed to courier, on the way, delivered |
step |
Current stage number, starting from 1 |
text |
Text displayed below the stages |
Sending the widget:
p = {}
p["title"] = "Order status"
p["steps"] = ["Accepted", "Preparing", "Delivered"]
p["step"] = 1
p["text"] = "Your order has been accepted and is being prepared"
w = tg_send_widget(platform_id, "order_status", p, "Your order")
client.order_widget_id = w['result']['message_id']
Move to the next stage from any other block, such as when a deal stage changes, a manager sends a command, or a timer triggers:
p = {}
p["step"] = 2
p["text"] = "Your order is being prepared and will be handed to the courier soon"
t = tg_edit_widget_data(platform_id, client.order_widget_id, p)

The caption below the widget does not change when the stage is updated. Use a neutral caption such as "Your order" instead of "Status: Accepted".
Frequently Asked Questions — faq
Displays a list of questions. Customers can tap a question to expand its answer.

| Parameter | Description |
|---|---|
title |
Widget title |
items |
List of question-and-answer pairs. Required |
p = {}
p["title"] = "Frequently Asked Questions"
p["items"] = [["How can I pay?", "You can pay by card. A payment link will be sent to this chat."], ["How long does delivery take?", "Delivery usually takes 1–2 days locally and 3–7 days nationwide."], ["Can I return an item?", "Yes, within 14 days."]]
w = tg_send_widget(platform_id, "faq", p, "Answers to frequently asked questions")
Text is displayed as plain text. HTML tags and other markup are not processed.
Quiz and Survey — quiz
Displays questions with multiple-choice answers, one question at a time.
If correct answers are specified, the widget acts as a quiz and calculates a score. Without correct answers, it works as a survey.

| Parameter | Description |
|---|---|
title |
Heading displayed above the questions |
questions |
List of questions. Required |
retry |
Set to 1 to display a "Try again" button at the end |
callback |
Custom callback text instead of widget_quiz_finished
|
Each question is a dictionary containing:
-
q— question text -
options— answer options -
correct— correct answer, specified by its 1-based index or exact text
p = {}
p["title"] = "Quiz: Test Your Knowledge"
p["retry"] = 1
p["questions"] = [{"q": "What is 2 + 2?", "options": ["3", "4", "5"], "correct": 2}, {"q": "What is the capital of France?", "options": ["London", "Paris", "Berlin"], "correct": "Paris"}]
w = tg_send_widget(platform_id, "quiz", p, "Take this two-question quiz!")
To create a survey without scoring, omit correct from every question.
Correct answers are checked by the platform and are not sent to the customer's device.
What the Bot Flow Receives
Individual question answers are not sent to the conversation.
When the customer answers the last question, the bot receives the widget_quiz_finished callback and the following variables are populated:
| Variable | Value |
|---|---|
widget_quiz_score |
Number of correct answers; empty for surveys |
widget_quiz_total |
Total number of questions |
widget_quiz_answers |
Indices of selected options, such as [2,2]
|
To process the result, create a connection from the block that sent the quiz or use a Start Condition block with the condition widget_quiz_finished.
Example response:
Your result: #{widget_quiz_score} out of #{widget_quiz_total} correct answers

Calendar — calendar
Displays a monthly calendar where customers can select a date and then a time slot.
It replaces multiple pages of date and time buttons, including "Back" and "Next" navigation.

The calendar supports three modes.
Custom Dates and Times
Available times are always passed through the slots parameter.
Same time slots every day — provide a list of times and a date range:
p = {}
p["title"] = "Select a date and time"
p["name"] = "Consultation"
p["days"] = 10
p["slots"] = ["10:00", "12:00", "15:00"]
w = tg_send_widget(platform_id, "calendar", p, "Choose a date and time")
Different time slots for specific dates — provide a dictionary mapping dates to time lists. Available dates are taken from the dictionary keys:
p = {}
p["slots"] = {"20.11.2026": ["10:00", "12:00"], "21.11.2026": ["15:00"]}
w = tg_send_widget(platform_id, "calendar", p, "Choose a date and time")
| Parameter | Description |
|---|---|
slots |
Available times, as a list or a dictionary grouped by date |
days |
Number of available days starting from today |
date_from, date_to
|
Alternatively, date range boundaries in "DD.MM.YYYY" format |
dates |
Alternatively, a list of specific dates |
weekdays_off |
Unavailable weekdays: 1 = Monday through 7 = Sunday |
title |
Hint displayed below the calendar grid |
name |
Name of the item or service being booked; displayed after selection |
change |
Set to 1 to display a "Choose another time" button after selection |
callback |
Custom callback text instead of widget_calendar_selected
|
Date Only
Do not pass slots if the customer only needs to select a date.
p = {}
p["days"] = 45
p["weekdays_off"] = [7]
w = tg_send_widget(platform_id, "calendar", p, "Choose a date")
Online Booking
Provide a branch and service, and the calendar will automatically retrieve available dates and time slots from the Services module.
cal = {}
cal["company_id"] = comp_id
cal["service_ids"] = serv_id
cal["days_limit"] = 30
cal["name"] = serv_title
w = tg_send_widget(platform_id, "calendar", cal, serv_title)
| Parameter | Description |
|---|---|
company_id |
Branch identifier |
service_ids |
Service or list of services |
employee_id |
Employee identifier; if omitted, any available employee is used |
days_limit |
Number of days ahead to search for availability. Default: 14 |
people_count |
Number of spots required for a group service |
auto_booking |
Set to 1 to let the platform create the booking automatically |
These parameters are the same as those used by get_dates_for_booking and get_slots_for_booking.
What the Bot Flow Receives
Individual date selections are not sent to the conversation.
When the customer selects a time slot, or a date if the calendar has no time slots, the bot receives the widget_calendar_selected callback and the following variables are populated:
| Variable | Value |
|---|---|
widget_date |
Selected date in DD.MM.YYYY format |
widget_time |
Selected time in HH:MM format; empty for date-only calendars |
widget_message_id |
ID of the message containing the widget |

The date and time formats are the same as those accepted by create_booking and modify_booking_time.
Online Booking with a Calendar: Complete Example
The widget handles date selection, time slots, and navigation, reducing the number of required bot blocks.
Block 1. Service Selection
Use a Start Condition block with the condition "book an appointment".
Response: "Choose a service:"
Set the buttons field to #{srv_buttons}.
comp_id = 8
srv_data = get_services_for_booking(comp_id)
srv_kb = tools_make_button_str_checker(srv_data, 'title', 1)
srv_buttons = srv_kb['buttons']
srv_checker = srv_kb['checker']
Block 2. Calendar
Response: #{none}.
Connect Block 1 to Block 2 using the condition #{srv_checker}.
serv_id = tools_check_user_input(srv_data, question, 'title', 'id')
serv_title = tools_check_user_input(srv_data, question, 'title', 'title')
cal = {}
cal["company_id"] = comp_id
cal["service_ids"] = serv_id
cal["days_limit"] = 30
cal["name"] = serv_title
cal_sent = tg_send_widget(platform_id, "calendar", cal, serv_title)
Block 3. Create Booking
Response: #{none}.
Connect Block 2 to Block 3 using the condition widget_calendar_selected with Exact match.
booking_date = widget_date
booking_time = widget_time
booking_result = create_booking(comp_id, serv_id, booking_date, booking_time, '')
booking_ok = booking_result['status']
Block 4. Booking Confirmed
Connect Block 3 to Block 4 using the variable condition booking_ok == True with a one-second delay.
Response:
Your appointment is confirmed: #{serv_title}, #{booking_date} at #{booking_time}. See you soon!
Block 5. Time Slot No Longer Available
Response: #{none}.
Connect Block 3 to Block 5 using the variable condition booking_ok != True with a one-second delay.
This block resets the existing calendar, allowing the customer to select another time and displaying an error message.
fix = {}
fix["reset"] = 1
fix["error"] = "This time slot has just been booked. Please choose another one."
t = tg_edit_widget_data(platform_id, widget_message_id, fix)
Add another connection from Block 5 to Block 3 with the condition widget_calendar_selected.
The customer can now select another time in the same calendar.
Simpler Option: Automatic Booking
Add the following parameter to the calendar settings:
cal["auto_booking"] = 1
The platform will create the booking immediately when the customer selects a time.
If the selected time slot becomes unavailable, the widget displays an error directly.
The widget_calendar_selected callback is sent only after a successful booking, along with the widget_booking_id variable.
With this approach, Blocks 3 and 5 are not required.
Use booking through the bot flow (the first approach) if you need prepayment or custom checks before confirming an appointment.
Updating a Sent Widget
Use the following function to update widget data:
tg_edit_widget_data(platform_id, message_id, data)
| Parameter | Description |
|---|---|
message_id |
ID of the widget message, saved when sending it or obtained from widget_message_id after an interaction |
data |
Dictionary containing the new values |
Only widget data can be updated. You cannot change the layout of an already sent widget. To use a different layout, send a new widget.
How updates are applied:
-
Promo code, timer, order status, FAQ: the new data completely replaces the previous update. Any missing fields fall back to the original values provided when the widget was sent. Therefore, include every field that should differ from the initial data, such as both
stepandtext. - Quiz and calendar: new data is applied on top of the current widget state.
Special update parameters:
| Template | Data | Result |
|---|---|---|
promo |
{"used": 1} |
Marks the promo code as used |
timer |
{"seconds": 600} or {"until": "…"}
|
Restarts the countdown |
calendar |
{"reset": 1, "error": "message"} |
Clears the selection, reopens the calendar, and displays a message |
Custom Widgets
When ready-made templates are not enough — for example, if you need a product catalog with a shopping cart, a star rating, or a price calculator — you can create a custom HTML widget.

How It Works
- Create a widget as a single HTML file containing its styles and JavaScript.
- Save the code as a
.txtfile in the project's Files section and copy the file URL. The storage system does not accept.htmlfiles. - A bot block sends the widget using the URL and passes its initial data.
- When a customer interacts with the widget, the bot receives a callback containing the text defined in the widget code.
- A handler block calculates the new data and sends it back to the widget, which updates its own interface.
Sending a Custom Widget
tg_send_widget_file(platform_id, url, data, caption)
Example:
st = {}
st["rating"] = 0
w = tg_send_widget_file(platform_id, "https://files.mavibot.ai/…/rating.txt", st, "Rate your order — tap a star")
The data parameter is a dictionary containing the initial state used to render the widget.
Handling Interactions
When a customer interacts with a custom widget, the bot receives a callback.
The following variables are also populated:
| Variable | Value |
|---|---|
widget_data |
Request text sent by the widget, such as rate:4
|
widget_message_id |
ID of the message where the interaction occurred |
widget_query_id |
Request identifier |
Create the handler using a Trigger block and enable the Trigger only on Callback setting.
The condition should match the widget request text.
This type of block works regardless of the customer's current position in the bot flow and does not move them away from their current step.
Important: Do not use a Start Condition block to handle custom widget interactions. A widget remains in the chat, and the customer can interact with it at any time — even while completing a questionnaire, booking an appointment, or making a payment. A Start Condition block would redirect the customer to the handler and interrupt their current step.
Example rating handler:
Condition (regular expression):
^rate:[1-5]$
Calculator:
local a = splitter(widget_data, ":")
client.widget_rating = a[1]
st = {}
st["rating"] = client.widget_rating
t = tg_edit_widget_data(platform_id, widget_message_id, st)
Custom widget data is replaced entirely during updates. Always send the complete state rather than only the changed field.
The data size limit is 1024 bytes.
Keep large, static datasets (such as product catalogs) inside the widget code, and send only the values that change through widget data.
Responding to a Widget Request
By default, the widget immediately receives an empty response, and updated data arrives through a separate widget update.
If the widget needs to receive data directly in response to its request, the request text must begin with ?, for example:
?price
In this case, the handler block must call the following function within 15 seconds:
tg_answer_widget_query(data)
Otherwise, the widget displays a timeout error.
The response size limit is 1024 bytes.
Code Requirements
-
Single file: include styles inside
<style>and JavaScript inside<script>. External scripts, stylesheets, fonts, and images cannot be loaded. Images must be embedded usingdata:URLs or SVG. - Size limits: maximum 32 KB before compression and 4 KB after compression.
-
No input fields:
input,select,textarea, andformelements are removed. Only<button>elements are supported as interactive controls. -
No network requests:
fetchand similar APIs do not work. The only way to communicate with the bot is throughTelegramWidget.request("text"), with a request size of 1–64 bytes. - No internal scrolling: maximum widget height is 600 pixels. Anything beyond that is clipped. To create a carousel, move a row of cards rather than using scrolling.
-
Element access: use
document.getElementById. Accessing elements as global variables based on their IDs does not work. -
Telegram theme: use Telegram theme variables such as
var(--tg-theme-text-color)so the widget looks correct in both light and dark themes. -
Widget data: read data from
TelegramWidget.chatDataand redraw the widget when thechatDataChangedevent occurs. -
Requests: call
requestdirectly inside a user interaction handler. Only one request is allowed per interaction.
The widget does not display coding errors. Unsupported elements are silently removed, and a JavaScript error may result in an empty widget. Always test the widget in Telegram after making changes.
Minimal Custom Widget Example
The example below displays a product, a button that sends a request to the bot, and a cart counter based on widget data.
<style>
body{margin:0;padding:14px}
.h{color:var(--tg-theme-hint-color);font-size:.85em}
button{width:100%;margin-top:8px}
</style>
<b>Margherita · $10</b>
<div class="h" id="c"></div>
<button id="add">Add to cart</button>
<div class="h" id="e"></div>
<script>
var W=window.TelegramWidget,$=function(i){return document.getElementById(i)},busy=0;
function draw(){
var s={};
try{s=JSON.parse(W.chatData)||{}}catch(e){}
$('c').textContent='In cart: '+(s.count||0)
}
$('add').onclick=function(){
if(busy)return;
busy=1;
$('e').textContent='';
W.request('cart_add').then(
function(){busy=0},
function(x){
busy=0;
$('e').textContent='Error: '+x.code
}
)
};
W.onEvent('chatDataChanged',draw);
draw();
</script>
Mavi and Widgets
Mavi can create complete bot flows with widgets automatically.
Simply describe what you need in plain language.
For example:
- "When the customer sends 'order', display an order status widget with three stages. When they send 'ready', move the order to the second stage."
- "Create a service booking flow: customers choose a service using buttons, then select a date and time using a calendar widget."
- "Create a star rating widget for orders and save the selected rating to a client variable."
Mavi automatically configures ready-made templates.
For custom widgets, Mavi writes and checks the code, saves the widget file to the project, and creates the blocks needed to send the widget and handle customer interactions.
Reference
Functions
| Function | Description |
|---|---|
tg_send_widget(platform_id, template, params, caption) |
Sends a ready-made widget template |
tg_send_widget_file(platform_id, url, data, caption) |
Sends a custom widget from a file |
tg_edit_widget_data(platform_id, message_id, data) |
Updates data in an existing widget |
tg_answer_widget_query(data) |
Responds to a custom widget request beginning with ?
|
Callbacks and Variables
| Event | Callback | Variables |
|---|---|---|
| Quiz completed | widget_quiz_finished |
widget_quiz_score, widget_quiz_total, widget_quiz_answers
|
| Date and time selected in calendar | widget_calendar_selected |
widget_date, widget_time; also widget_booking_id when auto_booking is enabled |
| Custom widget interaction | Request text defined in the widget code |
widget_data, widget_message_id, widget_query_id
|
If your bot contains multiple quizzes or calendars, assign a unique callback to each widget using the callback parameter so you can distinguish them in your bot flow conditions.
Limitations
- Widgets work only in private Telegram chats between a customer and a bot.
- Only the five most recent quizzes and calendars are retained for each customer. Older ones stop responding to interactions.
- Clearing all client variables in a bot flow disables previously sent quizzes and calendars.
- Widget data is limited to 1024 bytes.
- Requests from custom widgets are limited to 64 bytes.
- Custom widget requests requiring
tg_answer_widget_querymust be answered within 15 seconds.
Troubleshooting
| Problem | Cause / Solution |
|---|---|
Widget is not sent and the variable contains Unknown parameter…
|
The template received an unsupported parameter. The error message lists the allowed parameters |
| Calendar stops after date selection without showing time slots | The slots parameter was not provided |
| After interacting with a widget, the bot stops responding to buttons in the current step | The interaction handler uses a Start Condition block. Replace it with a Non-State Block with Condition |
| Custom widget appears empty | There is an error in the widget code. A common cause is accessing an element by its ID without using document.getElementById
|
| Telegram displays "Unsupported message" | The customer is using an outdated version of Telegram |
Bad Request: chat not found |
The widget was sent outside Telegram, for example from the bot testing window |