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.

Promo code

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

Widget in the bot builder

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.

Promo code

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.

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

Order status: first stage

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)

Order status: second stage

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.

FAQ widget

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.

Quiz question

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

Quiz result and bot response


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.

Calendar: selecting date and time

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

Calendar: selection completed

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 step and text.
  • 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.

Custom widget: star rating

How It Works

  1. Create a widget as a single HTML file containing its styles and JavaScript.
  2. Save the code as a .txt file in the project's Files section and copy the file URL. The storage system does not accept .html files.
  3. A bot block sends the widget using the URL and passes its initial data.
  4. When a customer interacts with the widget, the bot receives a callback containing the text defined in the widget code.
  5. 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 using data: URLs or SVG.
  • Size limits: maximum 32 KB before compression and 4 KB after compression.
  • No input fields: input, select, textarea, and form elements are removed. Only <button> elements are supported as interactive controls.
  • No network requests: fetch and similar APIs do not work. The only way to communicate with the bot is through TelegramWidget.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.chatData and redraw the widget when the chatDataChanged event occurs.
  • Requests: call request directly 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_query must 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