When building custom landing pages, routing contact form submissions directly to a Telegram group allows sales managers to claim leads instantly. This tutorial demonstrates how to implement a secure lead-routing pipeline in native PHP.
We will generate a cryptographically secure unique identifier, store the lead in a MySQL database, dispatch it to Telegram with an inline "Take Lead" button, and handle the callback query when a manager claims it. This implementation avoids framework dependencies and uses native PHP functions.
Database Schema
To manage the state of each lead, we need a database table. Instead of using auto-incrementing integers (which expose lead volume) or standard UUIDs (which consume 36 characters), we generate a 14-character hexadecimal string. This keeps our Telegram callback_data payload well under the 64-byte limit.
CREATE TABLE leads (
id VARCHAR(14) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
phone VARCHAR(50) NOT NULL,
status VARCHAR(20) DEFAULT 'new',
assigned_manager_id VARCHAR(100) DEFAULT NULL,
assigned_manager_name VARCHAR(255) DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Processing the Form Submission
When a user submits a form on your website, your backend must generate the unique ID, insert the record into the database, and format a message for the Telegram Bot API.
We use bin2hex(random_bytes(7)) to generate a 14-character unique ID. We also use htmlspecialchars to escape user input before sending it to Telegram with parse_mode=HTML to prevent message parsing errors or HTML injection.
<?php
// submit.php
require_once 'config.php'; // Contains PDO connection $pdo, BOT_TOKEN, and TARGET_CHAT_ID
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit('Method Not Allowed');
}
$name = trim($_POST['name'] ?? '');
$phone = trim($_POST['phone'] ?? '');
if (empty($name) || empty($phone)) {
http_response_code(400);
exit('Bad Request: Missing fields');
}
// Generate a unique 14-character hex ID (7 bytes)
$leadId = bin2hex(random_bytes(7));
// Insert into database
$stmt = $pdo->prepare("INSERT INTO leads (id, name, phone) VALUES (:id, :name, :phone)");
$stmt->execute([
'id' => $leadId,
'name' => $name,
'phone' => $phone
]);
// Format the Telegram message
$escapedName = htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
$escapedPhone = htmlspecialchars($phone, ENT_QUOTES, 'UTF-8');
$text = "<b>New Lead Received!</b>\n\n";
$text .= "<b>ID:</b> {$leadId}\n";
$text .= "<b>Name:</b> {$escapedName}\n";
$text .= "<b>Phone:</b> {$escapedPhone}";
// Build the inline keyboard. The callback_data is "take:" followed by the 14-char ID (19 bytes total)
$keyboard = [
'inline_keyboard' => [
[
[
'text' => '📥 Take Lead',
'callback_data' => 'take:' . $leadId
]
]
]
];
// Send to Telegram
$telegramUrl = "https://api.telegram.org/bot" . BOT_TOKEN . "/sendMessage";
$postData = [
'chat_id' => TARGET_CHAT_ID,
'text' => $text,
'parse_mode' => 'HTML',
'reply_markup' => json_encode($keyboard)
];
$ch = curl_init($telegramUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false || $httpCode !== 200) {
// Log error for debugging
error_log("Telegram API error: Status {$httpCode}, Response: {$response}");
http_response_code(500);
exit('Internal Server Error');
}
$result = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE || !isset($result['ok']) || !$result['ok']) {
error_log("Telegram API returned ok:false or invalid JSON");
http_response_code(500);
exit('Internal Server Error');
}
echo json_encode(['success' => true, 'lead_id' => $leadId]);
Handling the Webhook Callback
When a manager clicks the "Take Lead" button, Telegram sends a POST request (a callback_query) to your webhook URL. Your script must:
1. Verify the webhook authenticity. 2. Parse the callback_data. 3. Check if the lead is already claimed. 4. Update the database. 5. Answer the callback query to clear the loading state on the manager's screen. 6. Edit the original message to remove the button and display the claiming manager's name.
<?php
// webhook.php
require_once 'config.php';
// Verify Telegram Webhook Secret Token if configured
$secretToken = $_SERVER['HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN'] ?? '';
if (defined('WEBHOOK_SECRET_TOKEN') && $secretToken !== WEBHOOK_SECRET_TOKEN) {
http_response_code(403);
exit('Forbidden');
}
$input = file_get_contents('php://input');
$update = json_decode($input, true);
if (!$update || !isset($update['callback_query'])) {
exit('No callback query to process');
}
$callbackQuery = $update['callback_query'];
$callbackQueryId = $callbackQuery['id'];
$callbackData = $callbackQuery['data'] ?? '';
$from = $callbackQuery['from'];
$managerId = $from['id'];
$managerName = trim(($from['first_name'] ?? '') . ' ' . ($from['last_name'] ?? ''));
$message = $callbackQuery['message'];
$messageId = $message['message_id'];
$chatId = $message['chat']['id'];
// Validate action
if (strpos($callbackData, 'take:') !== 0) {
exit('Unknown action');
}
$leadId = substr($callbackData, 5); // Extract the 14-character ID
// Fetch the lead and check its status
$stmt = $pdo->prepare("SELECT * FROM leads WHERE id = :id LIMIT 1");
$stmt->execute(['id' => $leadId]);
$lead = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$lead) {
sendAnswerCallbackQuery($callbackQueryId, "Error: Lead not found.", true);
exit;
}
if ($lead['status'] !== 'new') {
$alreadyAssigned = htmlspecialchars($lead['assigned_manager_name'], ENT_QUOTES, 'UTF-8');
sendAnswerCallbackQuery($callbackQueryId, "This lead has already been taken by {$alreadyAssigned}.", true);
exit;
}
// Update lead status in database
$updateStmt = $pdo->prepare("UPDATE leads SET status = 'taken', assigned_manager_id = :manager_id, assigned_manager_name = :manager_name WHERE id = :id AND status = 'new'");
$updateStmt->execute([
'manager_id' => $managerId,
'manager_name' => $managerName,
'id' => $leadId
]);
if ($updateStmt->rowCount() === 0) {
// Concurrency safety check: another query updated it first
sendAnswerCallbackQuery($callbackQueryId, "Conflict: This lead was just claimed by someone else.", true);
exit;
}
// Acknowledge the callback query
sendAnswerCallbackQuery($callbackQueryId, "You have claimed this lead!", false);
// Update the original Telegram message to show who claimed it
$escapedName = htmlspecialchars($lead['name'], ENT_QUOTES, 'UTF-8');
$escapedPhone = htmlspecialchars($lead['phone'], ENT_QUOTES, 'UTF-8');
$escapedManager = htmlspecialchars($managerName, ENT_QUOTES, 'UTF-8');
$newText = "<b>Lead Claimed!</b>\n\n";
$newText .= "<b>ID:</b> {$leadId}\n";
$newText .= "<b>Name:</b> {$escapedName}\n";
$newText .= "<b>Phone:</b> {$escapedPhone}\n\n";
$newText .= "✅ <b>Claimed by:</b> {$escapedManager}";
updateTelegramMessage($chatId, $messageId, $newText);
/**
* Helper to answer callback query
*/
function sendAnswerCallbackQuery($callbackQueryId, $text, $showAlert = false) {
$url = "https://api.telegram.org/bot" . BOT_TOKEN . "/answerCallbackQuery";
$postData = [
'callback_query_id' => $callbackQueryId,
'text' => $text,
'show_alert' => $showAlert
];
executeCurl($url, $postData);
}
/**
* Helper to edit message text and remove inline keyboard
*/
function updateTelegramMessage($chatId, $messageId, $text) {
$url = "https://api.telegram.org/bot" . BOT_TOKEN . "/editMessageText";
$postData = [
'chat_id' => $chatId,
'message_id' => $messageId,
'text' => $text,
'parse_mode' => 'HTML',
'reply_markup' => json_encode(['inline_keyboard' => []]) // Removes the button
];
executeCurl($url, $postData);
}
/**
* Generic cURL executor
*/
function executeCurl($url, $postData) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
$res = curl_exec($ch);
curl_close($ch);
return $res;
}
Production Considerations
### Webhook Security Always configure a secret_token when setting up your webhook via setWebhook. Verify this token on every incoming request using the X-Telegram-Bot-Api-Secret-Token header. This prevents malicious actors from spoofing callback payloads to your server.
### Callback Data Size Limit Telegram limits the callback_data field to exactly 64 bytes. If you attempt to pass large JSON payloads or long strings inside this field, the API will reject the message payload. By generating a short, fixed-length database key (bin2hex(random_bytes(7)) which yields 14 characters), we ensure that our action prefix and ID fit comfortably within this limit.
### Concurrency and Race Conditions In active sales teams, multiple managers might click the "Take Lead" button at the exact same moment. The SQL update statement uses a strict conditional check: WHERE id = :id AND status = 'new'. Checking rowCount() ensures that only the manager whose query executed first is assigned the lead, preventing double-allocation.
Need assistance scaling your Telegram integrations or building complex interactive flows? Contact BotCreator — studio that ships Telegram bots / Mini Apps.