Making standard HTTP requests to the Telegram Bot API using raw PHP requires more than a simple file_get_contents() call. Production environments demand explicit timeout controls, network error verification, valid JSON parsing, handling of Telegram's API-level error payloads, and strict output escaping for formatted text.
In this tutorial, we will build a production-ready PHP function to execute sendMessage requests over cURL. We will cover network timeouts, HTTP status validation, JSON decoding safety, Telegram-level error handling, and escaping dynamic strings when sending HTML-formatted messages. We will not cover setting up webhook handlers, processing inbound updates, or building background queue processors.
Sanitizing Dynamic Inputs for Telegram HTML
When using parse_mode=HTML, Telegram supports a subset of HTML tags (<b>, <i>, <a>, <code>, <pre>). If dynamic data inserted into your template contains characters such as <, >, or &, the Telegram API will fail to parse the entity tree and return a 400 Bad Request error with the description Bad Request: can't parse entities.
Use htmlspecialchars() with ENT_QUOTES | ENT_SUBSTITUTE to encode raw user inputs before interpolating them into formatted message strings:
function sanitizeTelegramHtml(string $text): string
{
return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
// Usage example:
$userInput = 'User <script>alert(1)</script> & "special"';
$safeUser = sanitizeTelegramHtml($userInput);
$formattedMessage = "New lead received:\n<b>Name:</b> {$safeUser}";
Building the cURL Messenger
To make reliable HTTP POST requests, configure native cURL with connection and total execution timeouts. The script should explicitly check for three layers of failure:
1. Transport Layer: cURL errors (e.g., DNS resolution failure, network timeout). 2. HTTP Layer: Non-200 HTTP response codes. 3. Application Layer: Telegram API standard JSON structure where ok is false.
Here is the complete implementation:
<?php
declare(strict_types=1);
/**
* Sends a message to a Telegram chat using cURL.
*
* @param int|string $chatId Unique identifier for the target chat or username
* @param string $text Text of the message to be sent
* @param string $parseMode Optional formatting (e.g., 'HTML')
* @return array<string, mixed>
* @throws RuntimeException On transport, HTTP, or Telegram API failures
*/
function sendTelegramMessage(int|string $chatId, string $text, string $parseMode = 'HTML'): array
{
$botToken = getenv('TELEGRAM_BOT_TOKEN');
if (!$botToken) {
throw new RuntimeException('TELEGRAM_BOT_TOKEN environment variable is missing.');
}
$url = sprintf('https://api.telegram.org/bot%s/sendMessage', $botToken);
$payload = [
'chat_id' => $chatId,
'text' => $text,
];
if ($parseMode !== '') {
$payload['parse_mode'] = $parseMode;
}
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5, // Max time allowed to establish connection
CURLOPT_TIMEOUT => 10, // Max total time allowed for request execution
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
],
]);
$response = curl_exec($ch);
$curlErrno = curl_errno($ch);
$curlError = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// 1. Check cURL network transport errors
if ($curlErrno !== 0) {
throw new RuntimeException(sprintf('cURL error [%d]: %s', $curlErrno, $curlError));
}
if (!is_string($response) || $response === '') {
throw new RuntimeException('Received empty response from Telegram API server.');
}
// 2. Decode and validate JSON format
$data = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException(sprintf(
'Failed to parse Telegram JSON response. Error: %s. Raw response: %s',
json_last_error_msg(),
$response
));
}
// 3. Check Telegram Application level status
if (!isset($data['ok']) || $data['ok'] !== true) {
$errorCode = $data['error_code'] ?? $httpCode;
$description = $data['description'] ?? 'Unknown API error occurred.';
throw new RuntimeException(sprintf(
'Telegram API Error [%d]: %s',
$errorCode,
$description
));
}
return $data;
}
Executing the Code
You can run this function inside your request handler or CLI script by setting the environment variable:
export TELEGRAM_BOT_TOKEN="123456789:ABCdefGHIjklMNOpqrsTUVwxyZ"
php send_message.php
try {
$chatId = 123456789;
$rawMessage = "Order update: Status changed to <b>Processing</b>.";
$result = sendTelegramMessage($chatId, $rawMessage, 'HTML');
echo "Message sent successfully. Message ID: " . $result['result']['message_id'];
} catch (RuntimeException $e) {
error_log("Telegram delivery failed: " . $e->getMessage());
}
Production Considerations
- Rate Limits (HTTP 429): If Telegram responds with HTTP status 429 (Too Many Requests), inspect the parameters.retry_after payload field. If this occurs regularly, delegate outgoing messages to a database or Redis queue rather than executing cURL inside synchronous web HTTP requests. - User Blocks (HTTP 403): If a user has blocked your bot or deleted the chat, Telegram returns 403 Forbidden with the description Forbidden: bot was blocked by the user. Catch this error specifically to mark user records as inactive in your database. - JSON Payload Alternative: The example above uses application/x-www-form-urlencoded. If your payload includes complex nested arrays (like reply_markup inline keybaords), encode the whole payload with json_encode() and send it with Content-Type: application/json header instead.
Need to scale out custom automation, Webhooks, or serverless infrastructure for messaging systems? Contact BotCreator — studio that ships Telegram bots / Mini Apps.