Updates
getUpdates and update_id
Practical guide on getUpdates: how to get updates, work with update_id and offset, protect against duplicates, and parse incoming messages in PHP.
Telegram sends bot events as Update objects. In polling mode you request them yourself using method getUpdates. This is the first place where we actually read incoming data — until now we only did getMe.
How to call getUpdates
We use function telegramApi() which we created in chapter Первый запрос: getMe. There we wrote a wrapper around cURL with HTTP code checking, json_last_error and field ok — here we simply call it.
<?php
// get_updates.php
require_once __DIR__ . '/src/TelegramClient.php'; // там функция telegramApi()
$token = getenv('TELEGRAM_BOT_TOKEN');
if (!$token) {
die('TELEGRAM_BOT_TOKEN не задан в окружении\n');
}
$offset = 0; // первый запуск — берём всё
$updates = telegramApi($token, 'getUpdates', [
'offset' => $offset,
'limit' => 100, // макс. 100 за раз
'timeout' => 0, // 0 = короткий запрос (short polling)
]);
// Смотрим, что пришло — в терминале удобнее json_encode с флагами
// print_r($updates); // тоже работает, но JSON читаемее для вложенных структур
echo json_encode($updates, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Example of API response
A successful response always has ok: true and an array in result:
{
"ok": true,
"result": [
{
"update_id": 847392101,
"message": {
"message_id": 12,
"from": {
"id": 123456789,
"is_bot": false,
"first_name": "Ivan",
"username": "ivan_php",
"language_code": "ru"
},
"chat": {
"id": 123456789,
"first_name": "Ivan",
"username": "ivan_php",
"type": "private"
},
"date": 1724501234,
"text": "/start"
}
},
{
"update_id": 847392102,
"message": {
"message_id": 13,
"from": { ... },
"chat": { ... },
"date": 1724501235,
"text": "Привет, бוט!"
}
}
]
}
What to look for:
okmeans the request succeeded,false— an error will occur indescription(for example,"Bad Request: offset must be greater than...").result— the Update array. An empty array[]means there are no new events.update_id— monotonically increasing identifier. Each subsequent Update has a largerupdate_id.message— incoming message. Fields:message_id,from(User),chat(Chat),date(Unix timestamp),textand others.
Parameter offset — processing cursor
offset tells the server: "everything with value less than update_id has already been processed, don't resend". The value is last_processed_update_id + 1.
<?php
// После обработки пачки обновляем offset
$nextOffset = $offset;
foreach ($updates as $update) {
// ... ваша логика: processUpdate($update);
$nextOffset = max($nextOffset, $update['update_id'] + 1);
}
// Сохраняем $nextOffset в файл/БД/Redis для следующего запуска
file_put_contents(__DIR__ . '/.offset', (string)$nextOffset);
Important: if the script crashes after processing a message but before saving the offset — on the next run this Update will come again. That's normal, you need idempotent logic (see below).
Idempotency: protection against duplicates
The most reliable pattern is a table with UNIQUE on update_id. We insert before business logic; if there's a duplicate, we skip it.
<?php
// Пример с PDO (таблицу создайте заранее: CREATE TABLE telegram_updates (update_id BIGINT PRIMARY KEY, received_at INT NOT NULL);)
$pdo = new PDO('sqlite:' . __DIR__ . '/bot.db');
$pdo->exec("CREATE TABLE IF NOT EXISTS telegram_updates (
update_id BIGINT PRIMARY KEY,
received_at INT NOT NULL
);");
foreach ($updates as $update) {
$id = $update['update_id'];
try {
$pdo->prepare('INSERT INTO telegram_updates (update_id, received_at) VALUES (?, ?)')
->execute([$id, time()]);
} catch (PDOException $e) {
if ($e->getCode() === '23000') { // UNIQUE constraint violation
continue; // дубль — уже обрабатывали
}
throw $e;
}
// Здесь ваша бизнес-логика
processUpdate($update);
}
What's inside Update — event types
Each Update has exactly one of the event fields. Common ones:
message— new message (text, photo, sticker, document…)edited_message— edited messagecallback_query— inline button press (see callback_query)inline_query— inline modemy_chat_member— bot added/removed from chatchat_member— participant status changedpre_checkout_query,shipping_query— payments
Process only the needed types, explicitly ignore the rest:
<?php
foreach ($updates as $update) {
if (isset($update['message'])) {
handleMessage($update['message']);
} elseif (isset($update['callback_query'])) {
handleCallback($update['callback_query']);
} elseif (isset($update['my_chat_member'])) {
handleChatMember($update['my_chat_member']);
} else {
// Можно логировать неизвестные типы для отладки
error_log('Unknown update type: ' . json_encode(array_keys($update)));
}
}
Parameter allowed_updates — traffic saving
By default Telegram sends all types. If the bot works only with messages and buttons — limit the list:
<?php
$updates = telegramApi($token, 'getUpdates', [
'offset' => $nextOffset,
'limit' => 100,
'timeout' => 0,
'allowed_updates' => ['message', 'callback_query', 'my_chat_member'],
]);
This reduces response size and parsing on your side.
How to check in terminal
- Write something to the bot in Telegram.
- Run the script:
php get_updates.php. - You'll see an array with
update_id,message,from,chat,text. - Run again —
[](empty array) will return because offset was not saved. - Add saving offset to a file — repeated runs will be clean.
For production, short polling (timeout=0) doesn't work well — too many empty requests. In the next chapter we move to Long polling: keeping the connection open for up to 30 seconds and grabbing batches immediately upon arrival.
Next: Long polling — держать соединение открытым и получать обновления без задержек
"}