Messages

Photos, documents, media groups

Text sendMessage make up about a third of bot traffic. The other two-thirds are photos, PDFs, voice messages, videos, and albums. For these, the Bot API provides a family of methods: sendPhoto, sendDocument, sendVideo, sendAudio, sendVoice, sendAnimation, sendVideoNote. They all share the same signature: chat_id, a media field (photo, document, etc.), optional caption (up to 1024 characters), and parse_mode (HTML/MarkdownV2 — see HTML и Markdown).

Three ways to send a file

The media field accepts three forms:

  • file_id (string) — the file is already on Telegram servers. Free, instant, and does not consume your outgoing traffic.
  • HTTP/HTTPS URL — Telegram will download the file itself (limit: 5 MB for photos, 20 MB for other files). Convenient for generating QR codes, charts, and screenshots.
  • multipart/form-data — uploading from your disk via the bot. Limits: 10 MB for photos, 50 MB for documents (up to 2 GB via a local Bot API server).

Rule: if a file is going to be forwarded multiple times, send it once, retrieve the file_id from the response, and use only that string from then on.

The telegramApi() helper from the "First request: getMe" chapter

We will use the telegramApi() function that we created in the Первый запрос: getMe chapter. It makes a POST request to api.telegram.org, checks the HTTP code, json_last_error(), and the ok field in the response. Here we only call it — do not copy the implementation.

<?php
// reminder: telegramApi() из главы first-request
// function telegramApi(string $token, string $method, array $params = []): array { ... }

$token = getenv('TELEGRAM_BOT_TOKEN') ?: exit('TELEGRAM_BOT_TOKEN not set');
$chatId = 123456789; // замените на реальный chat_id

Sending a photo by URL (sendPhoto)

The most common use case is generating an image (QR code, chart, meme) and sending it immediately. Pass the URL in the photo field.

<?php
$photoUrl = 'https://example.com/qr.png?data=hello';
$caption = 'Ваш QR-код \n<code>hello</code>';

$result = telegramApi($token, 'sendPhoto', [
'chat_id' => $chatId,
'photo' => $photoUrl,
'caption' => $caption,
'parse_mode' => 'HTML',
]);

// Смотрим, что вернул API
print_r($result);
// или для красивого вывода в терминале:
// echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

Example of a Telegram response (a Message object):

{
"ok": true,
"result": {
"message_id": 42,
"date": 1700000000,
"chat": { "id": 123456789, "type": "private", "first_name": "Ivan" },
"photo": [
{ "file_id": "AgAC...", "file_unique_id": "AQAD...", "width": 90, "height": 90, "file_size": 1234 },
{ "file_id": "AgAC...", "file_unique_id": "AQAD...", "width": 320, "height": 320, "file_size": 8901 },
{ "file_id": "AgAC...", "file_unique_id": "AQAD...", "width": 800, "height": 800, "file_size": 45210 }
],
"caption": "Ваш QR-код \nhello",
"caption_entities": [ { "type": "code", "offset": 11, "length": 5 } ]
}
}

What to look for: the ok field = true indicates success. In result.photo, there is an array of PhotoSize objects of different resolutions. The last element is the largest one; save its file_id for resending. The file_unique_id is the same for all sizes of a single file — useful for deduplication in the database.

Uploading a file from disk (sendDocument)

For PDFs, archives, large photos, and any files that are not on Telegram, use multipart/form-data. In PHP 5.5+, there is the CURLFile class for this.

<?php
$path = __DIR__ . '/files/price-list.pdf';
$filename = 'price-list-2024.pdf';

if (!is_file($path)) {
throw new RuntimeException("File not found: $path");
}

$result = telegramApi($token, 'sendDocument', [
'chat_id' => $chatId,
'document' => new CURLFile($path, 'application/pdf', $filename),
'caption' => 'Прайс-лист 2024 \n<b>актуален до 31.12</b>',
'parse_mode' => 'HTML',
]);

var_dump($result);

The response is similar to a photo, except instead of photo, you receive a document object with the fields file_id, file_unique_id, file_name, mime_type, file_size. The file_id is your key for resending without uploading again.

Albums: sendMediaGroup

The sendMediaGroup method sends 2–10 objects of the same type (only photos OR only videos) as a single album message. In the update, a single Message arrives with the media_group_id field — you can use it to group incoming albums from users.

  • The media parameter is an array of JSON strings, where each element contains: type ("photo"/"video"), media (file_id or URL), and optionally caption, parse_mode.
  • The caption (caption) is taken into account only for the first element; the others ignore it.
  • You cannot mix photos and videos in a single album — send them as two consecutive groups.
<?php
$urls = [
'https://example.com/photo1.jpg',
'https://example.com/photo2.jpg',
'https://example.com/photo3.jpg',
];

$media = array_map(function (string $url) {
return json_encode([
'type' => 'photo',
'media' => $url,
], JSON_UNESCAPED_SLASHES);
}, $urls);

// Подпись только для первого кадра
$media[0] = json_encode([
'type' => 'photo',
'media' => $urls[0],
'caption' => 'Наш фотоотчёт \n<i>3 кадра</i>',
'parse_mode' => 'HTML',
], JSON_UNESCAPED_SLASHES);

$result = telegramApi($token, 'sendMediaGroup', [
'chat_id' => $chatId,
'media' => $media,
]);

// Результат — массив Message (по одному на каждый элемент альбома)
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

Example of a response (an array of messages):

{
"ok": true,
"result": [
{
"message_id": 43,
"date": 1700000001,
"chat": { "id": 123456789, "type": "private" },
"media_group_id": "1234567890123456789",
"photo": [ { "file_id": "AgAC...", "width": 800, "height": 600, "file_size": 54321 } ],
"caption": "Наш фотоотчёт 3 кадра"
},
{
"message_id": 44,
"media_group_id": "1234567890123456789",
"photo": [ { "file_id": "AgAC...", "width": 800, "height": 600, "file_size": 51234 } ]
},
{
"message_id": 45,
"media_group_id": "1234567890123456789",
"photo": [ { "file_id": "AgAC...", "width": 800, "height": 600, "file_size": 49876 } ]
}
]
}

Important: each message in the album has its own message_id and its own file_id for the photo. You can edit or delete each one individually (see the next chapter), but the media_group_id is the same — it is the album identifier.

Receiving a file from a user (getFile + downloading)

When an update arrives with a message.photo or message.document:

  1. Get the file_id of the required size (for photos, this is usually the last element of the photo array, which is the largest).
  2. Call getFile with this file_id — you will receive a file_path (the relative path on Telegram servers).
  3. Download it using the URL https://api.telegram.org/file/bot<TOKEN>/<file_path>. The link is valid for at least 1 hour.

We will use the processUpdate() function that we discussed in the Long polling chapter. There we already get the parsed $message — here we only show how to handle the incoming photo.

<?php
// Пример внутри processUpdate() из главы long-polling
function handleIncomingPhoto(array $message, string $token): void {
if (empty($message['photo'])) return;

// Последний = максимальный размер
$largest = end($message['photo']);
$fileId = $largest['file_id'];

$fileInfo = telegramApi($token, 'getFile', ['file_id' => $fileId]);
$filePath = $fileInfo['result']['file_path'] ?? null;

if (!$filePath) {
error_log('getFile failed: ' . json_encode($fileInfo));
return;
}

$downloadUrl = "https://api.telegram.org/file/bot{$token}/{$filePath}";
$content = file_get_contents($downloadUrl); // или cURL для больших файлов

if ($content !== false) {
$localPath = sys_get_temp_dir() . '/' . basename($filePath);
file_put_contents($localPath, $content);
echo "Сохранено в $localPath (" . strlen($content) . " bytes)\n";
}
}

The getFile response:

{
"ok": true,
"result": {
"file_id": "AgAC...",
"file_unique_id": "AQAD...",
"file_size": 45210,
"file_path": "photos/file_123.jpg"
}
}

Pitfalls

  • The image URL must return a correct Content-Type (image/jpeg, image/png) and be accessible without redirects to a login/captcha page. Telegram does not follow redirects to authorization pages.
  • If you send the exact same URL again, Telegram will re-upload it and issue a new file_id — do not confuse this with reusing the old file_id.
  • The caption is truncated after 1024 characters. For long texts, it is better to use a separate sendMessage.
  • Videos in an album require the same aspect ratio — otherwise, Telegram will stitch them incorrectly or return an error.
  • Media messages cannot be edited like text — only the caption (editMessageCaption) can be changed. To replace the file itself, delete the message and send a new one.

Next: Редактирование и удаление.