sendDocument and sendPhoto in Telegram with PHP: cURL, file_id, and Sending via URL

Working with media files is one of the core tasks when developing Telegram-бот s. The platform provides three main ways to transfer files in methods like sendPhoto, sendDocument, sendVideo, and others: direct binary data upload, passing a public link (URL), and using a previously saved file_id.

Each approach has its own limits, network resource requirements, and backend processing nuances. Using an unsuitable method for a specific scenario leads to performance drops, cURL timeout errors, and API errors.

Telegram Bot API File Upload Limits

Before choosing a file sending architecture, it is important to consider Telegram's strict server limits:

  • Direct upload (multipart/form-data): the maximum file size is 50 MB for standard bots. If you use a Local Bot API Server, the limit increases to 2 GB.
  • Passing via URL: Telegram downloads the file itself from the specified link. The limit for photos is 5 MB, and for documents and other files, it is 20 MB. Telegram servers also have a strict download timeout.
  • Resending via file_id: up to 2 GB for any file type. This is the fastest and most lightweight method, as the file is already stored on Telegram servers.

Uploading a Local File: multipart/form-data via cURL

To upload files from the server disk, you must construct an HTTP request with the multipart/form-data content type. In PHP, the CURLFile class is used for this purpose. Note: the file_get_contents function is not suitable for Telegram Bot API calls involving file uploads, as it does not allow efficient management of headers, streaming, and HTTP status handling.

When sending files, always verify the HTTP response code, json_last_error() error, and the ok field in the JSON body. Below is a ready-to-use service class for sending documents from disk.

<?php

declare(strict_types=1);

class TelegramUploader
{
private string $botToken;

public function __construct()
{
$token = getenv('TELEGRAM_BOT_TOKEN');
if (!$token) {
throw new RuntimeException('Переменная окружения TELEGRAM_BOT_TOKEN не задана');
}
$this->botToken = $token;
}

public function sendDocument(string $chatId, string $filePath, string $caption = ''): array
{
if (!file_exists($filePath) || !is_readable($filePath)) {
throw new InvalidArgumentException("Файл не найден или недоступен: {$filePath}");
}

$url = "https://api.telegram.org/bot{$this->botToken}/sendDocument";

$postFields = [
'chat_id' => $chatId,
'document' => new CURLFile(realpath($filePath)),
'caption' => $caption,
'parse_mode' => 'HTML',
];

$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postFields,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 60,
]);

$response = curl_exec($ch);
$curlError = curl_error($ch);
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($response === false) {
throw new RuntimeException("cURL ошибка при отправке файла: {$curlError}");
}

$data = json_decode((string)$response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException('Некорректный JSON-ответ от Telegram API: ' . json_last_error_msg());
}

if ($httpCode !== 200 || empty($data['ok'])) {
$description = $data['description'] ?? 'Неизвестная ошибка';
throw new RuntimeException("Telegram API Error [{$httpCode}]: {$description}");
}

return $data['result'];
}
}

Optimization: Reusing file_id

When you send a file via multipart/form-data or URL, Telegram saves it in its infrastructure and returns a message object in the response. Inside the document or photo object, there is a unique identifier called file_id.

Store file_id in your database (for example, in a product catalog or materials table). Повторная отправка файла using file_id happens almost instantly, consumes no outgoing traffic from your server, and bypasses the 50 MB limit.

Important note: file_id is unique for each bot. You cannot use a file_id obtained in one bot to send a message via another bot. Below is an example function for sending a document via file_id using the application/json header type.

<?php

declare(strict_types=1);

function sendDocumentByFileId(string $chatId, string $fileId, string $caption = ''): array
{
$botToken = getenv('TELEGRAM_BOT_TOKEN');
if (!$botToken) {
throw new RuntimeException('Токен бота не найден');
}

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

$payload = json_encode([
'chat_id' => $chatId,
'document' => $fileId,
'caption' => $caption,
'parse_mode' => 'HTML',
], JSON_UNESCAPED_UNICODE);

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

$response = curl_exec($ch);
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

$data = json_decode((string)$response, true);
if ($httpCode !== 200 || empty($data['ok'])) {
$err = $data['description'] ?? 'Ошибка API';
throw new RuntimeException("Не удалось отправить файл по file_id: {$err}");
}

return $data['result'];
}

Sending Media via URL: Pros and Pitfalls

Passing an HTTP URL as the value for the photo or document parameter seems convenient: you don't need to download the file to the bot's backend server, just pass a direct link to S3 or a CDN.

Limitations and Risks of the URL Method:

  • Size limit: 5 MB for photos and 20 MB for documents. Larger files will trigger an API error.
  • CDN availability: Telegram servers must have direct access to the file without CAPTCHA, Basic Auth, or complex cookies.
  • Download timeout: if your server serves the file slowly (longer than 5–10 seconds), Telegram will return the error bad request: wrong file identifier/HTTP URL specified.
  • Telegram-side caching: Telegram may cache the file by its URL. If the file content at the URL changes, bot users may receive the old version.
<?php

declare(strict_types=1);

function sendPhotoByUrl(string $chatId, string $photoUrl, string $caption = ''): array
{
$botToken = getenv('TELEGRAM_BOT_TOKEN');
$url = "https://api.telegram.org/bot{$botToken}/sendPhoto";

$payload = json_encode([
'chat_id' => $chatId,
'photo' => $photoUrl,
'caption' => $caption,
]);

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

$response = curl_exec($ch);
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

$data = json_decode((string)$response, true);

if ($httpCode !== 200 || empty($data['ok'])) {
$msg = $data['description'] ?? 'Неизвестная ошибка Telegram API';
throw new RuntimeException("Ошибка отправки фото по URL: {$msg}");
}

return $data['result'];
}

Common Mistakes When Working with Files and Interactive Elements

When building bot logic, developers often combine sending media files with inline keyboard buttons (for example, a "Download price list" or "Confirm receipt" button). At this stage, the following mistakes are frequently made:

  1. Exceeding callback_data size: Attempting to store a file path or a long URL directly in the callback_data of an interactive button. The maximum size of callback_data is 64 bytes. Create a short identifier in the DB (for example, lead_id = bin2hex(random_bytes(7))) and pass only that identifier in the button.
  2. Lack of HTTP 429 (Too Many Requests) handling: During bulk document distribution, Telegram will quickly return a 429 status code with a retry_after field. All media sends should be logged and wrapped in a queue system.
  3. Failure to save file_id: Repeatedly sending PDF catalogs or manuals via multipart/form-data clogs the backend bandwidth and leads to rate limit bans.

If you need help with complex bot development or Mini App integration, the experts at BotCreator can help implement a project of any complexity.

New articles on Telegram

We explain what to automate in your business and how it works in practice. No spam.