+7(921)-851-18-76 Иконка телефона Запросить звонок
Опубликовано:

Спасаем бизнес: Создаём VK-бота на PHP, Python, JS и C#.
Часть 4 — Inline‑клавиатуры, безопасность и надёжность

В третьей части мы разобрали обычные клавиатуры: бот уже умеет показывать меню, ловить нажатия и отвечать пользователю. Получилось довольно удобно.

Но для реального сервиса этого мало. Мы до сих пор отправляли запросы к API через GET, никак не проверяли, что уведомления приходят именно от VK, и ни слова не сказали про inline-клавиатуры — тот самый инструмент, которым пользуются почти все серьёзные боты.

В этой части серии исправляем все три момента.

Вы узнаете:

  • как делать inline-клавиатуры, которые «приклеиваются» к конкретному сообщению;
  • как обрабатывать нажатия callback-кнопок (отдельный тип события message_event);
  • как защитить сервер секретным ключом, чтобы никто не мог слать поддельные запросы;
  • почему стоит перейти с GET на POST при работе с VK API и как это сделать.

После этой части бот станет заметно надёжнее и ближе к тому, что уже будет не стыдно показать в своем git-репозитории.

Inline‑клавиатуры: кнопки, которые всегда рядом

Обычная клавиатура, которую мы разобрали в прошлой части, заменяет системную клавиатуру телефона и остаётся в поле ввода. Для главного меню, анкет или форм это ещё может быть приемлемо, но бывают ситуации, когда кнопки контекстно касаются какого-то одного сообщения, а не всего диалога. Например, под новостью или карточкой товара хочется видеть «Нравится», «Подробнее», «Поделиться» или «Оплатить». Для таких случаев в VK и внедрили inline-клавиатуры.

Как они устроены

С точки зрения API это всё тот же JSON-объект клавиатуры, только с одним ключевым отличием: поле "inline" ставится в true.

{
  "inline": true,
  "buttons": [ ... ]
}

Кнопки внутри могут делать разные вещи (в зависимости от action.type):

  • "text" — отправляет текст в чат, как обычная кнопка.
  • "callback" — тихо отправляет событие на ваш сервер, не показывая текст пользователю. Идеально для скрытых действий («Подтвердить заказ», «Отменить», «Выбрать вариант»).
  • "open_link" — просто открывает указанную ссылку.
  • "location", "vkpay" и остальные — смотрите в документации VK.

Важный момент: цвет кнопок (color) для inline-клавиатур не работает — внешний вид полностью определяет сама платформа.

Как отправлять inline-клавиатуру

Чтобы прикрепить её к сообщению, точно так же передаём объект в параметр keyboard метода messages.send. Структура массива buttons не меняется: массив рядов, в каждом ряду до 5 кнопок.


switch ($data['type']) {
    case 'confirmation':
        echo CONFIRMATION_TOKEN;
        exit();
    case 'message_new':
        // Формируем inline-клавиатуру с callback-кнопками
        $keyboardData = [
            "inline" => true,  // Ключевой параметр!
            "buttons" => [
                [
                    [
                        "action" => [
                            "type" => "callback",
                            "label" => "📦 Статус заказа",
                            "payload" => ["command" => "status"]
                        ]
                    ],
                    [
                        "action" => [
                            "type" => "callback",
                            "label" => "💰 Помощь",
                            "payload" => ["command" => "help"]
                        ]
                    ]
                ],
                [
                    [
                        "action" => [
                            "type" => "callback",
                            "label" => "🔧 Настройки",
                            "payload" => ["command" => "settings"]
                        ]
                    ],
                    [
                        "action" => [
                            "type" => "open_link",
                            "label" => "🔗 Перейти на сайт",
                            "link" => "https://example.com"
                        ]
                    ]
                ]
            ]
        ];
        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':
    # Формируем inline-клавиатуру с callback-кнопками
    keyboard_data = {
        "inline": True,  # Ключевой параметр!
        "buttons": [
            [
                {
                    "action": {
                        "type": "callback",
                        "label": "📦 Статус заказа",
                        "payload": json.dumps({"command": "status"})
                    }
                },
                {
                    "action": {
                        "type": "callback",
                        "label": "💰 Помощь",
                        "payload": json.dumps({"command": "help"})
                    }
                }
            ],
            [
                {
                    "action": {
                        "type": "callback",
                        "label": "🔧 Настройки",
                        "payload": json.dumps({"command": "settings"})
                    }
                },
                {
                    "action": {
                        "type": "open_link",
                        "label": "🔗 Перейти на сайт",
                        "link": "https://example.com"
                    }
                }
            ]
        ]
    }
    send_message(data['object']['message']['peer_id'], 'Сообщение с Python сервера', keyboard_data)
    return 'ok', 200
            

switch (data.type) {
  case "confirmation":
    return res.send(CONFIRMATION_TOKEN);
  case "message_new":
    // Формируем inline-клавиатуру с callback-кнопками
    keyboardData = {
      inline: true, // Ключевой параметр!
      buttons: [
        [
          {
            action: {
              type: "callback",
              label: "📦 Статус заказа",
              payload: JSON.stringify({ command: "status" }),
            },
          },
          {
            action: {
              type: "callback",
              label: "💰 Помощь",
              payload: JSON.stringify({ command: "help" }),
            },
          },
        ],
        [
          {
            action: {
              type: "callback",
              label: "🔧 Настройки",
              payload: JSON.stringify({ command: "settings" }),
            },
          },
          {
            action: {
              type: "open_link",
              label: "🔗 Перейти на сайт",
              link: "https://example.com",
            },
          },
        ],
      ],
    };
    sendMessage(data.object.message["peer_id"], "Сообщение с JS сервера", keyboardData);
    return res.status(200).send("ok");
  default:
    return res.status(200).send("ok");
}
                
switch (data.Type)
{
    case "confirmation":
        return Results.Ok(CONFIRMATION_TOKEN);
    case "message_new":
        // Формируем inline-клавиатуру с callback-кнопками
        KeyboardMarkup markup = new KeyboardMarkup()
        {
            Inline = true,
            Buttons = new KeyboardMarkupButton[2][]
            {
                new KeyboardMarkupButton[2]
                {
                    new KeyboardMarkupButton()
                    {
                        Action = new KeyboardMarkupButtonAction()
                        {
                            Type = "callback",
                            Label = "📦 Статус заказа",
                            Payload = new KeyboardMarkupButtonPayload()
                            {
                                Command = "status"
                            }
                        }
                    },
                    new KeyboardMarkupButton()
                    {
                        Action = new KeyboardMarkupButtonAction()
                        {
                            Type = "callback",
                            Label = "💰 Помощь",
                            Payload = new KeyboardMarkupButtonPayload()
                            {
                                Command = "help"
                            }
                        }
                    }
                },
                new KeyboardMarkupButton[2]
                {
                    new KeyboardMarkupButton()
                    {
                        Action = new KeyboardMarkupButtonAction()
                        {
                            Type = "callback",
                            Label = "🔧 Настройки",
                            Payload = new KeyboardMarkupButtonPayload()
                            {
                                Command = "settings"
                            }
                        }
                    },
                    new KeyboardMarkupButton()
                    {
                        Action = new KeyboardMarkupButtonAction()
                        {
                            Type = "open_link",
                            Label = "🔗 Перейти на сайт",
                            Link = "https://example.com"
                        }
                    }
                }
            }
        };
        // Отправляем клавиатуру в функцию
        await SendMessage(data.Object.Message.PeerId, "Сообщение с C# сервера", markup);
        return Results.Ok("ok");
    default:
        return Results.Ok("ok");
}
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)
    {
        /*
            Из за того, что не настроенный JsonSerializer пустые поля вставляет как null или 0
            что приведет к тому что VK API будет игнорировать наш запрос.
            Поэтому необходимо настроить игнорирование пустых полей через JsonSerializerOptions
        */
        parameters.Add("keyboard", JsonSerializer.Serialize(keyboard, new JsonSerializerOptions()
        {
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
        }));
    }
    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?>();
}
Сервер присылает кнопки

Результат обработки и отправки кнопок разными серверами

Обработка нажатий: два типа событий

Когда пользователь нажимает на inline‑кнопку, поведение зависит от её типа:

  • Для кнопок с "type": "text" VK отправляет обычное событие message_new с текстом кнопки в поле object.message.text. Мы уже умеем это обрабатывать.
  • Для кнопок с "type": "callback" VK не публикует сообщение в чате, а вместо этого присылает событие message_event. В этом событии содержатся: user_id, peer_id, payload (те данные, которые вы передали в кнопке), event_id и т.д.

Чтобы бот реагировал на callback‑кнопки, нужно:

  • В настройках Callback API включить событие «Нажатие на callback‑кнопку» (оно может быть отключено по умолчанию).
  • В обработчике сервера добавить ветку для type = "message_event".
  • Обязательно ответить на это событие методом messages.sendMessageEventAnswer, иначе VK будет считать, что обработка не удалась, и может показывать ошибку пользователю.

Структура события message_event


{
    "type": "message_event",
    "event_id": "...",
    "group_id": 123,
    "object": {
        "user_id": 456,
        "peer_id": 456,
        "event_id": "...",
        "payload": "{\"button\":\"pay\"}"
    }
}
    

Добавляем обработку в код

В основной обработчик запросов (там, где мы разбираем data['type']) добавляем новую ветку:


// Добавляем обработку нажатия inline кнопки
case 'message_event':
    /*
        Получаем данные о нажатии из object
        Для работы нам понадобится: peer_id, user_id, event_id
        А также мы заберем то что "запаковали" в payload
    */
    $peerId = $data['object']['peer_id'] ?? $data['object']['message']['peer_id'];
    $userId = $data['object']['user_id'] ?? $data['object']['message']['user_id'];
    $eventId = $data['object']['event_id'];
    $payload = $data['object']['payload'];
    $command = $payload['command'] ?? '';

    // Выбираем ответное сообщение в зависимости от команды
    $responseMessage = match ($command) {
        'status' => "📦 Статус вашего заказа: в обработке. Номер заказа: #12345",
        'help' => "💰 Для получения помощи:\n1. Напишите оператору\n2. Позвоните: +7 (XXX) XXX-XX-XX\n3. Email: support@example.com",
        'settings' => "🔧 Настройки:\n- Язык: Русский\n- Уведомления: Включены\n- Часовой пояс: MSK",
        default => "❌ Неизвестная команда"
    };

    // Отправляем ответ пользователю
    sendMessage($peerId, $responseMessage);

    http_response_code(200);
    exit('ok');

# Добавляем обработку нажатия inline кнопки
if data['type'] == 'message_event':
    # Получаем данные о нажатии из object
    # Для работы нам понадобится: peer_id, user_id, event_id
    # А также мы заберем то что "запаковали" в payload
    peer_id = data['object'].get('peer_id') or data['object']['message'].get('peer_id')
    user_id = data['object'].get('user_id') or data['object']['message'].get('user_id')
    event_id = data['object']['event_id']
    payload = data['object']['payload']
    command = payload.get('command', '')

    # Выбираем ответное сообщение в зависимости от команды
    match command:
        case 'status':
            response_message = "📦 Статус вашего заказа: в обработке. Номер заказа: #12345"
        case 'help':
            response_message = "💰 Для получения помощи:\n1. Напишите оператору\n2. Позвоните: +7 (XXX) XXX-XX-XX\n3. Email: support@example.com"
        case 'settings':
            response_message = "🔧 Настройки:\n- Язык: Русский\n- Уведомления: Включены\n- Часовой пояс: MSK"
        case _:
            response_message = "❌ Неизвестная команда"

    # Отправляем ответ пользователю
    send_message(peer_id, response_message)
    return 'ok', 200

// Добавляем обработку нажатия inline кнопки
case "message_event":
  /*
    Получаем данные о нажатии из object
    Для работы нам понадобится: peer_id, user_id, event_id
    А также мы заберем то что "запаковали" в payload
  */
  peerId = data.object['peer_id'] ?? data.object.message['peer_id'];
  userId = data.object['user_id'] ?? data.object.message['user_id'];
  eventId = data.object['event_id'];
  payload = data.object.payload;
  command = payload.command ?? "";

  // Выбираем ответное сообщение в зависимости от команды
  const responseMessage = {
    status: "📦 Статус вашего заказа: в обработке. Номер заказа: #12345",
    help: "💰 Для получения помощи:\n1. Напишите оператору\n2. Позвоните: +7 (XXX) XXX-XX-XX\n3. Email: support@example.com",
    settings: "🔧 Настройки:\n- Язык: Русский\n- Уведомления: Включены\n- Часовой пояс: MSK",
  }[command] ?? "❌ Неизвестная команда";

  // Отправляем ответ пользователю
  sendMessage(peerId, responseMessage);
  return res.status(200).send("ok");
            

public class VkRequestObject
{
    [JsonPropertyName("client_info")]
    public VkRequestObjectClientInfo? ClientInfo { get; set; }
    [JsonPropertyName("message")]
    public VkRequestObjectMessage? Message { get; set; }
    [JsonPropertyName("peer_id")]
    public long? PeerId { get; set; }
    [JsonPropertyName("user_id")]
    public long? UserId { get; set; }
    [JsonPropertyName("event_id")]
    public string? EventId { get; set; }
    [JsonPropertyName("payload")]
    public KeyboardMarkupButtonPayload? Payload { get; set; }
}
public class VkRequestObjectMessage
{
    [JsonPropertyName("date")]
    public long Date { get; set; }
    [JsonPropertyName("from_id")]
    public long FromId { get; set; }
    [JsonPropertyName("id")]
    public long Id { get; set; }
    [JsonPropertyName("version")]
    public long MessageVersion { get; set; }
    [JsonPropertyName("out")]
    public long Outgoing { get; set; }
    [JsonPropertyName("fwd_messages")]
    public string[]? FwdMessages { get; set; }
    [JsonPropertyName("important")]
    public bool Important { get; set; }
    [JsonPropertyName("is_hidden")]
    public bool IsHidden { get; set; }
    [JsonPropertyName("attachments")]
    public string[]? Attachments { get; set; }
    [JsonPropertyName("conversation_message_id")]
    public long ConversationMessageId { get; set; }
    [JsonPropertyName("text")]
    public string? Text { get; set; }
    [JsonPropertyName("peer_id")]
    public long PeerId { get; set; }
    [JsonPropertyName("random_id")]
    public long RandomId { get; set; }
    [JsonPropertyName("user_id")]
    public long UserId { get; set; }
}
// Добавляем обработку нажатия inline кнопки
case "message_event":
    /*
      Получаем данные о нажатии из object
      Для работы нам понадобится: peer_id, user_id, event_id
      А также мы заберем то что "запаковали" в payload
    */
    long peerId = data.Object!.PeerId ?? data.Object.Message!.PeerId;
    long userId = data.Object.UserId ?? data.Object.Message!.UserId;
    string? eventId = data.Object.EventId;
    KeyboardMarkupButtonPayload payload = data.Object.Payload;
    string command = payload.Command ?? "";
    // Выбираем ответное сообщение в зависимости от команды
    string responseMessage = command switch
    {
        "status" => "📦 Статус вашего заказа: в обработке. Номер заказа: #12345",
        "help" => "💰 Для получения помощи:\n1. Напишите оператору\n2. Позвоните: +7 (XXX) XXX-XX-XX\n3. Email: support@example.com",
        "settings" => "🔧 Настройки:\n- Язык: Русский\n- Уведомления: Включены\n- Часовой пояс: MSK",
        _ => "❌ Неизвестная команда"
    };
    // Отправляем ответ пользователю
    SendMessage(peerId, responseMessage);
    return Results.Ok("ok");

Проверим работу:

Зависание кнопок

Как видно, сервер реагирует и успешно определяет, какая кнопка была нажата, но есть нюанс, его можно увидеть на кнопке. Ее текст сменился иконкой загрузки.

Это происходит потому, что мы не отправляет API уведомление об успешной обработке запроса с кнопки. Давайте это исправим.

Функция завершения коллбэка кнопки

Согласно документации, после обработки кнопки, нам необходимо отправить API уведомление о завершении обработки на messages.sendMessageEventAnswer, указав при этом eventId, peerId, userId.

Создадим дополнительную функцию для этого процесса:


function sendMessageEventAnswer($eventId, $peerId, $userId)
{
    $params = [
        'event_id' => $eventId,
        'user_id' => $userId,
        'peer_id' => $peerId,
        'access_token' => ACCESS_TOKEN,
        'v' => VERSION,
    ];

    $url = ENDPOINT . 'messages.sendMessageEventAnswer?' . http_build_query($params);
    @file_get_contents($url);
}
            

def send_message_event_answer(event_id, peer_id, user_id):
    params = {
        'event_id': event_id,
        'user_id': user_id,
        'peer_id': peer_id,
        'access_token': ACCESS_TOKEN,
        'v': VERSION,
    }

    url = ENDPOINT + 'messages.sendMessageEventAnswer'
    requests.get(url, params=params)

function sendMessageEventAnswer(eventId, peerId, userId)
{
    $params = {
        event_id: eventId,
        user_id: userId,
        peer_id: peerId,
        access_token: ACCESS_TOKEN,
        v: VERSION,
    };

    $url = ENDPOINT + 'messages.sendMessageEventAnswer?' +  new URLSearchParams(params).toString();
    await fetch(url);
}
            

public static async Task SendMessageEventAnswer(string eventId, long peerId, long userId)
{
    Dictionary<string, string> params = new Dictionary<string, string>
    {
        ["event_id"] = eventId,
        ["user_id"] = userId.ToString(),
        ["peer_id"] = peerId.ToString(),
        ["access_token"] = ACCESS_TOKEN,
        ["v"] = VERSION
    };

    string url = ENDPOINT + "messages.sendMessageEventAnswer?" + string.Join("&", params.Select(p => $"{HttpUtility.UrlEncode(p.Key)}={HttpUtility.UrlEncode(p.Value)}"));
    HttpClient httpClient = new HttpClient();
    HttpResponseMessage response = await httpClient.GetAsync(url);
    response.Content.ReadFromJsonAsync<VkResponse>();
}
            

Обратите внимание на метод messages.sendMessageEventAnswer. Он требует event_id, user_id и peer_id из полученного события.

Теперь добавим вызов метода сразу после всех действий обработки нажатия (в нашем случае - отправки сообщения):


case 'message_event':
    $peerId = $data['object']['peer_id'] ?? $data['object']['message']['peer_id'];
    $userId = $data['object']['user_id'] ?? $data['object']['message']['user_id'];
    $eventId = $data['object']['event_id'];
    $payload = $data['object']['payload'];
    $command = $payload['command'] ?? '';

    $responseMessage = match ($command) {
        'status' => "📦 Статус вашего заказа: в обработке. Номер заказа: #12345",
        'help' => "💰 Для получения помощи:\n1. Напишите оператору\n2. Позвоните: +7 (XXX) XXX-XX-XX\n3. Email: support@example.com",
        'settings' => "🔧 Настройки:\n- Язык: Русский\n- Уведомления: Включены\n- Часовой пояс: MSK",
        default => "❌ Неизвестная команда"
    };
    
    sendMessage($peerId, $responseMessage);
    sendMessageEventAnswer($eventId, $peerId, $userId);
    
    http_response_code(200);
    exit('ok');

if data['type'] == 'message_event':
    peer_id = data['object'].get('peer_id') or data['object']['message'].get('peer_id')
    user_id = data['object'].get('user_id') or data['object']['message'].get('user_id')
    event_id = data['object']['event_id']
    payload = data['object']['payload']
    command = payload.get('command', '')
    match command:
        case 'status':
            response_message = "📦 Статус вашего заказа: в обработке. Номер заказа: #12345"
        case 'help':
            response_message = "💰 Для получения помощи:\n1. Напишите оператору\n2. Позвоните: +7 (XXX) XXX-XX-XX\n3. Email: support@example.com"
        case 'settings':
            response_message = "🔧 Настройки:\n- Язык: Русский\n- Уведомления: Включены\n- Часовой пояс: MSK"
        case _:
            response_message = "❌ Неизвестная команда"
    send_message(peer_id, response_message)
    
    send_message_event_answer(event_id, peer_id, user_id)

    return 'ok', 200

case "message_event":
  peerId = data.object["peer_id"] ?? data.object.message["peer_id"];
  userId = data.object["user_id"] ?? data.object.message["user_id"];
  eventId = data.object["event_id"];
  payload = data.object.payload;
  command = payload.command ?? "";
  const responseMessage =
    {
      status: "📦 Статус вашего заказа: в обработке. Номер заказа: #12345",
      help: "💰 Для получения помощи:\n1. Напишите оператору\n2. Позвоните: +7 (XXX) XXX-XX-XX\n3. Email: support@example.com",
      settings: "🔧 Настройки:\n- Язык: Русский\n- Уведомления: Включены\n- Часовой пояс: MSK",
    }[command] ?? "❌ Неизвестная команда";
  sendMessage(peerId, responseMessage);
  sendMessageEventAnswer(eventId, peerId, userId);
  return res.status(200).send("ok");

case "message_event":
    long peerId = data.Object!.PeerId ?? data.Object.Message!.PeerId;
    long userId = data.Object.UserId ?? data.Object.Message!.UserId;
    string? eventId = data.Object.EventId;
    KeyboardMarkupButtonPayload payload = data.Object.Payload;
    string command = payload.Command ?? "";
    string responseMessage = command switch
    {
        "status" => "📦 Статус вашего заказа: в обработке. Номер заказа: #12345",
        "help" => "💰 Для получения помощи:\n1. Напишите оператору\n2. Позвоните: +7 (XXX) XXX-XX-XX\n3. Email: support@example.com",
        "settings" => "🔧 Настройки:\n- Язык: Русский\n- Уведомления: Включены\n- Часовой пояс: MSK",
        _ => "❌ Неизвестная команда"
    };
    SendMessage(peerId, responseMessage);
    SendMessageEventAnswer(eventId, peerId, userId);
    return Results.Ok("ok");

Теперь, после нажатия, наш сервер будет уведомлять API о том что обработка завершена и кнопки не будут зависать.

Безопасность: секретный ключ

Иллюстрация атаки злоумышленника

До сих пор мы доверяли любому POST‑запросу на наш эндпоинт. Но злоумышленник, узнав адрес сервера, может отправить поддельное уведомление, имитируя VK. Чтобы этого не случилось, VK предоставляет механизм секретного ключа.

Как настроить

В настройках Callback API вашего сообщества есть поле «Секретный ключ». Вы можете задать там любую строку (например, сгенерировать случайный пароль). После этого каждый запрос от VK будет содержать либо заголовок X-VK-API-Secret, либо (в зависимости от настроек) поле "secret" в теле JSON.

Проверка в коде

В начале обработчика запроса нужно сверить полученный секрет с тем, который вы сохранили в константе. Если секретного ключа нет или они не совпадают - ставим запросу 401 (не авторизован) или 400 (некорректный запрос) статус, и, главное — не выполняем логику бота.

Начнем с того, что сгенерируем токен:

Поле вставки секретного ключа в VK

Теперь нужно зарегистрировать новую константу:

define('SECRET', '9czSbCI4N6EKIAWxADZb9r');
SECRET = '9czSbCI4N6EKIAWxADZb9r'
            
const SECRET = '9czSbCI4N6EKIAWxADZb9r';
private const string SECRET = "9czSbCI4N6EKIAWxADZb9r";

Далее - все просто, секретный ключ будет передаваться в поле ['secret'] уведомления от API. Наша задача - просто взять его, сверить с константой и выполнить действия, которые мы условили выше:


if (!isset($data['type'])) {
    http_response_code(200);
    exit('ok');
}

if (!isset($data['secret']) || $data['secret'] !== SECRET) {
    http_response_code(401);
    exit('Unauthorized');
}
switch ($data['type'])

if data['type'] == 'confirmation':
    return CONFIRMATION_TOKEN, 200

if ('secret' not in data or data['secret'] != SECRET):
    return 'Unauthorized', 401

if data['type'] == 'message_new':

if (!data || !data.type) {
  return res.status(200).send("ok");
}

if (!data.secret || data.secret !== SECRET) {
  return res.status(401).send("Unauthorized");
}
switch (data.type)

if (data is null) return Results.Ok("ok");
if (string.IsNullOrEmpty(data.Secret) || data.Secret != SECRET) return Results.Unauthorized();
switch (data.Type)

Эта простая проверка резко повышает безопасность нашего бота.

Переход на POST: почему это важно

В предыдущих частях мы отправляли запросы к API VK (например, messages.send) через GET. Это упрощало примеры, но в реальном проекте лучше использовать POST.

Это важно по нескольким причинам:

  • Документация VK рекомендует POST для методов, изменяющих данные. GET может не поддерживаться для больших объёмов (например, длинное сообщение + сложная клавиатура).
  • Надёжность: некоторые прокси или CDN могут кэшировать GET‑запросы, что неприемлемо для отправки сообщений.
  • Безопасность: параметры не светятся в логах и истории браузера.

Переделаем функцию sendMessage так, чтобы она отправляла POST‑запрос. При этом не забываем сохранить все параметры: peer_id, message, keyboard, random_id, access_token, v. Тело запроса должно быть в формате application/x-www-form-urlencoded (или multipart/form-data, если есть вложения).


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) {
        $params['keyboard'] = json_encode($keyboard, JSON_UNESCAPED_UNICODE);
    }

    $url = ENDPOINT . 'messages.send';

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Content-Type: application/x-www-form-urlencoded'
    ]);

    $response = curl_exec($ch);
    $error = curl_error($ch);
    curl_close($ch);

    if ($error) {
        error_log("cURL Error: " . $error);
        return null;
    }

    return json_decode($response, true);
}

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:
        params['keyboard'] = json.dumps(keyboard, ensure_ascii=False)
    
    url = ENDPOINT + 'messages.send'
    
    response = requests.post(url, data=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) params.keyboard = JSON.stringify(keyboard);

  const url = ENDPOINT + "messages.send";
  
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams(params).toString()
  });

  return response.json();
}

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)
    {
        JsonSerializerOptions options = new JsonSerializerOptions
        {
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
        };
        parameters.Add("keyboard", JsonSerializer.Serialize(keyboard, options));
    }

    FormUrlEncodedContent content = new FormUrlEncodedContent(parameters);
    using HttpClient httpClient = new HttpClient();
    HttpResponseMessage response = await httpClient.PostAsync(ENDPOINT + "messages.send", content);
    return await response.Content.ReadFromJsonAsync<VkResponse>();
}

После замены убедитесь, что все параметры передаются корректно, и ответы API по‑прежнему обрабатываются. Так же не забудьте исправить метод sendMessageEventAnswer под POST запросы.

Заключение

Мы проделали огромную работу: добавили интерактивные inline‑клавиатуры, научились обрабатывать callback‑кнопки через событие message_event, защитили сервер секретным ключом и привели код в соответствие с лучшими практиками.

Теперь наш бот готов к более или менее реальной работе и может использоваться в бизнес‑сценариях, где важна безопасность и стабильность.

Получить коммерческое предложение

Оставьте заявку, и мы свяжемся с вами, чтобы обсудить ваши потребности и предоставить коммерческое предложение по разработке сайта. Наша команда поможет вам создать эффективный и привлекательный веб-сайт, адаптированный к вашему бизнесу.

Я не робот

Нажмите чтобы продолжить

*Согласие обязательно
Я ознакомлен с политикой конфиденциальности и согласен на обработку персональных данных
Оставьтезаявку
Кнопка закрытия формы
Стрелка вверх
Все получилось Заявкауспешно
отправлена
Спасибо, что доверяете нам! Наш специалист уже получил вашу заявку
и скоро свяжется с вами.
Кнопка закрытия формы