DriversRecommendedOutdated drivers can make a good PC feel brokenScan driver issues before chasing fixes manually.Scan NowOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsWindows FixRecommendedWindows errors stealing your time? Find the fix fastScan stability, cleanup and performance issues.Fix Now×
Skip to content
World desk9 min

Send Telegram Messages via cURL in PHP with Robust Error Handling

A reusable PHP cURL sender for Telegram's sendMessage method that separates transport failures, HTTP status, and Telegram API rejections, and keeps the bot token out of logs.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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_id of 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(), and curl_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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
<?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() returns false. Reading the body would be meaningless here, so the function reports curl_errno and curl_error and 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 ok value 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 result object. The returned message is the Message object, 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:

  • ok decides success. Nothing else in the envelope overrides it.
  • description is for people reading logs. It is human-readable text, so do not build control flow around matching its wording.
  • error_code is useful for logging and alerting. Telegram warns that the contents of error_code may change, so do not hard-code a long list of numeric branches as if they were permanent.
  • parameters is 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

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 to curl_error text as well, since error strings are written by the library rather than by your code.
  • Do not log the message text by 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 telegram rejection 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_id when you need to reconcile.
  • Encoding failures. If you switch to JSON, check the result of json_encode() and treat false as 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Outdated Drivers Are Slowing You DownFree scan - exact matches
Windows Errors? Fix Them Before They SpreadFree repair scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.