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}";
Enter fullscreen mode Exit fullscreen mode
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:
- Transport Layer: cURL errors (e.g., DNS resolution failure, network timeout).
- HTTP Layer: Non-200 HTTP response codes.
-
Application Layer: Telegram API standard JSON structure where
okisfalse.
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;
}
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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());
}
Enter fullscreen mode Exit fullscreen mode
Production Considerations
-
Rate Limits (HTTP 429): If Telegram responds with HTTP status 429 (
Too Many Requests), inspect theparameters.retry_afterpayload 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 Forbiddenwith the descriptionForbidden: 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 (likereply_markupinline keybaords), encode the whole payload withjson_encode()and send it withContent-Type: application/jsonheader instead.
Need to scale out custom automation, Webhooks, or serverless infrastructure for messaging systems? Contact BotCreator — studio that ships Telegram bots / Mini Apps.