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:

  • ok means the request succeeded, false — an error will occur in description (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 larger update_id.
  • message — incoming message. Fields: message_id, from (User), chat (Chat), date (Unix timestamp), text and 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 message
  • callback_query — inline button press (see callback_query)
  • inline_query — inline mode
  • my_chat_member — bot added/removed from chat
  • chat_member — participant status changed
  • pre_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

  1. Write something to the bot in Telegram.
  2. Run the script: php get_updates.php.
  3. You'll see an array with update_id, message, from, chat, text.
  4. Run again — [] (empty array) will return because offset was not saved.
  5. 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 — держать соединение открытым и получать обновления без задержек

"}