Users & chats
chat_id and user_id
Difference between chat_id and user_id in the Telegram Bot API. How to correctly extract identifiers on PHP, avoid data type overflow, and check bot permissions.
Understanding the difference between chat_id and user_id is fundamental for working with users and chats in Telegram. Errors in their handling cause bots to send messages to the wrong chats, confuse user profiles in the database, or fail with authorization errors. In this chapter we will examine the anatomy of these identifiers, learn how to correctly extract them from updates on PHP, and verify bot permissions before sending messages.
What's the difference: user_id vs chat_id
Although both identifiers are whole numbers, they serve fundamentally different roles in the Telegram Bot API architecture.
user_id(User Identifier) — this is a unique, always positive 64-bit number representing a specific person or bot account. It is tightly bound to the account and never changes, even if the user changes their username@usernameor name. This ID is always found in thefrom.idfield within message objects, button clicks, and queries.chat_id(Chat Identifier) — this is the address of a specific space where messages are sent. The formatchat_iddirectly depends on the type of chat.
The chat type determines the structure of its identifier:
- Private Chat (Private):
chat_idis always positive and exactly equals your interlocutor'suser_id. - Regular Group (Legacy Group): a negative number (e.g.,
-456789012). - Supergroup and Channel: a negative number that always starts with the prefix
-100(e.g.,-1001234567890). The prefix-100is not a visual element but part of the mathematical value of the identifier.
Extracting IDs from incoming updates
When your script receives a webhook from Telegram, the JSON structure may vary depending on whether the user sent a text message, clicked an inline button, or published a post in a channel. We need a reliable way to extract both IDs.
Below is a PHP script for processing incoming webhooks that safely extracts chat_id and user_id, taking into account the structure differences of various update types.
<?php
// Получаем сырые данные от вебхука
$rawInput = file_get_contents('php://input');
$update = json_decode($rawInput, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log('Ошибка декодирования JSON: ' . json_last_error_msg());
http_response_code(400);
exit;
}
$chatId = null;
$userId = null;
if (isset($update['message'])) {
// Обычное текстовое сообщение, фото, документ и т.д.
$chatId = $update['message']['chat']['id'];
$userId = $update['message']['from']['id'];
} elseif (isset($update['callback_query'])) {
// Нажатие на inline-кнопку
$chatId = $update['callback_query']['message']['chat']['id'] ?? null;
$userId = $update['callback_query']['from']['id'];
} elseif (isset($update['channel_post'])) {
// Пост в канале (у каналов нет отправителя-пользователя в from.id)
$chatId = $update['channel_post']['chat']['id'];
$userId = null;
}
if ($chatId === null) {
// Если это техническое обновление без чата (например, inline_query)
http_response_code(200);
exit;
}
// Логируем результат для отладки
error_log(sprintf("Получен запрос. Chat ID: %s, User ID: %s", $chatId, $userId ?? 'NULL'));
Let's consider an example of an incoming message JSON structure sent by a user to a supergroup. Note the difference between message.from.id (who sent it) and message.chat.id (where it was sent):
{
"update_id": 876543210,
"message": {
"message_id": 452,
"from": {
"id": 987654321,
"is_bot": false,
"first_name": "Алексей",
"username": "alex_dev"
},
"chat": {
"id": -1001500300200,
"title": "PHP Чат",
"type": "supergroup"
},
"date": 1700000000,
"text": "Всем привет!"
}
}
Chat Types and Logic Branching
The chat.type field determines the context in which the bot operates. This is critical because the logic in a personal chat differs from that in a group. For example, in a personal chat the bot can respond immediately to messages, while in a group it should only react to mentions or commands.
<?php
$chatType = $update['message']['chat']['type'] ?? null;
switch ($chatType) {
case 'private':
// Личный диалог с пользователем.
// Здесь chat_id гарантированно равен user_id.
// Безопасно храним состояние FSM (конечного автомата) для этого пользователя.
break;
case 'group':
case 'supergroup':
// Групповой чат.
// chat_id отрицательный. Здесь могут находиться сотни пользователей.
// Нельзя сохранять личные состояния пользователя на весь chat_id.
break;
case 'channel':
// Публичный или приватный канал.
// Бот здесь выступает только как вещатель. Отправителя (from) обычно нет.
break;
}
Bot Permission Check via getChatMember
Before sending a message to a group or channel, it is important to ensure the bot has permission to do so (for example, it is blocked or is an administrator in the channel). For this purpose, the method getChatMember is used.
To find out the bot's own status, we need to pass its own user_id. The simplest way to obtain user_id without extra requests is to extract it from the API token. The token always starts with the bot's ID (a number up to the colon).
We use function telegramApi() to make API calls. Its implementation based on cURL was explained in detail in chapter Первый запрос: getMe. Here we call it to check permissions.
<?php
function getBotStatusInChat(string|int $chatId): ?string
{
$token = getenv('TELEGRAM_BOT_TOKEN');
if (!$token) {
error_log('TELEGRAM_BOT_TOKEN не задан в окружении');
return null;
}
// Извлекаем ID бота из токена (например, "123456:ABC-DEF" -> 123456)
$botUserId = (int) explode(':', $token)[0];
// Запрашиваем статус бота в конкретном чате
$response = telegramApi('getChatMember', [
'chat_id' => $chatId,
'user_id' => $botUserId
]);
if (!$response['ok']) {
error_log('Ошибка getChatMember: ' . ($response['description'] ?? 'Неизвестная ошибка'));
return null;
}
// Возвращает статус: "creator", "administrator", "member", "restricted", "left", "kicked"
return $response['result']['status'] ?? null;
}
// Пример использования перед отправкой сообщения
$status = getBotStatusInChat($chatId);
if ($status === 'kicked' || $status === 'left') {
error_log("Бот был удален из чата {$chatId}. Отправка невозможна.");
exit;
}
On a successful getChatMember request, an object ChatMember is returned. Here is an example response for a bot that is an administrator of a group:
{
"ok": true,
"result": {
"user": {
"id": 555666777,
"is_bot": true,
"first_name": "HelperBot",
"username": "my_helper_bot"
},
"status": "administrator",
"can_be_edited": false,
"can_manage_chat": true,
"can_change_info": true,
"can_post_messages": true,
"can_edit_messages": true,
"can_delete_messages": true,
"can_invite_users": true,
"can_restrict_members": true,
"can_pin_messages": true,
"can_manage_topics": true
}
}
PHP Specifics: 32-bit System Overflow Problem
Telegram identifiers (both for users and chats) have long exceeded the limit of 32-bit signed integers 2 147 483 647. Currently, IDs fit within the 64-bit range.
If your PHP script runs on an outdated 32-bit platform (or you use old PHP versions), casting large IDs to (int) will lead to distortion (overflow). To prevent this problem, follow three rules:
- Use 64-bit PHP. On modern 64-bit servers, the type
intin PHP is 64-bit by default and easily accommodates any Telegram ID. - Store IDs in the database as strings or BIGINT. In MySQL use type
BIGINT(orVARCHAR(64)), in PostgreSQL —bigint. Never use plainINT. - Decode JSON without loss of precision. When using
json_decode()on 32-bit systems, pass the flagJSON_BIGINT_AS_STRINGto automatically convert huge numbers to PHP strings, preserving accuracy:
<?php
// Безопасное декодирование на любых платформах
$update = json_decode($rawInput, true, 512, JSON_BIGINT_AS_STRING);
How to Debug Incoming IDs
The easiest way to understand which exact IDs are arriving at your bot is to write the raw JSON from the webhook to a log file. You can observe the data structure in real time using the tail utility in the Linux terminal.
Logging incoming updates to a file:
<?php
$rawInput = file_get_contents('php://input');
if (!empty($rawInput)) {
// Записываем лог в файл рядом со скриптом
file_put_contents(__DIR__ . '/telegram_updates.log', $rawInput . PHP_EOL, FILE_APPEND);
}
To view the log in real time, run the command in your server terminal. It will output each new line passed through the formatting tool jq:
tail -f telegram_updates.log | jq .
Continue: Группы
"}