Deep linking в Telegram — мощный инструмент для направления пользователей на конкретные действия (запись на услугу, открытие карточки, бронирование). Однако использование t.me/Bot?start=payload требует строгого соблюдения ограничений API и защиты от подмены данных.
ЛИМИТЫ payload и ограничения API
Telegram позволяет передавать в callback_data не более 64 байт. Это критично для безопасности и производительности. Если payload слишком большой, сервер отклоняет запрос или он просто не проходит валидацию. Также учтите, что payload должен быть UTF-8 кодированным строковым значением.
МАППИНГ start → сущность в БД
При получении t.me/Bot?start=payload вы должны сопоставить этот payload с существующей сущностью в базе данных. Стандартная практика — генерировать уникальный lead_id (bin2hex(random_bytes(7))) и сохранять его вместе с payload. Для идемпотентности используйте update_id из webhook‑ответа как ключ для проверки дубликатов.
БЕЗОПАСНОСТЬ: защита от подмены payload
Подмена callback_data — классическая атака, которая может привести к несанкционированному действию бота. Чтобы защититься:
- Всегда валидируйте update_id из webhook перед обработкой payload.
- Храните map в БД с timestamp и проверяйте, что current_update_id не совпадает с сохраненным.
- Используйте HMAC‑SHA256 для подписи важных параметров, если бизнес‑логика требует дополнительной проверки.
Практическая реализация
Ниже приведены примеры кода на PHP (Yii2/Laravel) для отправки deep‑link и его обработки с идемпотентностью.
// Отправка 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');
}
}
Пример сохранения в БД (псевдокод для ясности):
// Таблица 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;
}
Запомните: всегда проверяйте HTTP статус (200 OK), используйте json_last_error() для диагностики, и храните update_id вместе с payload для гарантии идемпотентности. Это минимизирует риск подмены и обеспечивает стабильную работу вашего бота.