Параметр start у deep‑link t.me/Bot?start=payload дозволяє передати боту довільний рядок при першому запуску діалогу. Відповідно до документації Bot API, довжина payload обмежена 64 байтами, а значення доступне лише в об'єкті message з командою /start, коли користувач ще не мав чату з ботом. Якщо чат уже існує, бот отримає звичайне повідомлення /start без параметра — це важлива відмінність від продовження діалогу.
Генерація безпечного payload
Для прив'язки payload до сутності (наприклад, ID запису на послугу) не варто зберігати сам ID відкритим: його можна підмінити. Краще використовувати криптостійкий одноразовий токен, а в базі зберігати лише його хеш. Приклад генерації посилання в Laravel:
use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB;
function makeDeepLink(int $entityId, int $ttlSeconds = 300): string
{
$botUsername = getenv('TELEGRAM_BOT_USERNAME');
$raw = Str::random(7); // 7 байт → base64url ≈ 11 символов
$token = rtrim(strtr(base64_encode($raw), '+/', '-_'), '=');
$hash = hash('sha256', $token);
$expires = now()->addSeconds($ttlSeconds);
DB::table('deep_link_tokens')->insert([
'token_hash' => $hash,
'entity_id' => $entityId,
'expires_at' => $expires,
'used' => false,
]);
$payload = $token; // уже безопасен для URL
return "https://t.me/{$botUsername}?start={$payload}";
}
Токен довжиною 11 символів добре вкладається в ліміт 64 байти. Ми зберігаємо лише його SHA‑256 хеш, тому навіть при витоку бази зловмисник не зможе підібрати оригінальний токен без перебору.
Обробка start‑команди у вебхуці
При отриманні оновлення через вебхук необхідно:
- Перевірити заголовок
X-Telegram-Bot-Api-Secret-Token(якщо вебхук зареєстрований ізsecret_token). - Витягти
update_idта забезпечити ідемпотентність — не обробляти одне й те саме оновлення двічі. - Якщо повідомлення містить команду
/startз параметром, взяти payload, знайти відповідний хеш у таблиціdeep_link_tokens, перевірити термін дії та прапорецьused. - При успішній перевірці позначити токен як використаний, отримати
entity_idта надіслати персоналізоване повідомлення через Bot API, використовуючи cURL.
header('Content-Type: application/json');
$secretToken = getenv('TELEGRAM_WEBHOOK_SECRET');
if ($secretToken && $_SERVER['HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN'] !== $secretToken) {
http_response_code(403);
exit;
}
$input = file_get_contents('php://input');
$update = json_decode($input, true);
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
exit;
}
$updateId = $update['update_id'] ?? null;
if ($updateId === null) {
http_response_code(200);
exit;
}
// Идемпотентность: сохраняем обработанные update_id в Redis (пример)
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
if ($redis->get('tg_update:' . $updateId)) {
http_response_code(200);
exit; // уже обработано
}
$redis->setex('tg_update:' . $updateId, 300, '1');
$message = $update['message'] ?? null;
if (!$message || !isset($message['text'])) {
http_response_code(200);
exit;
}
$text = trim($message['text']);
if (str_starts_with($text, '/start')) {
$parts = explode(' ', $text, 2);
$payload = $parts[1] ?? '';
if ($payload === '') {
// обычный /start без параметра
http_response_code(200);
exit;
}
// проверяем длину payload (ограничение Bot API)
if (strlen($payload) > 64) {
http_response_code(200);
exit;
}
// ищем токен в БД
$pdo = new PDO('mysql:host=' . getenv('DB_HOST') . ';dbname=' . getenv('DB_NAME'),
getenv('DB_USER'), getenv('DB_PASS'));
$stmt = $pdo->prepare('SELECT id, entity_id, expires_at, used FROM deep_link_tokens WHERE token_hash = ?');
$hash = hash('sha256', $payload);
$stmt->execute([$hash]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
// токен не найден или уже использован
http_response_code(200);
exit;
}
if (new DateTime($row['expires_at']) < new DateTime()) {
// срок истёк
http_response_code(200);
exit;
}
if ((int)$row['used'] === 1) {
// уже использован
http_response_code(200);
exit;
}
// помечаем как использованный
$upd = $pdo->prepare('UPDATE deep_link_tokens SET used = 1 WHERE id = ?');
$upd->execute([$row['id']]);
$entityId = (int)$row['entity_id'];
$chatId = $message['chat']['id'];
// Формируем персонализированное сообщение
$reply = "Привет! Вы перешли по ссылке для сущности #{$entityId}.";
// Отправляем через Bot API cURL
$botToken = getenv('TELEGRAM_BOT_TOKEN');
$apiUrl = "https://api.telegram.org/bot{$botToken}/sendMessage";
$data = [
'chat_id' => $chatId,
'text' => $reply,
];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $apiUrl,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($data),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpCode !== 200) {
// логируем ошибку, но не прерываем вебхук
error_log("Telegram sendMessage failed: {$httpCode} {$response}");
} else {
$decoded = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE || !$decoded['ok'] ?? false) {
error_log("Bad Telegram response: {$response}");
}
}
curl_close($ch);
}
http_response_code(200);
Відмінність від продовження діалогу та ідемпотентність
Параметр start доступний лише тоді, коли користувач уперше відкриває чат із ботом через deep‑link. Якщо користувач уже мав діалог і надсилає /start (або бот отримує команду з меню), об'єкт message міститиме лише команду без параметра. Тому логіка, прив'язана до payload, має виконуватися лише при першому запуску.
Для захисту від повторної обробки одного й того самого оновлення використовуємо ідемпотентність: зберігаємо update_id у швидкому сховищі (Redis, Memcached або окрема таблиця з TTL). При повторному надходженні того самого оновлення ми просто повертаємо 200 OK без жодних дій.
Безпека та захист від підміни payload
- Payload ніколи не зберігається відкрито в базі — зберігаємо лише його криптостійкий хеш (SHA‑256).
- Токен генерується функцією
random_bytes, що робить його непередбачуваним. - Встановлюємо короткий TTL (наприклад, 5 хвилин) і прапорець
used, щоб токен не можна було використовувати повторно після активації. - Перевіряємо довжину payload — не більше ніж 64 байти, інакше відхиляємо запит.
- Якщо вебхук зареєстрований із
secret_token, обов'язково перевіряємо заголовокX-Telegram-Bot-Api-Secret-Token.
Таким чином, навіть якщо зловмисник перехопить або підмінить параметр start, він не зможе підібрати валідний токен без доступу до секретного ключа, що використовується при генерації хешу, і не зможе повторно використати вже погашений токен.
Пам'ятайте: deep link — це лише точка входу. Вся бізнес‑логіка (валідація, запис у CRM, нарахування бонусів) має перебувати в захищеному серверному обробнику, а не в клієнтській частині.
Для швидкого старту ви можете використовувати готовий пакет BotCreator, який надає шаблони вебхуків та утиліти для роботи з deep‑link у Laravel та чистому PHP.