This tutorial demonstrates how to integrate a website contact form with Telegram, allowing form submissions to be routed as messages to a group of managers. We will cover processing form data, generating a unique lead identifier, storing this identifier, and sending an interactive message to Telegram with an inline button for managers to claim the lead. We will not be covering front-end form validation or advanced database schema design, focusing solely on the backend PHP logic for Telegram integration.
1. Setting up the Environment and Telegram Bot
Before we dive into the PHP code, ensure you have a Telegram bot token and a chat ID for the group where managers will receive lead notifications. You can obtain a bot token from BotFather. To get the chat ID, add your bot to a group, send a message, and then visit https://api.telegram.org/botYOUR_BOT_TOKEN/getUpdates to find the chat.id.
For security, store your bot token and chat ID as environment variables or in a configuration file, not directly in your code. We'll use getenv() for this example.
// config.php or environment variables
// TELEGRAM_BOT_TOKEN=YOUR_BOT_TOKEN
// TELEGRAM_MANAGER_CHAT_ID=-123456789
2. Processing Form Submissions
Let's assume you have an HTML form on your website that submits data via POST to a PHP script. For this example, we'll simulate a form submission with name and email fields.
<?php
// Ensure this script is only accessible via POST requests
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405); // Method Not Allowed
echo 'Only POST requests are allowed.';
exit;
}
// Load environment variables (e.g., from a .env file or server configuration)
// For simplicity, we'll assume they are already loaded or set in the server environment.
$telegramBotToken = getenv('TELEGRAM_BOT_TOKEN');
$telegramManagerChatId = getenv('TELEGRAM_MANAGER_CHAT_ID');
if (!$telegramBotToken || !$telegramManagerChatId) {
error_log('Missing Telegram bot token or manager chat ID.');
http_response_code(500); // Internal Server Error
echo 'Configuration error.';
exit;
}
// Sanitize and validate input data
$name = filter_input(INPUT_POST, 'name', FILTER_SANITIZE_STRING);
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$message = filter_input(INPUT_POST, 'message', FILTER_SANITIZE_STRING);
if (!$name || !$email) {
http_response_code(400); // Bad Request
echo 'Invalid input data.';
exit;
}
// Generate a unique lead ID
// Using bin2hex(random_bytes(7)) generates a 14-character hexadecimal string,
// which is sufficiently unique for most applications and fits within typical database column sizes.
$leadId = bin2hex(random_bytes(7));
// Store lead data (e.g., in a database)
// For this example, we'll just simulate storage. In a real application,
// you would insert this into a 'leads' table with columns like id, name, email, message, created_at, status.
$dbConnection = new PDO('sqlite::memory:'); // Example: using SQLite in memory for demonstration
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$dbConnection->exec("CREATE TABLE IF NOT EXISTS leads (id TEXT PRIMARY KEY, name TEXT, email TEXT, message TEXT, created_at DATETIME)");
try {
$stmt = $dbConnection->prepare("INSERT INTO leads (id, name, email, message, created_at) VALUES (:id, :name, :email, :message, DATETIME('now'))");
$stmt->execute([
':id' => $leadId,
':name' => $name,
':email' => $email,
':message' => $message
]);
} catch (PDOException $e) {
error_log('Database error: ' . $e->getMessage());
http_response_code(500);
echo 'Failed to store lead.';
exit;
}
// Prepare message for Telegram
$telegramMessage = "<b>New Lead!</b>\n\n";
$telegramMessage .= "<b>ID:</b> " . htmlspecialchars($leadId) . "\n";
$telegramMessage .= "<b>Name:</b> " . htmlspecialchars($name) . "\n";
$telegramMessage .= "<b>Email:</b> " . htmlspecialchars($email) . "\n";
$telegramMessage .= "<b>Message:</b> " . htmlspecialchars($message) . "\n";
// Create an inline keyboard button for managers to 'take' the lead
// The callback_data must be a string and its length must not exceed 64 bytes.
// We'll use a simple format: 'take:LEAD_ID'
$callbackData = 'take:' . $leadId;
if (strlen($callbackData) > 64) {
error_log('Callback data exceeds 64 bytes: ' . $callbackData);
// Handle this edge case, e.g., by logging and not sending the button
$inlineKeyboard = [];
} else {
$inlineKeyboard = [
'inline_keyboard' => [
[
['text' => 'Take Lead', 'callback_data' => $callbackData]
]
]
];
}
// Send message to Telegram
$telegramApiUrl = "https://api.telegram.org/bot{$telegramBotToken}/sendMessage";
$postFields = [
'chat_id' => $telegramManagerChatId,
'text' => $telegramMessage,
'parse_mode' => 'HTML',
'reply_markup' => json_encode($inlineKeyboard)
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $telegramApiUrl);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($response === false) {
error_log('cURL error: ' . $curlError);
http_response_code(500);
echo 'Failed to send message to Telegram (cURL error).';
exit;
}
$responseData = json_decode($response, true);
if ($httpCode !== 200 || !isset($responseData['ok']) || $responseData['ok'] !== true) {
error_log('Telegram API error: HTTP ' . $httpCode . ' - ' . ($responseData['description'] ?? 'Unknown error'));
http_response_code(500);
echo 'Failed to send message to Telegram (API error).';
exit;
}
http_response_code(200);
echo 'Lead submitted successfully and sent to Telegram.';
?>
3. Handling the Callback Query (Manager's Action)
When a manager clicks the 'Take Lead' button, Telegram sends a callback_query update to your bot's webhook. You need a separate script (or a webhook handler that routes requests) to process this. This script will receive the callback_data we defined (take:LEAD_ID).
<?php
// This script assumes it's configured as your bot's webhook URL.
// It should only accept POST requests from Telegram.
$telegramBotToken = getenv('TELEGRAM_BOT_TOKEN');
if (!$telegramBotToken) {
error_log('Missing Telegram bot token for webhook handler.');
http_response_code(500);
exit;
}
// Read the incoming Telegram update
$input = file_get_contents('php://input');
$update = json_decode($input, true);
// Log the incoming update for debugging (optional)
// file_put_contents('telegram_updates.log', date('Y-m-d H:i:s') . "\n" . $input . "\n\n", FILE_APPEND);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log('Invalid JSON received: ' . json_last_error_msg());
http_response_code(400); // Bad Request
exit;
}
// Check if it's a callback query
if (isset($update['callback_query'])) {
$callbackQuery = $update['callback_query'];
$callbackData = $callbackQuery['data'];
$queryId = $callbackQuery['id'];
$chatId = $callbackQuery['message']['chat']['id'];
$messageId = $callbackQuery['message']['message_id'];
$fromUser = $callbackQuery['from'];
$managerName = htmlspecialchars($fromUser['first_name'] . (isset($fromUser['last_name']) ? ' ' . $fromUser['last_name'] : ''));
// Check if the callback data starts with 'take:'
if (strpos($callbackData, 'take:') === 0) {
$leadId = substr($callbackData, 5);
// In a real application, you would query your database here
// to check the lead's status and assign it to the manager.
// For demonstration, we'll simulate this.
$dbConnection = new PDO('sqlite::memory:'); // Re-initialize for this script, or use a shared connection
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$dbConnection->exec("CREATE TABLE IF NOT EXISTS leads (id TEXT PRIMARY KEY, name TEXT, email TEXT, message TEXT, created_at DATETIME, assigned_to TEXT DEFAULT NULL)");
// Attempt to retrieve the lead and assign it
try {
$stmt = $dbConnection->prepare("SELECT assigned_to FROM leads WHERE id = :id");
$stmt->execute([':id' => $leadId]);
$lead = $stmt->fetch(PDO::FETCH_ASSOC);
$responseMessage = '';
if ($lead) {
if ($lead['assigned_to'] === null) {
// Assign the lead to the manager
$updateStmt = $dbConnection->prepare("UPDATE leads SET assigned_to = :managerName WHERE id = :id");
$updateStmt->execute([':managerName' => $managerName, ':id' => $leadId]);
$responseMessage = "Lead {$leadId} assigned to {$managerName}.";
// Edit the original message to reflect the assignment
$originalText = $callbackQuery['message']['text'];
$newText = $originalText . "\n\n<i>Assigned to: {$managerName}</i>";
$this->editTelegramMessage($telegramBotToken, $chatId, $messageId, $newText, true); // Remove inline keyboard
} else {
$responseMessage = "Lead {$leadId} is already assigned to {$lead['assigned_to']}.";
}
} else {
$responseMessage = "Lead {$leadId} not found.";
}
// Answer the callback query to remove the loading state from the button
$this->answerCallbackQuery($telegramBotToken, $queryId, $responseMessage);
} catch (PDOException $e) {
error_log('Database error during lead assignment: ' . $e->getMessage());
$this->answerCallbackQuery($telegramBotToken, $queryId, 'Error processing lead assignment.');
}
}
}
// Always respond with 200 OK to Telegram to acknowledge receipt of the update
http_response_code(200);
class TelegramApi {
private function callApi(string $method, array $params = []): array
{
$telegramBotToken = getenv('TELEGRAM_BOT_TOKEN');
if (!$telegramBotToken) {
error_log('Telegram bot token not set.');
return ['ok' => false, 'description' => 'Bot token missing'];
}
$url = "https://api.telegram.org/bot{$telegramBotToken}/{$method}";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($response === false) {
error_log("cURL error for {$method}: " . $curlError);
return ['ok' => false, 'description' => 'cURL error: ' . $curlError];
}
$responseData = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log("JSON decode error for {$method}: " . json_last_error_msg() . " Response: " . $response);
return ['ok' => false, 'description' => 'Invalid JSON response'];
}
if ($httpCode !== 200 || !isset($responseData['ok']) || $responseData['ok'] !== true) {
error_log("Telegram API error for {$method}: HTTP {$httpCode} - " . ($responseData['description'] ?? 'Unknown error') . " Response: " . $response);
return ['ok' => false, 'description' => $responseData['description'] ?? 'API error'];
}
return $responseData;
}
public function answerCallbackQuery(string $botToken, string $callbackQueryId, string $text, bool $showAlert = false):
void {
$params = [
'callback_query_id' => $callbackQueryId,
'text' => $text,
'show_alert' => $showAlert ? 'true' : 'false'
];
$this->callApi('answerCallbackQuery', $params);
}
public function editTelegramMessage(string $botToken, int $chatId, int $messageId, string $newText, bool $removeKeyboard = false):
void {
$params = [
'chat_id' => $chatId,
'message_id' => $messageId,
'text' => $newText,
'parse_mode' => 'HTML'
];
if ($removeKeyboard) {
$params['reply_markup'] = json_encode(['inline_keyboard' => []]); // Empty keyboard to remove it
}
$this->callApi('editMessageText', $params);
}
}
// Instantiate the class and call the method
$this = new TelegramApi();
?>
4. Production Notes and Considerations
* Security: Always validate and sanitize all user inputs. For webhooks, implement a secret_token to verify that incoming requests are from Telegram. This is crucial to prevent unauthorized access to your webhook endpoint. You can set a secret_token when setting your webhook via setWebhook API method and then verify the X-Telegram-Bot-Api-Secret-Token header in your PHP script. * Database: The SQLite in-memory database used here is for demonstration only. In a production environment, you would use a persistent database like MySQL, PostgreSQL, or a file-based SQLite database, ensuring proper connection management and error handling. * Idempotency: For webhook handlers, it's good practice to ensure idempotency. Telegram sends update_id with each update. You can store the last processed update_id in your database or a cache (like Redis) and ignore updates with an update_id less than or equal to the last processed one. This prevents duplicate processing if Telegram retries sending an update. * Error Handling and Logging: Implement robust error handling and logging. Use error_log() to write errors to your server's error log, and consider a more sophisticated logging solution for production. Always return appropriate HTTP status codes. * Asynchronous Processing: For heavy processing or external API calls, consider using a message queue (e.g., RabbitMQ, Redis Queue, AWS SQS) to process Telegram updates asynchronously. This prevents your webhook from timing out and improves responsiveness. * callback_data Length: The 64-byte limit for callback_data is strict. If your lead IDs or other data exceed this, you might need to store a mapping in your database (e.g., short_id -> long_id) and send only the short_id in the callback_data. * Telegram API Rate Limits: Be mindful of Telegram Bot API rate limits. If you're sending many messages or making many API calls, you might hit limits. Implement retry logic with exponential backoff if you encounter 429 Too Many Requests errors. * User Interface: When a manager takes a lead, editing the original message to show who took it (and removing the inline button) provides clear feedback and prevents multiple managers from trying to claim the same lead simultaneously.
This setup provides a solid foundation for integrating website forms with Telegram for lead management. By following these guidelines, you can build a reliable and secure system.
BotCreator — studio that ships Telegram bots / Mini Apps.