Спасаем бизнес: Создаём VK-бота на PHP, Python, JS и C#. Часть 3 — Кнопки и клавиатуры: делаем бота удобным
В прошлых частях мы научили бота реагировать на сообщения и отвечать пользователю. Но текстовый интерфейс — это как разговор с глухим: пользователь
должен угадать, что можно написать, и каждый раз набирать команды руками. Это неудобно, медленно и повышает порог входа.
Можно, конечно
подключить к ответам нейросеть или создать огромный массив фраз, на которые отвечает пользователь, но мы немного упростим задачу.
VK API
предоставляет два способа сделать общение с ботом интуитивным: обычные клавиатуры, которые прикрепляются к полю ввода и заменяют собой клавиатуру
телефона, и inline-клавиатуры, которые крепятся к конкретному сообщению и остаются рядом с ним. Первые хороши для пошаговых сценариев (главное меню,
формы), вторые — для контекстных действий (кнопки «Лайк», «Подробнее» под постом).
В этой части мы разберём оба типа: научимся создавать
клавиатуры, настраивать их внешний вид, обрабатывать нажатия и выбирать подходящий сценарий для каждой задачи. Все примеры — снова на четырёх
языках: PHP, Python, JavaScript и C#.
Предварительная настройка: готовим сообщество к работе с клавиатурами
Прежде чем погружаться в код, убедимся, что ваше сообщество настроено правильно. Клавиатуры — это расширенная функциональность, и для их работы нужно выполнить несколько простых шагов:
-
Сообщения сообщества включены
Перейдите в Управление сообществом → Сообщения → включите опцию «Сообщения сообщества». Без этого бот просто не сможет отправлять или получать сообщения, а значит, и клавиатуры не появятся.

-
Токен с нужными правами
В первой части мы создавали ключ доступа. Убедитесь, что при его создании вы отметили право «Сообщения сообщества» (и, если планируете использовать бота в беседах, также «Доступ к сообщениям»). Если токен был создан без этих прав, клавиатуры могут не отправляться.
-
Кнопка «Начать» (опционально)
В настройках сообщества есть раздел «Сообщения» → «Настройка для бота». Там можно включить кнопку «Начать». Она позволяет пользователю инициировать диалог одним касанием, после чего бот может сразу показать приветственное меню. Клавиатура будет работать и без этой кнопки, но с ней пользовательский опыт становится плавнее.
Создание обычной (встроенной) клавиатуры
Доработка функции отправки сообщения
Обычная клавиатура — это панель с кнопками, которая появляется вместо стандартной клавиатуры телефона внизу экрана. Она остаётся на месте (если не
установлен флаг one_time) и позволяет пользователю взаимодействовать с ботом без набора текста. Чтобы отправить такую клавиатуру, нужно при вызове
метода messages.send передать параметр keyboard — JSON-строку определённой структуры.
Доработаем нашу функцию отправки сообщения:
// Добавим в параметры необязательный аргумент - клавиатуру function sendMessage($peerId, $message, $keyboard = null){ $params = [ 'peer_id' => $peerId, 'message' => $message, 'access_token' => ACCESS_TOKEN, 'v' => VERSION, 'random_id' => rand(1, 1000000), ];// Если аргумент передан, добавляем новое поле if ($keyboard) { // Обязательно сериализуем в json $params['keyboard'] = json_encode($keyboard); }$url = ENDPOINT . 'messages.send?' . http_build_query($params); $response = file_get_contents($url); return json_decode($response); }
# Добавим в параметры необязательный аргумент - клавиатуру def send_message(peer_id, message, keyboard = None):params = { 'peer_id': peer_id, 'message': message, 'access_token': ACCESS_TOKEN, 'v': VERSION, 'random_id': random.randint(1, 1000000) }# Если аргумент передан, добавляем новое поле if keyboard: # Обязательно сериализуем в json params['keyboard'] = json.dumps(keyboard, ensure_ascii=False)url = ENDPOINT + 'messages.send' response = requests.get(url, params=params) return response.json()
// Добавим в параметры необязательный аргумент - клавиатуру async function sendMessage(peerId, message, keyboard) {const params = { peer_id: peerId, message: message, access_token: ACCESS_TOKEN, v: VERSION, random_id: Math.floor(Math.random() * 1000000) + 1, };// Если аргумент передан, добавляем новое поле if (keyboard) { // Обязательно сериализуем в json params.keyboard = JSON.stringify(keyboard); }const url = ENDPOINT + "messages.send?" + new URLSearchParams(params).toString(); const response = await fetch(url); return response.json(); }
// Классы для типизации разметки клавиатуры public class KeyboardMarkup { [JsonPropertyName("one_time")] public bool OneTime { get; set; } [JsonPropertyName("buttons")] public KeyboardMarkupButton[][] Buttons { get; set; } } public class KeyboardMarkupButton { [JsonPropertyName("action")] public KeyboardMarkupButtonAction Action { get; set; } [JsonPropertyName("color")] public string Color { get; set; } } public class KeyboardMarkupButtonAction { [JsonPropertyName("type")] public string Type { get; set; } [JsonPropertyName("label")] public string Label { get; set; } [JsonPropertyName("payload")] public string? Payload { get; set; } } // Добавим в параметры необязательный аргумент - клавиатуру public static async Task<VkResponse?> SendMessage(long peerId, string message, KeyboardMarkup? keyboard = null){ Dictionary<string, string> parameters = new Dictionary<string, string> { ["peer_id"] = peerId.ToString(), ["message"] = message, ["access_token"] = ACCESS_TOKEN, ["v"] = VERSION, ["random_id"] = new Random().Next(1, 1000000).ToString() // обязательно для отправки };// Если аргумент передан, добавляем новое поле if (keyboard is not null) { // Обязательно сериализуем в json parameters.Add("keyboard", JsonSerializer.Serialize(keyboard)); }string url = ENDPOINT + "messages.send?" + string.Join("&", parameters.Select(p => $"{HttpUtility.UrlEncode(p.Key)}={HttpUtility.UrlEncode(p.Value)}")); HttpClient httpClient = new HttpClient(); HttpResponseMessage response = await httpClient.GetAsync(url); return await response.Content.ReadFromJsonAsync<VkResponse>(); }
Формат разметки клавиатуры
Итак, мы подготовили функцию отправки сообщения, которая умеет принимать клавиатуру. Теперь нужно понять, в каком формате передавать в неё разметку. VK API ожидает JSON-объект строго определённой структуры. Рассмотрим её:
{
"one_time": "true/false",
"inline": "true/false",
"buttons": [
[
{
"action": {
"type": "ТИП_КНОПКИ",
"label": "ТЕКСТ_КНОПКИ",
"payload": "ЛЮБЫЕ_ДАННЫЕ",
},
"color": "ВАРИАНТ_ЦВЕТА",
}
],
[
{
"action": {
"type": "ТИП_КНОПКИ",
"label": "ТЕКСТ_КНОПКИ",
},
"color": "ВАРИАНТ_ЦВЕТА",
}
],
],
}
Описание полей
Поля разметки
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
one_time |
boolean | да | Если true — клавиатура исчезает после первого нажатия (скрывается). Если false — остаётся на месте. |
inline |
boolean | да | false — обычная клавиатура (вместо системной клавиатуры). true — inline-клавиатура (крепится к сообщению). |
buttons |
массив | да | Массив рядов. Каждый ряд — массив кнопок. Максимум 10 рядов, в ряду до 4 кнопок (для обычной клавиатуры) или до 5 (для inline). |
Поля кнопки
| Поле | Тип | Описание |
|---|---|---|
action |
объект | Описывает действие кнопки. |
color |
строка | Цвет кнопки (только для обычной клавиатуры, у inline цвет не поддерживается). |
Объект action
| Поле | Тип | Описание |
|---|---|---|
type |
строка |
Тип действия. Для текстовых кнопок обычной клавиатуры — "text". Для inline есть и другие (callback,
open_link и т.д.).
|
label |
строка | Текст на кнопке (до 40 символов). |
payload (необязательное) |
строка | Произвольные данные в формате JSON-строки (до 255 символов). Возвращается при нажатии и помогает идентифицировать кнопку. |
Цвета кнопок (для обычной клавиатуры)
| Цвет | Назначение |
|---|---|
primary |
Синий, главное действие. |
secondary |
Белый, второстепенное действие. |
negative |
Красный, опасное действие (удалить, отмена). |
positive |
Зелёный, подтверждение (оплатить, согласиться). |
Поле color не используется для inline-клавиатур — их внешний вид определяется системой.
Отправляем кнопки пользователю
Теперь, зная структуру, мы можем сформировать массив данных кнопок, которые нам нужны и отправить их пользователю.
Давайте в нашем обработчике
сообщений создадим массив из трех кнопок. Две будут вверху, третья внизу на всю ширину:
switch ($data['type']) { case 'confirmation': echo CONFIRMATION_TOKEN; exit(); // Добавляем обработку нового сообщения case 'message_new':// Формируем клавиатуру $keyboardData = [ "one_time" => false, "buttons" => [ [ [ "action" => [ "type" => "text", "label" => "Кнопка 1 от PHP", "payload" => ["button" => 1] ], "color" => "primary" ], [ "action" => [ "type" => "text", "label" => "Кнопка 2 от PHP", "payload" => ["button" => 2] ], "color" => "secondary" ] ], [ [ "action" => [ "type" => "text", "label" => "Кнопка 3 от PHP" ], "color" => "negative" ] ] ] ]; // Отправляем клавиатуру в функцию sendMessage($data['object']['message']['peer_id'], 'Сообщение с PHP сервера', $keyboardData);http_response_code(200); exit('ok'); default: http_response_code(200); exit('ok'); }
if data['type'] == 'confirmation': return CONFIRMATION_TOKEN, 200 # Добавляем обработку нового сообщения if data['type'] == 'message_new':# Формируем клавиатуру keyboard_data = { "one_time" : False, "buttons" : [ [ { "action" : { "type" : "text", "label" : "Кнопка 1 Python", "payload" : { "button": 1 }, }, "color" : "primary" }, { "action" : { "type" : "text", "label" : "Кнопка 2 Python", "payload" : { "button": 2 }, }, "color" : "secondary" } ], [ { "action" : { "type" : "text", "label" : "Кнопка 3 Python" }, "color" : "negative" } ] ] } # Отправляем клавиатуру в функцию send_message(data['object']['message']['peer_id'], 'Сообщение с Python сервера', keyboard_data)return 'ok', 200 return 'ok', 200
switch (data.type) { case "confirmation": return res.send(CONFIRMATION_TOKEN); // Добавляем обработку нового сообщения case "message_new":// Формируем клавиатуру const keyboardData = { one_time: false, buttons: [ [ { action: { type: "text", label: "Кнопка 1 от JS", payload: { button: 1 }, }, color: "primary", }, { action: { type: "text", label: "Кнопка 2 JS", payload: { button: 2 }, }, color: "secondary", }, ], [ { action: { type: "text", label: "Кнопка 3 JS", }, color: "negative", }, ], ], }; // Отправляем клавиатуру в функцию sendMessage(data.object.message["peer_id"], "Сообщение с JS сервера", keyboardData);return res.status(200).send("ok"); default: return res.status(200).send("ok"); }
// Класс для типизации payload public class KeyboardMarkupButtonPayload { [JsonPropertyName("button")] public int Button { get; set; } }switch (data.Type) { case "confirmation": return Results.Ok(CONFIRMATION_TOKEN);// Добавляем обработку нового сообщения case "message_new": // Формируем клавиатуру KeyboardMarkup markup = new KeyboardMarkup() { OneTime = false, Buttons = new KeyboardMarkupButton[2][] { new KeyboardMarkupButton[2] { new KeyboardMarkupButton() { Action = new KeyboardMarkupButtonAction() { Type = "text", Label = "Кнопка 1 C#", Payload = new KeyboardMarkupButtonPayload() { Button = 1 } }, Color = "primary" }, new KeyboardMarkupButton() { Action = new KeyboardMarkupButtonAction() { Type = "text", Label = "Кнопка 2 C#", Payload = new KeyboardMarkupButtonPayload() { Button = 2 } }, Color = "secondary" } }, new KeyboardMarkupButton[1] { new KeyboardMarkupButton() { Action = new KeyboardMarkupButtonAction() { Type = "text", Label = "Кнопка 3 C#" }, Color = "negative" } } } }; // Отправляем клавиатуру в функцию await SendMessage(data.Object.Message.PeerId, "Сообщение с C# сервера", markup);return Results.Ok("ok"); default: return Results.Ok("ok"); }
Теперь, когда нам придет любое новое сообщение, мы будем отправлять клавиатуру. Мы разметили ее таким образом, что так как в первом подмассиве у
нас две кнопки, то они поделять место в первом ряду, а третья кнопка, так как находится во втором подмассиве и она там одна, растянется на всю
ширину второго ряда.
Вот результат:



Давайте теперь посмотрим, что будет если их нажать
Результаты будут по кнопкам C# сервера, но они идентичны остальным:
{
"group_id": 236815260,
"type": "message_new",
"event_id": "a9dfe1339e157429895e8cb8b1e4b0a7a6dc45e3",
"v": "5.199",
"object": {
"client_info": {
"button_actions": [
"text",
"vkpay",
"open_app",
"location",
"open_link",
"open_photo",
"callback",
"intent_subscribe",
"intent_unsubscribe"
],
"keyboard": true,
"inline_keyboard": true,
"carousel": true,
"lang_id": 0
},
"message": {
"date": 1774033735,
"from_id": 123052131,
"id": 46,
"version": 10000117,
"out": 0,
"fwd_messages": [],
"important": false,
"is_hidden": false,
"attachments": [],
"conversation_message_id": 46,
"payload": "{\"button\":1}",
"text": "Кнопка 1 C#",
"peer_id": 123052131,
"random_id": 0
}
}
}
{
"group_id": 236815260,
"type": "message_new",
"event_id": "ca2b9e428953f7d37c285f6eff81a00be2c693ff",
"v": "5.199",
"object": {
"client_info": {
"button_actions": [
"text",
"vkpay",
"open_app",
"location",
"open_link",
"open_photo",
"callback",
"intent_subscribe",
"intent_unsubscribe"
],
"keyboard": true,
"inline_keyboard": true,
"carousel": true,
"lang_id": 0
},
"message": {
"date": 1774033736,
"from_id": 123052131,
"id": 47,
"version": 10000118,
"out": 0,
"fwd_messages": [],
"important": false,
"is_hidden": false,
"attachments": [],
"conversation_message_id": 47,
"payload": "{\"button\":2}",
"text": "Кнопка 2 C#",
"peer_id": 123052131,
"random_id": 0
}
}
}
{
"group_id": 236815260,
"type": "message_new",
"event_id": "3a019ce59dd88b07eb0608179cdae5da4a3f5537",
"v": "5.199",
"object": {
"client_info": {
"button_actions": [
"text",
"vkpay",
"open_app",
"location",
"open_link",
"open_photo",
"callback",
"intent_subscribe",
"intent_unsubscribe"
],
"keyboard": true,
"inline_keyboard": true,
"carousel": true,
"lang_id": 0
},
"message": {
"date": 1774033691,
"from_id": 123052131,
"id": 45,
"version": 10000116,
"out": 0,
"fwd_messages": [],
"important": false,
"is_hidden": false,
"attachments": [],
"conversation_message_id": 45,
"text": "Кнопка 3 C#",
"peer_id": 123052131,
"random_id": 0
}
}
}
Обратите внимание на то, что прожатые кнопки инициируют событие "message_new", а внутри них лежат данные в том же формате, что мы описывали ранее, с типом, группой, откуда пришло сообщение, но также, там лежат:
-
В поле
object -> message -> textтекст нашей кнопки.
Он лежит там, так как нажатая кнопка переносит свой текст как сообщение и отпрвляет его -
В поле
object -> message -> payloadлежит то что мы отправили в payload нашей кнопки в клавиатуре
Заключение
Мы разобрали первый тип клавиатур, научились создавать кнопки с разными цветами и обрабатывать нажатия. Теперь ваш бот стал не просто отвечающим, а интерактивным: пользователь может выбирать действия одним касанием, не вводя команды вручную.
Что мы сделали:
Доработали функцию отправки сообщений, добавив поддержку клавиатур.
Изучили структуру JSON для обычной клавиатуры.
Настроили цвета и обработку payload.
Проверили, как выглядят нажатия кнопок в событиях.
Клавиатуры — важный шаг к удобному боту, но впереди ещё больше. В следующей части мы научимся работать с inline-клавиатурой.