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

Промокод

Отправить виджет можно двумя способами:

  • готовый шаблон — одна строка в калькуляторе, вёрстка и логика уже сделаны;
  • свой виджет — вы пишете 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

Список вопросов, ответ раскрывается по нажатию.

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-код.

Свой виджет: оценка

Как это устроено

  1. Вы пишете виджет: один HTML-файл со стилями и скриптом внутри.
  2. Сохраняете его текстовым файлом .txt в разделе «Файлы» проекта и копируете ссылку. Файлы .html хранилище не принимает.
  3. Блок отправляет виджет по ссылке и передаёт начальные данные.
  4. Клиент нажимает в виджете — в диалог приходит колбэк с текстом, который задан в коде виджета.
  5. Блок-обработчик считает новые данные и отправляет их в виджет. Виджет перерисовывает себя сам.

Отправка

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, например из окна тестирования бота