Deep linking в Telegram — powerful tool for directing users to specific actions (signing up for a service, opening a card, booking). However, using t.me/Bot?start=payload requires strict adherence to API limitations and protection against data tampering.
\n\nLIMITS of payload and API restrictions
\nTelegram allows passing up to 64 bytes in callback_data. This is critical for security and performance. If the payload is too large, the server rejects the request or it simply fails validation. Also note that the payload must be a UTF-8 encoded string value.
\n\nMAPPING start → entity in DB
\nUpon receiving t.me/Bot?start=payload you must match this payload with an existing entity in the database. The standard practice is to generate a unique lead_id (bin2hex(random_bytes(7))) and save it together with the payload. For idempotency use update_id from the webhook response as a key for duplicate checking.
\n\nSECURITY: protection against payload substitution
\nPayload substitution is a classic attack that can lead to unauthorized bot action. To protect yourself:
\n- \n
- Always validate the update_id from the webhook before processing the payload. \n
- Store the map in the DB with a timestamp and check that the current_update_id does not match the saved one. \n
- Use HMAC‑SHA256 to sign important parameters if business logic requires additional verification. \n
PRACTICAL IMPLEMENTATION
\nBelow are code examples in PHP (Yii2/Laravel) for sending a deep‑link and its handling with idempotency.
\n\n// Отправка deep link через webhook с валидацией
$url = 'https://api.telegram.org/bot/sendMessage';
$data = [
'text' => 'Запись на услугу',
'callback_data' => 'deep_link_abc123def456', // ≤ 64 байтов
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer '
]);
$response = curl_exec($ch);
$body = json_decode($response, true);
if (!isset($body['ok']) || $body['ok'] !== true) {
throw new RuntimeException('Telegram API rejected request');
}
curl_close($ch);
// Обработка deep link после получения
function handleDeepLink($updateId, $callbackData) {
// Генерация lead_id для идемпотентности
$leadId = bin2hex(random_bytes(7));
// Проверка, что payload соответствует ожидаемому формату
if (strpos($callbackData, 'deep_link_') !== false) {
// Извлекаем сущность
$entityType = substr(str_replace(['deep_link_', ''], '', $callbackData), 0, strlen($callbackData) - 16);
$payload = explode('|', $callbackData)[1] ?? null;
// Сохраняем в БД (пример)
$record = new DeepLinkRecord($leadId, $entityType, 1, ['name' => $payload]);
$db->save($record);
// Сохраняем update_id для идемпотентности
$updatedUpdateId = $updateId;
} else {
throw new RuntimeException('Invalid deep link payload');
}
}
\n\nExample saving to DB (pseudocode for clarity):
\n// Таблица deep_link_entities
// columns: id, lead_id (PK), entity_type, record_id, created_at
// Создание записи при first time
$leadId = bin2hex(random_bytes(7));
$entity = 'service'; // или 'appointment', 'product'
$record = new EntityRecord($leadId, $entity, 1, ['name' => 'Услуга А']);
$db->insert('deep_link_entities', [
'lead_id' => $leadId,
'entity_type' => $entity,
'record_id' => $record->id,
'created_at' => date('c')
]);
// Проверка дубликатов при повторном запросе
$existing = $db->find('deep_link_entities', ['lead_id' => $leadId'])
->where('entity_type', $entity)
->first();
if ($existing) {
// Payload уже обработан — возвращаем результат предыдущей операции
return $existing->processedPayload;
}\n\nRemember: always check the HTTP status (200 OK), use json_last_error() for diagnostics, and store the update_id together with the payload to guarantee idempotency. This minimizes the risk of tampering and ensures stable operation of your bot.
"}