DEV Community

Serhii
Serhii

Posted on Originally published at botservice.biz

Optimize Telegram Bot File Uploads in PHP with CURLFile and file_id Reuse

When building Telegram bots in PHP, you often need to dispatch media such as PDF invoices, system logs, or images. The Telegram Bot API provides three distinct ways to send files: uploading a local file via multipart/form-data, passing a publicly accessible URL, or reusing an existing Telegram file_id.

This guide demonstrates how to implement these three approaches reliably using PHP's cURL extension, handle Telegram's strict size limits, and cache file identifiers to minimize bandwidth and latency. We do not cover asynchronous queueing or chunked uploads for user accounts; this focuses entirely on standard Bot API operations.

Method 1: Uploading Local Files with CURLFile

To upload a file stored on your server's disk, you must send a multipart/form-data POST request. In PHP, this is accomplished using the CURLFile class. Do not use the deprecated @ prefix syntax, which is disabled in modern PHP versions.

Here is a robust function to upload a local document to Telegram. It includes strict error handling for cURL failures, HTTP status codes, and Telegram API error payloads.

<?php

/**
 * Uploads a local file to Telegram as a document.
 *
 * @param string $chatId The recipient chat ID.
 * @param string $filePath Absolute path to the local file.
 * @param string $caption Optional caption for the document.
 * @return string The Telegram file_id of the uploaded file.
 * @throws Exception If the upload fails or the API returns an error.
 */
function sendLocalDocument(string $chatId, string $filePath, string $caption = ''): string
{
    $token = getenv('TELEGRAM_BOT_TOKEN');
    if (!$token) {
        throw new Exception('Telegram bot token is not configured in environment variables.');
    }

    if (!file_exists($filePath) || !is_readable($filePath)) {
        throw new Exception("File not found or not readable: {$filePath}");
    }

    $url = "https://api.telegram.org/bot{$token}/sendDocument";

    // Detect MIME type dynamically
    $mimeType = mime_content_type($filePath) ?: 'application/octet-stream';
    $postName = basename($filePath);

    $payload = [
        'chat_id' => $chatId,
        'document' => new CURLFile($filePath, $mimeType, $postName),
    ];

    if ($caption !== '') {
        $payload['caption'] = $caption;
    }

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    // Set a generous timeout for large file uploads
    curl_setopt($ch, CURLOPT_TIMEOUT, 60);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);

    $response = curl_exec($ch);

    if ($response === false) {
        $error = curl_error($ch);
        $errno = curl_errno($ch);
        curl_close($ch);
        throw new Exception("cURL error ({$errno}): {$error}");
    }

    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $data = json_decode($response, true);
    if (json_last_error() !== JSON_ERROR_NONE) {
        throw new Exception('Failed to parse Telegram API JSON response.');
    }

    if ($httpCode !== 200 || !isset($data['ok']) || $data['ok'] !== true) {
        $description = $data['description'] ?? 'Unknown error';
        throw new Exception("Telegram API error (HTTP {$httpCode}): {$description}");
    }

    // Return the file_id for database caching
    return $data['result']['document']['file_id'];
}
Enter fullscreen mode Exit fullscreen mode

Method 2: Sending via URL or Reusing file_id

Uploading files repeatedly wastes server bandwidth and introduces unnecessary latency. If your file is already hosted on a public web server, or if you have previously uploaded the file and saved its file_id, you should pass a simple string instead of a CURLFile object.

When you pass a string to the document or photo parameters, Telegram automatically determines whether it is a URL or a file_id:

  • URL: Telegram downloads the file from your server to their servers and delivers it to the user.
  • file_id: Telegram instantly forwards the existing file from their servers to the user. This is near-instantaneous and consumes zero upload bandwidth from your server.

Because these payloads do not contain binary file streams, you can send them as a standard JSON payload, which is more efficient than multipart/form-data.

<?php

/**
 * Sends a document using a public URL or a cached file_id.
 *
 * @param string $chatId The recipient chat ID.
 * @param string $fileIdentifier A public URL or a Telegram file_id.
 * @param string $caption Optional caption.
 * @return string The Telegram file_id.
 * @throws Exception If the request fails.
 */
function sendExistingDocument(string $chatId, string $fileIdentifier, string $caption = ''): string
{
    $token = getenv('TELEGRAM_BOT_TOKEN');
    if (!$token) {
        throw new Exception('Telegram bot token is not configured.');
    }

    $url = "https://api.telegram.org/bot{$token}/sendDocument";

    $payload = [
        'chat_id' => $chatId,
        'document' => $fileIdentifier,
    ];

    if ($caption !== '') {
        $payload['caption'] = $caption;
    }

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Content-Type: application/json'
    ]);
    curl_setopt($ch, CURLOPT_TIMEOUT, 15);

    $response = curl_exec($ch);

    if ($response === false) {
        $error = curl_error($ch);
        curl_close($ch);
        throw new Exception("cURL error: {$error}");
    }

    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $data = json_decode($response, true);
    if (json_last_error() !== JSON_ERROR_NONE) {
        throw new Exception('Failed to parse Telegram API JSON response.');
    }

    if ($httpCode !== 200 || !isset($data['ok']) || $data['ok'] !== true) {
        $description = $data['description'] ?? 'Unknown error';
        throw new Exception("Telegram API error (HTTP {$httpCode}): {$description}");
    }

    return $data['result']['document']['file_id'];
}
Enter fullscreen mode Exit fullscreen mode

Production Considerations and Limits

When implementing file transfers in production, you must design around Telegram's structural limitations:

Size Limits

  • Local Uploads: Bots can upload files up to 50 MB in size via the standard Bot API. If you need to send files up to 2000 MB, you must host and run a local instance of the Telegram Bot Database/API server.
  • Sending by URL: When sending files via a public URL, the limit is strictly 20 MB for photos and other documents. The server hosting the file must also respond quickly; slow connections will cause Telegram to return a 400 Bad Request timeout.

Implementing a file_id Cache

To optimize performance, store the relationship between your local files and their Telegram file_ids in a database.

Before uploading a file, check your database for an existing file_id associated with the file's unique hash (e.g., sha1 or md5 of the file contents). If a match is found, call sendExistingDocument(). If no match is found, call sendLocalDocument(), capture the returned file_id from the response, and store it in your database for future requests.

Note that a file_id is unique to your specific bot token. You cannot share a file_id between two different bots.

BotCreator — studio that ships Telegram bots / Mini Apps.

Top comments (0)