Как отправить заявку с сайта напрямую менеджеру в Telegram: PHP + inline-кнопки

Когда клиент заполняет форму на сайте, заявка должна попасть к менеджеру за секунды. Почта медленная, CRM часто требует ручной обработки. Связка «сайт → PHP → Telegram» с inline-кнопкой «Взять заявку» даёт мгновенный отклик: менеджер открывает чат и одним тапом берёт лид.

Архитектура

HTML-форма шлёт POST на ваш эндпоинт. PHP генерирует короткий lead_id, пишет строку в таблицу leads, отправляет sendMessage с inline_keyboard в чат менеджеров. По нажатию бот получает callback_query и идемпотентно обновляет статус.

Лимит Telegram: callback_data не длиннее 64 байт. Поэтому в кнопку кладём компактный id, а не JSON с ФИО и телефоном.

Таблица leads

CREATE TABLE leads (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
lead_id CHAR(14) NOT NULL,
name VARCHAR(120) NOT NULL,
phone VARCHAR(32) NOT NULL,
email VARCHAR(190) NULL,
message TEXT NULL,
status ENUM('new','taken','closed') NOT NULL DEFAULT 'new',
taken_by BIGINT NULL,
taken_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_lead_id (lead_id),
KEY idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Генерация lead_id

bin2hex(random_bytes(7)) даёт 14 hex-символов: непредсказуемо и укладывается в лимит кнопки вместе с префиксом take:.

<?php
declare(strict_types=1);

function newLeadId(): string
{
return bin2hex(random_bytes(7)); // 14 chars, e.g. a3f8b2c1d9e04f
}

// callback_data = "take:" + lead_id → 5 + 14 = 19 байт (< 64)
function takeCallbackData(string $leadId): string
{
$data = 'take:' . $leadId;
if (strlen($data) > 64) {
throw new InvalidArgumentException('callback_data too long');
}
return $data;
}

Приём формы и отправка в Telegram

<?php
declare(strict_types=1);

header('Content-Type: application/json; charset=utf-8');

$token = getenv('TG_BOT_TOKEN') ?: '';
$managerChat = getenv('TG_MANAGER_CHAT_ID') ?: '';
if ($token === '' || $managerChat === '') {
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'bot not configured']);
exit;
}

$input = json_decode(file_get_contents('php://input') ?: '{}', true) ?: $_POST;
$name = trim((string) ($input['name'] ?? ''));
$phone = trim((string) ($input['phone'] ?? ''));
$email = trim((string) ($input['email'] ?? ''));
$msg = trim((string) ($input['message'] ?? ''));

if ($name === '' || $phone === '') {
http_response_code(422);
echo json_encode(['ok' => false, 'error' => 'name and phone required']);
exit;
}

$pdo = new PDO(
getenv('DSN'),
getenv('DB_USER'),
getenv('DB_PASS'),
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);

$leadId = newLeadId();
$pdo->prepare(
'INSERT INTO leads (lead_id, name, phone, email, message)
VALUES (:lead_id, :name, :phone, :email, :message)'
)->execute([
':lead_id' => $leadId,
':name' => $name,
':phone' => $phone,
':email' => $email !== '' ? $email : null,
':message' => $msg !== '' ? $msg : null,
]);

$text = "<b>Новая заявка</b>\n"
. 'ID: <code>' . htmlspecialchars($leadId) . "</code>\n"
. 'Имя: ' . htmlspecialchars($name) . "\n"
. 'Телефон: ' . htmlspecialchars($phone);

$keyboard = [
'inline_keyboard' => [[
[
'text' => 'Взять заявку',
'callback_data' => takeCallbackData($leadId),
],
]],
];

$ch = curl_init("https://api.telegram.org/bot{$token}/sendMessage");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 10,
CURLOPT_POSTFIELDS => [
'chat_id' => $managerChat,
'text' => $text,
'parse_mode' => 'HTML',
'reply_markup' => json_encode($keyboard, JSON_UNESCAPED_UNICODE),
],
]);
$body = curl_exec($ch);
curl_close($ch);
$decoded = json_decode((string) $body, true);

if (empty($decoded['ok'])) {
http_response_code(502);
echo json_encode(['ok' => false, 'error' => 'telegram send failed']);
exit;
}

echo json_encode(['ok' => true, 'lead_id' => $leadId]);

Обработка callback_query: идемпотентный UPDATE

Два менеджера могут нажать одновременно. Атомарный UPDATE … WHERE status='new' оставляет победителем только первый запрос.

<?php
declare(strict_types=1);

function handleCallbackQuery(PDO $pdo, string $token, array $cq): void
{
$data = (string) ($cq['data'] ?? '');
$fromId = (int) ($cq['from']['id'] ?? 0);
$cqId = (string) ($cq['id'] ?? '');

if (!preg_match('/^take:([a-f0-9]{14})$/', $data, $m)) {
answerCallback($token, $cqId, 'Неизвестная кнопка');
return;
}
$leadId = $m[1];

$stmt = $pdo->prepare(
"UPDATE leads
SET status = 'taken', taken_by = :uid, taken_at = NOW()
WHERE lead_id = :lead_id AND status = 'new'"
);
$stmt->execute([':uid' => $fromId, ':lead_id' => $leadId]);

if ($stmt->rowCount() === 0) {
answerCallback($token, $cqId, 'Заявка уже взята или не найдена');
return;
}

$lead = $pdo->prepare('SELECT * FROM leads WHERE lead_id = ?');
$lead->execute([$leadId]);
$row = $lead->fetch(PDO::FETCH_ASSOC);

answerCallback($token, $cqId, 'Заявка закреплена за вами');

$detail = "Заявка <code>{$leadId}</code>\n"
. "Имя: {$row['name']}\nТелефон: {$row['phone']}\n"
. ($row['email'] ? "Email: {$row['email']}\n" : '')
. ($row['message'] ? "Сообщение: {$row['message']}" : '');

tgSend($token, [
'chat_id' => $fromId,
'text' => $detail,
'parse_mode' => 'HTML',
]);
}

function answerCallback(string $token, string $callbackQueryId, string $text): void
{
tgSend($token, [
'callback_query_id' => $callbackQueryId,
'text' => $text,
'show_alert' => false,
], 'answerCallbackQuery');
}

function tgSend(string $token, array $params, string $method = 'sendMessage'): void
{
$ch = curl_init("https://api.telegram.org/bot{$token}/{$method}");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_POSTFIELDS => $params,
]);
curl_exec($ch);
curl_close($ch);
}

Почему так надёжнее CRM-почты

  • Менеджер видит заявку в привычном чате за 1–2 секунды.
  • Статус «взята» защищён от двойного клика на уровне SQL.
  • Данные клиента живут в БД, а не в 64 байтах кнопки.

Рядом по теме: inline-клавиатуры в PHP и webhook на Yii2.

Собрать такую воронку быстрее на готовом каркасе — botservice.biz.

Новые статьи — в Telegram

Разбираем, что автоматизировать в бизнесе и как это работает на практике. Без спама.