Passing context when a user transitions to a Telegram bot is a standard task for CRMs, интернет-магазин s, and marketing campaigns. The Deep Linking mechanism allows passing a parameter to the bot via a URL formatted as https://t.me/YourBot?start=payload. When the user presses the "Start" button, Telegram sends an incoming message with the text /start payload to your server.
However, deep links are often confused with a dialogue state management system (FSM). The start parameter triggers only once at the moment the start command is sent and serves merely as an entry point passing external context (referral ID, promo code, CRM request ID). In this article, we will break down protocol limitations, methods for preventing parameter spoofing, and a production-ready PHP implementation for handling deep links.
Telegram Bot API Deep Linking Mechanics and Strict Limits
Telegram imposes strictly regulated technical limitations on the start string parameter:
- Allowed characters: Latin letters
A-Z,a-z, digits0-9, as well as underscores_and hyphens-. Characters like=,?,&,%, or Cyrillic characters are prohibited. - Maximum length: exactly 64 bytes.
- Call specifics: if the user has launched the bot before, clicking the link
t.me/YourBot?start=payloadagain will open the dialogue with the bot, but the "Start" button at the bottom of the chat will appear again. The/start payloadmessage will be sent to the webhook only after physically clicking this button.
The main mistake of inexperienced developers is attempting to place unencoded JSON objects or key combinations like lead_123_user_456_promo_winter into the payload. This approach quickly hits the 64-byte limit and induces security vulnerabilities.
Securely Passing Context: Token Generation and DB Mapping
Passing plaintext entity IDs from a database (for example, ?start=order_1540) is extremely dangerous. Any user can manually replace the number in the URL and trigger business logic for someone else's order. You also cannot rely on sessions ($_SESSION), as the transition originates from the external Telegram application where web browser cookies are unavailable.
A secure approach consists of using one-time or temporary cryptographically secure tokens. The token is generated on the backend, saved in the database bound to the target entity, and then the final link is generated.
<?php
declare(strict_types=1);
function createLeadDeepLink(PDO $pdo, string $botUsername, string $entityType, int $entityId): string
{
// Генерируем 7 случайных байт и переводим в hex (ровно 14 символов, [a-f0-9])
$leadToken = bin2hex(random_bytes(7));
$stmt = $pdo->prepare('
INSERT INTO bot_deep_links (token, entity_type, entity_id, created_at, is_used)
VALUES (:token, :type, :id, NOW(), 0)
');
$stmt->execute([
':token' => $leadToken,
':type' => $entityType,
':id' => $entityId,
]);
return "https://t.me/{$botUsername}?start={$leadToken}";
}
The length of the resulting token is 14 bytes, which fits comfortably within the 64-byte limit. Even if an attacker attempts a brute-force attack, the 56-bit entropy makes the attack impractical.
Webhook Processing: Secret Token Verification and Duplicate Protection
Upon receiving a request from Telegram, two network security and reliability tasks must be addressed:
- Source authentication: validation of the
X-Telegram-Bot-Api-Secret-Tokenheader set when callingsetWebhook. - Idempotency: preventing re-processing of the same event during network failures (Telegram resends the Update if the backend does not respond with an HTTP 200 code in time). Deduplication is performed by
update_id.
<?php
declare(strict_types=1);
$secretToken = getenv('TELEGRAM_WEBHOOK_SECRET') ?: '';
$botToken = getenv('TELEGRAM_BOT_TOKEN') ?: '';
// 1. Валидация Secret Token
$incomingSecret = $_SERVER['HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN'] ?? '';
if ($secretToken !== '' && !hash_equals($secretToken, $incomingSecret)) {
http_response_code(403);
echo json_encode(['error' => 'Access denied']);
exit;
}
$rawInput = file_get_contents('php://input');
if (!$rawInput) {
http_response_code(400);
exit;
}
$update = json_decode($rawInput, true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($update)) {
http_response_code(400);
exit;
}
$pdo = new PDO('mysql:host=localhost;dbname=app_db;charset=utf8mb4', 'db_user', 'db_pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
// 2. Проверка идемпотентности по update_id
$updateId = (int)($update['update_id'] ?? 0);
if ($updateId > 0) {
$stmt = $pdo->prepare('INSERT IGNORE INTO processed_updates (update_id, created_at) VALUES (:id, NOW())');
$stmt->execute([':id' => $updateId]);
if ($stmt->rowCount() === 0) {
// Уведомление уже обрабатывалось ранее
echo json_encode(['ok' => true, 'description' => 'Duplicate update ignored']);
exit;
}
}
Parsing the /start Command and Executing Business Logic
Once request validation is complete, we extract the message text. A message from Telegram containing a deep link features a text like /start 4a8b1c9d0e2f3a. We need to separate the /start command from the payload itself and map it to a database record.
<?php
$message = $update['message'] ?? null;
$text = trim($message['text'] ?? '');
$chatId = $message['chat']['id'] ?? null;
if ($chatId && strpos($text, '/start') === 0) {
$parts = explode(' ', $text, 2);
$payload = isset($parts[1]) ? trim($parts[1]) : null;
handleStartCommand($pdo, $botToken, (int)$chatId, $payload);
}
echo json_encode(['ok' => true]);
function handleStartCommand(PDO $pdo, string $botToken, int $chatId, ?string $payload): void
{
if ($payload === null || $payload === '') {
$replyText = "Добро пожаловать! Вы запустили бота напрямую.";
sendTelegramMessage($botToken, $chatId, $replyText);
return;
}
// Проверяем соответствие payload регулярному выражению лимита Telegram
if (!preg_match('/^[a-zA-Z0-9_-]{1,64}$/', $payload)) {
sendTelegramMessage($botToken, $chatId, "Ошибка: недействительный формат ссылки.");
return;
}
// Ищем токен в БД
$stmt = $pdo->prepare('
SELECT id, entity_type, entity_id, is_used
FROM bot_deep_links
WHERE token = :token
LIMIT 1
');
$stmt->execute([':token' => $payload]);
$linkData = $stmt->fetch();
if (!$linkData) {
sendTelegramMessage($botToken, $chatId, "Ссылка не найдена или ее срок действия истек.");
return;
}
// Пример обработки сущности (например, привязка лида к CRM)
$entityType = $linkData['entity_type'];
$entityId = (int)$linkData['entity_id'];
// Делаем токен использованным (если диплинк одноразовый)
$updateStmt = $pdo->prepare('UPDATE bot_deep_links SET is_used = 1 WHERE id = :id');
$updateStmt->execute([':id' => $linkData['id']]);
$replyText = "Контекст успешно принят! Тип: <strong>{$entityType}</strong>, ID: <strong>{$entityId}</strong>.";
sendTelegramMessage($botToken, $chatId, $replyText);
}
Interacting with the Telegram Bot API via cURL
To send messages in Telegram, you should not use the file_get_contents function, as it does not provide flexible timeout settings or proper network error handling. We use cURL with explicit checks for HTTP response codes and the JSON response structure of the Telegram API.
<?php
declare(strict_types=1);
function sendTelegramMessage(string $botToken, int $chatId, string $text): array
{
$url = "https://api.telegram.org/bot{$botToken}/sendMessage";
$payload = [
'chat_id' => $chatId,
'text' => $text,
'parse_mode' => 'HTML',
'disable_web_page_preview' => true,
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 10,
]);
$rawResponse = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($rawResponse === false) {
throw new RuntimeException("cURL Error: {$curlError}");
}
if ($httpCode !== 200) {
throw new RuntimeException("Telegram API returned HTTP code {$httpCode}: {$rawResponse}");
}
$response = json_decode((string)$rawResponse, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException("Failed to decode Telegram JSON response");
}
if (empty($response['ok'])) {
$code = $response['error_code'] ?? 'unknown';
$desc = $response['description'] ?? 'no description';
throw new RuntimeException("Telegram API Error [{$code}]: {$desc}");
}
return $response;
}
Approach Comparison: Token Mapping vs Signed Payload (HMAC)
There are two main architectural patterns for working with start=payload:
1. Mapping via a Random Token in the Database (Stateful)
The server generates a token (for example, via bin2hex(random_bytes(7))) and writes the mapping to a table. An SQL query is executed during webhook processing.
- Pros: complete security (no real IDs in the link), the ability to revoke the link at any moment, ability to track the
is_usedstatus. - Cons: requires a database query on every click.
2. HMAC Data Signature (Stateless)
If writing to a DB is undesirable, parameters are packaged and signed with a backend secret key: payload = base64url(data) + '.' + hmac_sha256(data, secret). Due to the 64-byte limit, the signature must be truncated (for example, to 8–12 characters).
- Pros: no database is required for authentication.
- Cons: the strict 64-byte limit severely restricts payload size; reusing the link cannot be prevented without a table of used signatures.
For most commercial tasks (authentication, order mapping, сквозная аналитика of leads), the option of storing a short token in the database proves to be more flexible and reliable.
If you require professional интеграция Telegram with company CRMs and services, the BotCreator team will develop a fault-tolerant turnkey solution.