Telegraft

Справочник по 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`, поэтому членство в группах отслеживается точно, а блокировки в личных чатах — нет.

Можно ли попросить разблокировать бота или связаться с человеком иначе?

Нет. Блокировка снимает само разрешение писать этому человеку, и запасного канала нет. Если он вернётся по диплинку и снова запустит бота, разрешение вернётся вместе с ним, — но это целиком его действие, а не ваше.

Заблокировавшего подписчика удалять из базы или просто помечать?

Помечать — с причиной и отметкой времени. Удаление теряет историю, и человек, который разблокировал бота и вернулся, выглядит новым подписчиком, что искажает любую метрику удержания, которая позже понадобится. Заодно теряется и сам факт ухода, а он обычно интереснее всего остального.

Влияет ли блокировка на членство в каналах, которыми управляет бот?

Нет. Блокировка мешает боту писать человеку в личку, но не удаляет его из канала или группы. Поэтому бот с доступом по подписке обязан считать членство в канале и возможность написать разными состояниями: участник, заблокировавший бота, доступ сохраняет, но сообщить ему уже ничего нельзя.

Смежные ограничения