Recommended Free Tools
To send a Telegram text message from PHP, POST chat_id and text to the bot’s sendMessage method with cURL, then judge the outcome in three separate steps: did cURL receive a response at all, what HTTP status came back, and does Telegram’s JSON envelope report ok as true. Only the third step confirms that Telegram accepted the message. A 200 status with an unreadable body is not a sent message.
What you need before you write code
- A bot token issued for your bot. The token is part of the request URL, so it must be handled as a credential throughout this article.
- The
chat_idof the chat you want to message. This is the destination identifier Telegram expects in the request body. - PHP with the cURL extension enabled, so that
curl_init(),curl_exec(), andcurl_getinfo()are available. - Outbound HTTPS access from your server to
api.telegram.org. If this is blocked, every request will fail at the transport layer described below.
The three layers of a Telegram send
Most broken integrations treat “something came back” as “it worked.” Telegram’s API sits on top of ordinary HTTPS, so a single call passes through three distinct layers, and each one can fail independently. Keep them separate in your code and your logs.
As an Amazon Associate I earn from qualifying purchases.
| Layer | How you detect it in PHP | What a failure means | What to record |
|---|---|---|---|
| Transport | curl_exec() returns false |
No HTTP exchange completed, so there is no response body to parse | curl_errno() and curl_error() |
| HTTP | curl_getinfo($ch, CURLINFO_HTTP_CODE) |
The server responded, and the status code describes how it handled the request | The status code and a redacted excerpt of the body |
| Telegram envelope | Decode the JSON, then check ok |
ok is false: the method was rejected. ok is true: the method succeeded |
error_code, description, and parameters when present |
The sendMessage request
The Bot API is an HTTPS interface. A method call uses the URL https://api.telegram.org/bot<token>/sendMessage, and the official Bot API documentation supports both GET and POST with several parameter encodings. For a text message, POST is the sensible choice because the text can be long and should not appear in a URL that may be logged by web servers or proxies.
sendMessage requires two parameters: chat_id and text. A successful call returns a Message object in the result field. Telegram limits text to 1 to 4096 characters after entity parsing. That limit is measured on the text Telegram actually processes, not on the raw string your code builds. If you send formatted text, a raw length check in PHP is therefore only an approximation. Use it as a guard against obviously invalid input, and rely on the API’s description for the authoritative rejection.
#1 Best Overall
Choosing form encoding or JSON
Telegram documents both form encoding and JSON for non-file requests. For a text-only sender, the choice is mostly about consistency with the rest of your codebase.
| Consideration | Form-encoded POST | JSON body |
|---|---|---|
| How PHP builds it | http_build_query() on an associative array, as in the code below |
json_encode() plus a Content-Type: application/json header |
| Special characters | Encoded by http_build_query() |
Encoded by json_encode(); check for false on failure |
| Fit with the rest of the application | Matches classic PHP form posts and simple scripts | Matches services that already exchange JSON |
| File uploads | Usable for files, which Telegram does not accept as JSON | Not usable for file requests, so a codebase that may send files later will still need form-style requests |
The examples below use form encoding because it keeps the request construction to a single line and avoids a JSON encoding failure path for a text-only sender.
A reusable sender with separated error branches
The function below returns an associative array in every case. Each return value states which layer produced it, so the caller never has to guess whether a failure happened on the network or inside Telegram. The timeout values are example settings for this sketch, not Telegram requirements.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errorsRank #2
<?php
declare(strict_types=1);
function sendTelegramMessage(string $botToken, string $chatId, string $text): array
{
// Telegram counts the limit after entity parsing, so this raw check is only a guard.
if ($text === '' || mb_strlen($text, 'UTF-8') > 4096) {
return ['ok' => false, 'stage' => 'input', 'description' => 'text must be 1 to 4096 characters'];
}
$ch = curl_init('https://api.telegram.org/bot' . $botToken . '/sendMessage');
if ($ch === false) {
return ['ok' => false, 'stage' => 'init', 'description' => 'curl_init failed'];
}
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query(['chat_id' => $chatId, 'text' => $text]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 15,
]);
$body = curl_exec($ch);
// Layer 1: transport. No HTTP exchange completed, so there is no body to parse.
if ($body === false) {
$result = [
'ok' => false,
'stage' => 'transport',
'curl_errno' => curl_errno($ch),
'curl_error' => curl_error($ch),
];
curl_close($ch);
return $result;
}
// Layer 2: HTTP status. Recorded, but it does not decide success on its own.
$httpStatus = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Layer 3: Telegram envelope. Only ok === true with a result means the method succeeded.
$data = json_decode($body, true);
if (!is_array($data) || !isset($data['ok']) || !is_bool($data['ok'])) {
return ['ok' => false, 'stage' => 'malformed', 'http_status' => $httpStatus];
}
if ($data['ok'] === false) {
return [
'ok' => false,
'stage' => 'telegram',
'http_status' => $httpStatus,
'error_code' => $data['error_code'] ?? null,
'description' => $data['description'] ?? null,
'parameters' => $data['parameters'] ?? null,
];
}
if (!isset($data['result']) || !is_array($data['result'])) {
return ['ok' => false, 'stage' => 'malformed', 'http_status' => $httpStatus];
}
return ['ok' => true, 'http_status' => $httpStatus, 'message' => $data['result']];
}
function redactForLog(string $line, string $token): string
{
return str_replace($token, '***', $line);
}
$token = (string) getenv('TG_BOT_TOKEN');
if ($token === '') {
error_log('Telegram: TG_BOT_TOKEN is not set');
exit(1);
}
$send = sendTelegramMessage($token, '123456789', 'Deploy finished.');
if ($send['ok'] !== true) {
error_log(redactForLog('Telegram send failed: ' . json_encode($send), $token));
}
Why each branch is separate
- The transport branch runs only when
curl_exec()returnsfalse. Reading the body would be meaningless here, so the function reportscurl_errnoandcurl_errorand stops. - The HTTP status is captured before the body is interpreted. It is useful diagnostic data, but the function never treats a 200 as proof of delivery.
- The envelope check is the only path to success. An
okvalue that is missing or not a Boolean is classified as malformed, not as a Telegram rejection, because Telegram’s documented envelope was not present. - The success path also requires a
resultobject. The returnedmessageis theMessageobject, which is the record you would store if you need the sent message’s identifier.
Reading Telegram’s response envelope
The Bot API documentation describes the response envelope in these terms:
“The response contains a JSON object, which always has a Boolean field ‘ok’ and may have an optional String field ‘description’ with a human-readable description of the result.”
In practice, a rejected call also carries an integer error_code, and may carry parameters with extra detail. Treat these fields in the following way:
okdecides success. Nothing else in the envelope overrides it.descriptionis for people reading logs. It is human-readable text, so do not build control flow around matching its wording.error_codeis useful for logging and alerting. Telegram warns that the contents oferror_codemay change, so do not hard-code a long list of numeric branches as if they were permanent.parametersis optional. Read it when it is present, and do not require it.
Troubleshooting each failure branch
curl_exec() returns false
The request never produced a usable HTTP response. Read curl_errno and curl_error from the returned array, and confirm outbound HTTPS to api.telegram.org from the same host and user that runs PHP. A transport failure says nothing about the bot token or the chat, so do not change either while investigating it.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →A non-200 status with a Telegram JSON body
Telegram’s rejections arrive as a JSON envelope, and the HTTP status is recorded alongside it. When ok is false, the stage is telegram, and the description is the first thing to read. The HTTP status is context, not the verdict: the envelope is what tells you the method was rejected.
A non-200 status, or a 200, with no usable JSON
If the body does not decode into the expected object, the stage is malformed. This is the branch to inspect when a proxy, firewall, or captive portal answers in place of Telegram, since such intermediaries return HTML or plain text. Record the status and a short redacted excerpt of the body so you can see what actually answered.
Rank #4
ok is false and the description points at the input
Check the chat_id and the text you passed. Confirm that the chat_id identifies the chat you intended, and that the text is not empty. The required parameters are documented, but the exact wording of each rejection depends on the failure, so use the description together with parameters when they are present.
The rejection suggests a token problem
The Telegram PHP sample’s error handling includes a case for an invalid access token, so a rejection that points at the credential should be treated as a configuration issue. Check the value of TG_BOT_TOKEN in the environment the script actually runs in, including any stray whitespace or a token from a different bot. Do not paste the token into a ticket or a log to compare it.
Logging without leaking the token
The token is embedded in the request URL, so anything that prints the URL prints the credential. Follow three rules:
- Never log the full request URL, and never include it in exception messages.
- Pass any text you are about to log through a redaction step that replaces the token. The
redactForLog()helper above does this. Apply it tocurl_errortext as well, since error strings are written by the library rather than by your code. - Do not log the message
textby default. It can contain user data, and the failure reason rarely depends on its contents.
This is operational guidance rather than a rule Telegram states in quoted form, but it follows directly from where the credential sits in the endpoint.
Production hardening
The sketch above is a starting point, not a complete client. Before relying on it, decide the following:
- Timeouts. Set a connection timeout and a total timeout that suit your workload. The values in the code are examples. Telegram does not prescribe them.
- Retries. Bound them by attempt count and by the error type. Retry transport failures and some HTTP-level failures, but do not retry a
telegramrejection for bad input, since it will fail the same way again. The official PHP sample shows its own retry handling for server errors, but that delay is an illustration, not a universal policy. - Duplicate sends. If a request times out after Telegram has already processed it, a retry can deliver the same text twice. Decide whether a duplicate is acceptable for your application before adding retries, and store the returned
message_idwhen you need to reconcile. - Encoding failures. If you switch to JSON, check the result of
json_encode()and treatfalseas an input error. - Resource cleanup. Close the cURL handle on every path, including the early returns, as the function above does.
- Input validation. Reject empty text, and validate the chat identifier format your application uses before it reaches the API.
Checking a successful send
A call is successful when the returned array has ok set to true, the stage key is absent, and message holds the Message object from Telegram. Confirm delivery in the target chat during your first run, because a correct return value only shows that Telegram accepted the method call.
Free tools Windows power users keep installed
One-click scans. No signup required.
Version and scope
This walkthrough follows the Telegram Bot API documentation, which is labelled “Bot API 10.3” and dated August 24, 2026, as of this writing in October 2026. Method details and fields can change between versions, so check the Telegram Bot API documentation when you upgrade your integration. The article covers outbound text messages only. Receiving updates from users is a separate topic. For the official PHP cURL example that this sender’s structure follows, see Telegram’s Hellobot PHP sample.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




