Справочник по Bot API
Ошибка, которая означает «перестань отправлять»
Отправка пользователю, который заблокировал бота, падает с кодом HTTP 403 и описанием «Forbidden: bot was blocked by the user». Состояние держится, пока человек не разблокирует бота сам, сам бот отменить его не может, и это единственный однозначный сигнал о доставке конкретному получателю, который даёт Bot API.
Заблокирован пользователем: точные цифры
- Ошибка при отправке тому, кто заблокировал бота
- HTTP 403 с описанием «Forbidden: bot was blocked by the user»
- Код ошибки
- HTTP 403
- Описание
- Forbidden: bot was blocked by the user
- Может ли отменить бот
- Нет. Разблокировать может только сам пользователь
- Видно ли заранее
- Нет. Выясняется попыткой отправки
- Правильная обработка
- Один раз вывести подписчика из списка, при первом же случае
As of 2025-10-01, Telegram Bot API 13.4
Что это значит на практике
Бот не может начать переписку. Он вправе написать только тому, кто написал ему первым, а блокировка это разрешение отзывает, — поэтому данная ошибка зеркальна правилу, которое управляет любым исходящим сообщением. Нет вызова API, чтобы запросить разрешение заново, нет апдейта о том, что человек заблокировал бота, и нет способа узнать состояние до попытки отправки. Вы узнаёте его, попробовав.
Это делает ошибку операционно ценной, а не просто раздражающей. Там, где почта даёт мягкие отказы, папку «Спам» и молчаливую недоставку, Telegram даёт один явный, перехватываемый и постоянный сигнал. Список подписчиков, который вычёркивает людей по этой ошибке, остаётся точным без всяких догадок, и это положение лучше того, что предлагает большинство каналов.
Обработка обязана быть идемпотентной и немедленной. Пометка подписчика неактивным при первом же случае, в той же транзакции, которая фиксирует попытку отправки, не даёт списку пытаться снова в следующей кампании. Боты, которые пишут ошибку в лог и продолжают отправлять при каждой рассылке, тратят растущую долю бюджета скорости на получателей, которые никогда ничего не получат, — а на большом списке это реальная цена, потому что потолок общий для бота.
Эту ошибку стоит отличать от соседних, потому что правильная реакция у них разная. «Chat not found» обычно означает, что человек никогда не запускал бота или идентификатор чата неверен. «User is deactivated» означает, что аккаунт в Telegram удалён, — это тоже навсегда, но по другой причине. «Bot was kicked from the group» — групповой эквивалент. Все они возвращаются с кодом 403 и все означают «прекратить», но только блокировку человек может позже снять сам, поэтому причину стоит хранить, а не схлопывать всё в один флаг неактивности.
Одно продуктовое следствие стоит проговорить при согласовании объёма работ. Блокировка постоянна со стороны бота и невидима до попытки отправки, поэтому метрики кампаний деградируют тихо: список, который выглядит как сорок тысяч подписчиков, может содержать несколько тысяч тех, кто заблокировал бота месяцы назад. Сверить это можно только попыткой отправки — то есть точное число известно всегда после кампании, а не до неё.
Как это обрабатывать в коде
// Distinguish the permanent 403s rather than flattening them into one flag.
type Terminal = 'blocked' | 'deactivated' | 'chat-missing'
function terminalReason(error: unknown): Terminal | undefined {
const e = error as { error_code?: number; description?: string }
if (e.error_code !== 403 && e.error_code !== 400) return undefined
const d = e.description ?? ''
if (d.includes('blocked by the user')) return 'blocked'
if (d.includes('user is deactivated')) return 'deactivated'
if (d.includes('chat not found')) return 'chat-missing'
return undefined
}
async function sendOrRetire(chatId: number, text: string): Promise<void> {
try {
await bot.api.sendMessage(chatId, text)
} catch (error) {
const reason = terminalReason(error)
if (reason === undefined) throw error
// Permanent. Retire once, and never spend rate budget on this recipient again.
await retireSubscriber(chatId, reason)
}
}Какие вопросы это порождает
Узнаёт ли бот о том, что его заблокировали?
В личной переписке — нет: апдейта об этом не приходит, и состояние обнаруживается только при следующей отправке. В группах иначе: бот, удалённый из группы, получает апдейт `my_chat_member`, поэтому членство в группах отслеживается точно, а блокировки в личных чатах — нет.
Можно ли попросить разблокировать бота или связаться с человеком иначе?
Нет. Блокировка снимает само разрешение писать этому человеку, и запасного канала нет. Если он вернётся по диплинку и снова запустит бота, разрешение вернётся вместе с ним, — но это целиком его действие, а не ваше.
Заблокировавшего подписчика удалять из базы или просто помечать?
Помечать — с причиной и отметкой времени. Удаление теряет историю, и человек, который разблокировал бота и вернулся, выглядит новым подписчиком, что искажает любую метрику удержания, которая позже понадобится. Заодно теряется и сам факт ухода, а он обычно интереснее всего остального.
Влияет ли блокировка на членство в каналах, которыми управляет бот?
Нет. Блокировка мешает боту писать человеку в личку, но не удаляет его из канала или группы. Поэтому бот с доступом по подписке обязан считать членство в канале и возможность написать разными состояниями: участник, заблокировавший бота, доступ сохраняет, но сообщить ему уже ничего нельзя.
Смежные ограничения
Почему эта ошибка — единственный однозначный сигнал по каждому получателю, объяснено на странице об отсутствии подтверждений доставки.
Продукт, в котором неточное число подписчиков напрямую искажает отчётность, — это бот лояльности и рефералов.