Виджет — это интерактивный блок прямо в сообщении бота: промокод с кнопкой копирования, таймер акции, статус заказа, квиз, календарь для выбора даты и времени. Клиент нажимает внутри сообщения, бот меняет это же сообщение на месте — без новых сообщений и без отметки «изменено».

Отправить виджет можно двумя способами:
- готовый шаблон — одна строка в калькуляторе, вёрстка и логика уже сделаны;
- свой виджет — вы пишете HTML-код сами (или просите Сейла) и обрабатываете нажатия блоками воронки.
Где работают виджеты
| Мессенджер | только Telegram |
| Тип чата | только личный чат клиента с ботом |
| Не работают | группы, каналы, Telegram Business |
| Приложение клиента | свежая версия Telegram; в старой клиент увидит «сообщение не поддерживается» и подпись |
| Пересылка | виджет нельзя переслать и скопировать |
Внутри виджета нет полей ввода и клавиатуры: клиент может только нажимать. Если нужно, чтобы клиент что-то написал, — это обычный вопрос бота или Web App.
Важно! В окне тестирования бота в кабинете виджеты не показываются: это не Telegram. Проверяйте в самом Telegram, написав боту.
Готовые шаблоны
Все шаблоны отправляются одной функцией:
tg_send_widget(platform_id, шаблон, параметры, подпись)
| Параметр | Описание |
|---|---|
platform_id |
кому отправить; для текущего клиента — переменная platform_id
|
| шаблон | имя шаблона: promo, timer, order_status, faq, quiz, calendar
|
| параметры | словарь с настройками шаблона |
| подпись | текст под виджетом. Его же клиент увидит в списке чатов, в уведомлении и в старой версии Telegram, поэтому подпись должна быть понятна сама по себе |
Функция возвращает ответ Telegram. Номер отправленного сообщения — ответ['result']['message_id']; сохраните его, если собираетесь менять виджет позже.
Если вместо ответа Telegram вернулся текст — это ошибка: неизвестный шаблон, незнакомый или пропущенный параметр, слишком большой виджет. В тексте сказано, что именно не так.
Как собирать параметры
Собирайте словарь по одному ключу на строку:
p = {}
p["code"] = "WIDGET10"
p["title"] = "Ваш промокод"
p["note"] = "Скидка 10% на любой заказ"
w = tg_send_widget(platform_id, "promo", p, "Ваш промокод WIDGET10 — скидка 10%")
Код пишется в поле «Калькулятор» блока. На снимке — блок «Стартовое условие», который по слову «виджет заказ» отправляет виджет статуса заказа:

В блоке, который отправляет виджет, в поле ответа обычно ставят #{none}: блок сам ничего не пишет, текстом служит подпись виджета.
Язык
У шаблонов есть встроенные тексты: подписи кнопок, названия месяцев и дней недели, «Время вышло» и другие. По умолчанию они на русском. Другой язык задаётся параметром lang:
p["lang"] = "en"
Доступно: ru, en, de, es, fr, pt, tr, uk, kk, ms, th, ar.
Тексты, которые вы передаёте сами (заголовок, вопросы, этапы, подпись), показываются как есть.
Промокод — promo
Код и кнопка, которая копирует его одним нажатием.

| Параметр | Описание |
|---|---|
code |
сам промокод. Обязательный |
title |
строка над кодом |
note |
строка под кодом, например срок действия |
button |
текст кнопки. По умолчанию «Скопировать промокод» |
used_text |
текст, когда код отмечен использованным |
p = {}
p["code"] = "WIDGET10"
p["title"] = "Ваш промокод"
p["note"] = "Скидка 10% на любой заказ"
p["button"] = "Скопировать код"
w = tg_send_widget(platform_id, "promo", p, "Ваш промокод WIDGET10 — скидка 10%")
client.promo_widget_id = w['result']['message_id']
Отметить код использованным (он зачеркнётся, кнопка пропадёт):
p = {}
p["used"] = 1
t = tg_edit_widget_data(platform_id, client.promo_widget_id, p)
Таймер — timer
Обратный отсчёт до заданного момента. Считает на устройстве клиента, бот в этом не участвует.

| Параметр | Описание |
|---|---|
until |
до какого момента: "дд.мм.гггг чч:мм" по часовому поясу проекта |
seconds |
либо сколько секунд от момента отправки |
title |
заголовок. По умолчанию «До конца акции» |
done |
текст, когда время вышло |
text |
строка под отсчётом |
button, url
|
кнопка и ссылка, которую она открывает (только https://) |
hide_button |
1 — убрать кнопку, когда время вышло |
Нужен один из двух: until или seconds.
p = {}
p["seconds"] = 7200
p["title"] = "До конца акции"
p["button"] = "На сайт"
p["url"] = "https://salebot.pro"
w = tg_send_widget(platform_id, "timer", p, "Акция заканчивается через 2 часа")
Когда время выходит, в диалог ничего не приходит. Если по окончании акции бот должен что-то сделать, поставьте обычную стрелку с таймером.
Статус заказа — order_status
Список этапов с отметкой текущего. Бот двигает этап на месте — клиент видит одно и то же сообщение.

| Параметр | Описание |
|---|---|
title |
заголовок |
steps |
список этапов, от 1 до 12. По умолчанию: принят, собираем, передан курьеру, в пути, доставлен |
step |
номер текущего этапа, с единицы |
text |
строка под этапами |
Отправка:
p = {}
p["title"] = "Статус заказа"
p["steps"] = ["Принят", "Готовится", "Доставлен"]
p["step"] = 1
p["text"] = "Заказ принят, готовим к отправке"
w = tg_send_widget(platform_id, "order_status", p, "Ваш заказ")
client.order_widget_id = w['result']['message_id']
Переход на следующий этап — в любом другом блоке (смена этапа сделки, команда менеджера, таймер):
p = {}
p["step"] = 2
p["text"] = "Заказ готовится, скоро передадим курьеру"
t = tg_edit_widget_data(platform_id, client.order_widget_id, p)

Подпись под виджетом при смене этапа не меняется. Делайте её нейтральной («Ваш заказ»), а не «Статус: принят».
Частые вопросы — faq
Список вопросов, ответ раскрывается по нажатию.

| Параметр | Описание |
|---|---|
title |
заголовок |
items |
список пар «вопрос, ответ». Обязательный |
p = {}
p["title"] = "Частые вопросы"
p["items"] = [["Как оплатить?", "Картой или по СБП. Ссылка придёт в этот чат."], ["Сколько идёт доставка?", "По Москве 1–2 дня, по России 3–7 дней."], ["Можно ли вернуть товар?", "Да, в течение 14 дней."]]
w = tg_send_widget(platform_id, "faq", p, "Ответы на частые вопросы")
Текст показывается как есть: теги и разметка не применяются.
Квиз и опрос — quiz
Вопросы с вариантами ответа по одному на экране. С правильными ответами — квиз с подсчётом баллов, без них — опрос.

| Параметр | Описание |
|---|---|
title |
заголовок над вопросами |
questions |
список вопросов. Обязательный |
retry |
1 — показать в конце кнопку «Пройти ещё раз» |
callback |
свой текст колбэка вместо widget_quiz_finished
|
Вопрос — это словарь: q — текст, options — варианты, correct — номер правильного варианта с единицы или его текст.
p = {}
p["title"] = "Квиз: проверь себя"
p["retry"] = 1
p["questions"] = [{"q": "Сколько будет 2+2?", "options": ["3", "4", "5"], "correct": 2}, {"q": "Столица Франции?", "options": ["Лондон", "Париж", "Берлин"], "correct": "Париж"}]
w = tg_send_widget(platform_id, "quiz", p, "Пройди квиз из двух вопросов!")
Чтобы получился опрос без оценок, не указывайте correct ни в одном вопросе.
Правильные ответы проверяет платформа, на устройство клиента они не попадают.
Что приходит в воронку
Ответы на отдельные вопросы в диалог не попадают. Когда клиент ответил на последний вопрос, в диалог приходит колбэк widget_quiz_finished и заполняются переменные:
| Переменная | Значение |
|---|---|
widget_quiz_score |
сколько правильных ответов; у опроса пусто |
widget_quiz_total |
сколько всего вопросов |
widget_quiz_answers |
номера выбранных вариантов, например [2,2]
|
Поймать результат — стрелкой из блока, который отправил квиз, или блоком «Стартовое условие» с условием widget_quiz_finished:
Твой результат: #{widget_quiz_score} из #{widget_quiz_total} правильных ответов

Календарь — calendar
Сетка месяца: клиент выбирает дату, затем время. Заменяет страницы кнопок с датами и слотами и кнопки «назад» и «далее».

Календарь работает в трёх режимах.
Свои дата и время
Время всегда передаётся в параметре slots.
Одно и то же время каждый день — список времени плюс дни:
p = {}
p["title"] = "Выбор даты и времени"
p["name"] = "Консультация"
p["days"] = 10
p["slots"] = ["10:00", "12:00", "15:00"]
w = tg_send_widget(platform_id, "calendar", p, "Выберите дату и время")
Своё время на каждую дату — словарь «дата → список времени», даты берутся из ключей:
p = {}
p["slots"] = {"20.11.2026": ["10:00", "12:00"], "21.11.2026": ["15:00"]}
w = tg_send_widget(platform_id, "calendar", p, "Выберите дату и время")
| Параметр | Описание |
|---|---|
slots |
время: список или словарь по датам |
days |
сколько дней от сегодня доступно |
date_from, date_to
|
либо границы диапазона, "дд.мм.гггг"
|
dates |
либо список конкретных дат |
weekdays_off |
выходные дни недели: 1 — понедельник … 7 — воскресенье |
title |
подсказка под сеткой |
name |
что бронируется; показывается после выбора |
change |
1 — после выбора показать кнопку «Выбрать другое время» |
callback |
свой текст колбэка вместо widget_calendar_selected
|
Только дата
Не передавайте slots — клиент выберет только день:
p = {}
p["days"] = 45
p["weekdays_off"] = [7]
w = tg_send_widget(platform_id, "calendar", p, "Выберите дату")
Онлайн-запись
Передайте филиал и услугу — свободные даты и время календарь возьмёт из модуля «Услуги» сам:
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)
| Параметр | Описание |
|---|---|
company_id |
идентификатор филиала |
service_ids |
услуга или список услуг |
employee_id |
сотрудник; без него — любой свободный |
days_limit |
на сколько дней вперёд искать даты. По умолчанию 14 |
people_count |
сколько мест нужно в групповой услуге |
auto_booking |
1 — запись создаёт сама платформа (см. ниже) |
Параметры те же, что у функций get_dates_for_booking и get_slots_for_booking.
Что приходит в воронку
Нажатия на даты в диалог не попадают. Когда клиент выбрал время (или дату, если календарь без времени), приходит колбэк widget_calendar_selected и заполняются переменные:
| Переменная | Значение |
|---|---|
widget_date |
дата, дд.мм.гггг
|
widget_time |
время, чч:мм; у календаря без времени пусто |
widget_message_id |
номер сообщения с виджетом |

Форматы те же, что принимают create_booking и modify_booking_time.
Онлайн-запись с календарём: полный пример
Четыре блока вместо шести: даты, слоты и листание делает виджет.
Блок 1. Выбор услуги (блок «Стартовое условие», условие «записаться»). Ответ: «Выберите услугу:», в поле кнопок #{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']
Блок 2. Календарь. Ответ: #{none}. Стрелка из блока 1 с условием #{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)
Блок 3. Запись. Ответ: #{none}. Стрелка из блока 2 с условием widget_calendar_selected (полное совпадение).
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']
Блок 4. Успех. Стрелка из блока 3 без условия, по переменной booking_ok == True, через 1 секунду.
Вы записаны: #{serv_title}, #{booking_date} в #{booking_time}. Ждём вас!
Блок 5. Время уже заняли. Ответ: #{none}. Стрелка из блока 3 по переменной booking_ok != True, через 1 секунду. Блок возвращает тот же календарь к выбору и показывает в нём сообщение:
fix = {}
fix["reset"] = 1
fix["error"] = "Это время только что заняли — выберите другое"
t = tg_edit_widget_data(platform_id, widget_message_id, fix)
Из блока 5 — ещё одна стрелка в блок 3 с условием widget_calendar_selected: клиент выберет другое время в том же календаре.
Вариант короче: запись создаёт платформа
Добавьте в параметры календаря cal["auto_booking"] = 1. Тогда запись создаётся сразу при выборе времени, занятый слот показывается прямо в виджете, а колбэк widget_calendar_selected приходит только после успешной записи — вместе с переменной widget_booking_id. Блоки 3 и 5 не нужны.
Оставляйте запись в воронке (первый вариант), если нужна предоплата или свои проверки перед записью.
Изменение отправленного виджета
tg_edit_widget_data(platform_id, message_id, данные)
| Параметр | Описание |
|---|---|
message_id |
номер сообщения с виджетом: сохранённый при отправке или widget_message_id после нажатия |
| данные | словарь с новыми значениями |
Меняются только данные. Вёрстку отправленного виджета изменить нельзя — для другого вида отправьте новый виджет.
Как применяются данные:
-
Промокод, таймер, статус заказа, FAQ. Новые данные заменяют предыдущую правку целиком. Чего в данных нет, то берётся из параметров отправки. Поэтому передавайте все поля, которые должны отличаться от исходных: и
step, иtext. - Квиз и календарь. Данные накладываются на текущее состояние.
Особые данные:
| Шаблон | Данные | Что произойдёт |
|---|---|---|
promo |
{"used": 1} |
код отмечен использованным |
timer |
{"seconds": 600} или {"until": "…"}
|
отсчёт перезапущен |
calendar |
{"reset": 1, "error": "текст"} |
выбор снят, календарь снова открыт, показано сообщение |
Свой виджет
Когда готовых шаблонов не хватает — каталог с корзиной, оценка звёздами, калькулятор стоимости, — можно отправить собственный HTML-код.

Как это устроено
- Вы пишете виджет: один HTML-файл со стилями и скриптом внутри.
- Сохраняете его текстовым файлом
.txtв разделе «Файлы» проекта и копируете ссылку. Файлы.htmlхранилище не принимает. - Блок отправляет виджет по ссылке и передаёт начальные данные.
- Клиент нажимает в виджете — в диалог приходит колбэк с текстом, который задан в коде виджета.
- Блок-обработчик считает новые данные и отправляет их в виджет. Виджет перерисовывает себя сам.
Отправка
tg_send_widget_file(platform_id, ссылка, данные, подпись)
st = {}
st["rating"] = 0
w = tg_send_widget_file(platform_id, "https://files.salebot.pro/…/rating.txt", st, "Оцените заказ — нажмите на звезду")
данные — словарь, из которого виджет рисует себя в первый раз.
Обработка нажатий
Нажатие приходит в диалог колбэком. Вместе с ним заполняются переменные:
| Переменная | Значение |
|---|---|
widget_data |
текст запроса из виджета, например rate:4
|
widget_message_id |
номер сообщения, в котором нажали |
widget_query_id |
идентификатор запроса |
Обработчик делайте блоком «Не состояние с условием» с включённой настройкой «Срабатывать только на Callback». Условие — текст запроса. Такой блок срабатывает, где бы ни находился клиент, и не сдвигает его с текущего шага.
Важно. Не делайте обработчик блоком «Стартовое условие». Виджет остаётся в чате, и клиент может нажать на него в любой момент — посреди анкеты, записи или оплаты. «Стартовое условие» перенесёт клиента в обработчик, и шаг, на котором он стоял, перестанет отвечать.
Пример обработчика для оценки. Условие — регулярное выражение ^rate:[1-5]$:
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)
Данные своего виджета заменяются целиком: передавайте всё состояние, а не только изменившееся поле. Размер данных — до 1024 байт, поэтому большие неизменные списки (каталог товаров) держите в самом коде виджета, а в данных — только то, что меняется.
Ответ виджету
По умолчанию виджет сразу получает пустой ответ, а новые данные приходят ему правкой.
Если виджету нужны данные именно в ответе на запрос, текст запроса в коде должен начинаться с ? — например ?price. Тогда блок-обработчик обязан за 15 секунд вызвать:
tg_answer_widget_query(данные)
Иначе виджет покажет ошибку ожидания. Размер ответа — до 1024 байт.
Правила кода
- Один файл: стили в
<style>, код в<script>. Внешние скрипты, стили, шрифты и картинки не загружаются; картинки — только встроенные (data:) или SVG. - Размер — до 32 КБ и до 4 КБ после сжатия.
- Нет полей ввода:
input,select,textarea,formвырезаются. Из элементов управления — только<button>. - Нет сети:
fetchи подобные не работают. Единственная связь с ботом —TelegramWidget.request("текст"), от 1 до 64 байт. - Нет прокрутки внутри виджета. Высота до 600 пикселей, лишнее обрезается. Карусель делается сдвигом ряда карточек, а не прокруткой.
- Элементы ищите через
document.getElementById: обращение к элементу поidкак к переменной не работает. - Цвета берите из темы Telegram (
var(--tg-theme-text-color)и другие), тогда виджет выглядит правильно в светлой и тёмной теме. - Данные читайте из
TelegramWidget.chatDataи перерисовывайте виджет по событиюchatDataChanged. -
requestвызывается прямо в обработчике нажатия, один запрос на одно нажатие.
Ошибок виджет не показывает: неподдерживаемое молча вырезается, а при ошибке в скрипте остаётся пустой блок. Проверяйте виджет в Telegram после каждой правки.
Минимальный пример — кнопка, которая отправляет запрос, и счётчик из данных:
<style>body{margin:0;padding:14px}.h{color:var(--tg-theme-hint-color);font-size:.85em}
button{width:100%;margin-top:8px}</style>
<b>Маргарита · 590 ₽</b><div class="h" id="c"></div>
<button id="add">В корзину</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='В корзине: '+(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='Ошибка: '+x.code})};
W.onEvent('chatDataChanged',draw);draw();
</script>
Сейл и виджеты
Сейл умеет собирать сценарии с виджетами сам. Напишите ему, что нужно, обычными словами:
- «По слову „заказ“ отправляй виджет статуса заказа из трёх этапов, а по слову „готово“ переводи на второй»;
- «Сделай запись на услуги: услуга кнопками, а дату и время клиент выбирает в календаре-виджете»;
- «Сделай виджет оценки заказа звёздами, оценку сохраняй в переменную клиента».
Готовые шаблоны Сейл подставляет сам. Свой виджет он пишет, проверяет код и сохраняет файл в проект, а затем создаёт блоки отправки и обработки нажатий.
Функции
| Функция | Что делает |
|---|---|
tg_send_widget(platform_id, template, params, caption) |
отправляет готовый шаблон |
tg_send_widget_file(platform_id, url, data, caption) |
отправляет свой виджет из файла |
tg_edit_widget_data(platform_id, message_id, data) |
меняет данные отправленного виджета |
tg_answer_widget_query(data) |
отвечает своему виджету на запрос, начинающийся с ?
|
Колбэки и переменные
| Событие | Колбэк | Переменные |
|---|---|---|
| Квиз пройден | widget_quiz_finished |
widget_quiz_score, widget_quiz_total, widget_quiz_answers
|
| В календаре выбраны дата и время | widget_calendar_selected |
widget_date, widget_time; при auto_booking ещё widget_booking_id
|
| Нажатие в своём виджете | текст запроса из кода виджета |
widget_data, widget_message_id, widget_query_id
|
Если в боте два квиза или два календаря, задайте каждому свой текст колбэка параметром callback, чтобы различать их в условиях.
Ограничения
- Только личный чат с ботом в Telegram.
- У клиента хранятся пять последних квизов и календарей: более старый перестаёт отвечать на нажатия.
- Очистка всех переменных клиента в сценарии отключает уже отправленные квизы и календари.
- Данные виджета — до 1024 байт, запрос из своего виджета — до 64 байт.
- На ответ своему виджету через
tg_answer_widget_queryесть 15 секунд.
Частые ошибки
| Что видно | Причина |
|---|---|
Виджет не пришёл, в переменной текст Unknown parameter…
|
в параметрах шаблона незнакомый ключ; в тексте перечислены допустимые |
| Календарь завершается на дате, без времени | не передан slots
|
| После нажатия на виджет бот перестал отвечать на кнопки текущего шага | обработчик нажатия сделан блоком «Стартовое условие» — замените на «Не состояние с условием» |
| Свой виджет пустой | ошибка в коде виджета; чаще всего обращение к элементу по id без document.getElementById
|
| В Telegram «сообщение не поддерживается» | у клиента старая версия приложения |
Bad Request: chat not found |
виджет отправлен не в Telegram, например из окна тестирования бота |


