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
mediaparameter is an array of JSON strings, where each element contains:type("photo"/"video"),media(file_id or URL), and optionallycaption,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:
- Get the
file_idof the required size (for photos, this is usually the last element of thephotoarray, which is the largest). - Call
getFilewith thisfile_id— you will receive afile_path(the relative path on Telegram servers). - 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
captionis truncated after 1024 characters. For long texts, it is better to use a separatesendMessage. - 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: Редактирование и удаление.