Legan Studio
Все статьи
~ 5 мин чтения

Chat join requests: модерация заявок в закрытый канал

Как работают chat_join_request: автоматическое одобрение, проверка через бота, антифрод, лид-магнит на входе и связка с CRM.

  • Telegram
  • join requests
  • модерация
  • лидогенерация

Chat join requests — это режим закрытого канала или группы, где пользователь не подписывается мгновенно, а отправляет заявку, которую админ или бот одобряет. Для маркетинга это золотая фича: сам факт подачи заявки трактуется Telegram как разрешение на DM от бота, и вы можете сразу написать новому подписчику в личку с лид-магнитом.

Разберём, как включить заявки, как обрабатывать chat_join_request в Bot API, как фильтровать ботов и как связать всё с CRM-воронкой.

Зачем нужны заявки

В обычном режиме «открытая ссылка-приглашение» подписчики идут потоком, и среди них 10–30% — боты, фейки и фрод. В режиме заявок:

  • бот сразу видит, кто подал заявку (from_user);
  • может проверить юзера по антифрод-критериям;
  • может задать вопрос или показать оффер до одобрения;
  • получает разрешение писать в DM этому юзеру;
  • админы видят чистую аудиторию без чисток постфактум.

Минус один — конверсия из «увидел канал» в «подписан» падает на 15–25% за счёт лишнего шага «подать заявку → ждать».

Включение заявок

В настройках канала или группы → Invite Links → создать ссылку с галкой Request Admin Approval. Старая ссылка-приглашение продолжит работать без заявок — нужно её удалить или сменить.

Программно через бота:

link = await bot.create_chat_invite_link(
    chat_id=CHANNEL_ID,
    name="Реклама в Я.Директ",
    creates_join_request=True,
)
print(link.invite_link)  # https://t.me/+abc123...

Имя ссылки удобно для атрибуции: разные источники трафика — разные ссылки.

Обработка chat_join_request

Бот должен быть админом канала с правом invite_users (или manage_chat). Подпишитесь на апдейт в setWebhook:

await bot.set_webhook(
    url=URL,
    secret_token=SECRET,
    allowed_updates=["message", "chat_join_request", "callback_query"],
)

Хендлер:

from aiogram import Router, F
from aiogram.types import ChatJoinRequest

router = Router()

@router.chat_join_request(F.chat.id == CHANNEL_ID)
async def on_join_request(req: ChatJoinRequest, bot: Bot):
    user = req.from_user
    # 1. Антифрод
    if await is_likely_bot(user):
        await bot.decline_chat_join_request(req.chat.id, user.id)
        return
    # 2. Одобрение
    await bot.approve_chat_join_request(req.chat.id, user.id)
    # 3. DM с лид-магнитом
    try:
        await bot.send_message(
            user.id,
            f"Привет, {user.first_name}! Спасибо за подписку. Держи чек-лист:",
            reply_markup=lead_magnet_kb(),
        )
    except TelegramForbiddenError:
        pass  # юзер запретил DM

Антифрод-критерии

Простейший набор проверок, отсеивающий 80% ботов:

КритерийПризнак бота
Возраст аккаунтаменее 7 дней (user.id < некоторого порога)
Usernameпустой или [a-z]{4}\d{6}
Аватаротсутствует
Premiumредко у ботов
Языкне из ожидаемого списка
Историяноль сообщений в общих чатах
async def is_likely_bot(user) -> bool:
    if user.is_bot:
        return True
    if not user.username and user.first_name in ("User", "Telegram"):
        return True
    photos = await bot.get_user_profile_photos(user.id, limit=1)
    if photos.total_count == 0 and user.id > 7_000_000_000:
        return True
    return False

Юзер-ID > 7B — признак свежесозданного аккаунта (в 2026 году нумерация перешла за этот порог недавно).

Каптча в DM как доп-фильтр

Для жёсткой фильтрации можно сначала отправить в DM каптчу, и одобрить заявку только после успешного решения:

@router.chat_join_request(F.chat.id == CHANNEL_ID)
async def on_join(req: ChatJoinRequest, bot: Bot):
    sent = await bot.send_message(
        req.from_user.id,
        "Чтобы одобрить заявку, нажмите кнопку ниже в течение 5 минут.",
        reply_markup=captcha_kb(),
    )
    await pending.put(req.from_user.id, req.chat.id, expires=300)


@router.callback_query(F.data == "captcha:ok")
async def captcha_ok(cb: CallbackQuery, bot: Bot):
    pending_req = await pending.get(cb.from_user.id)
    if not pending_req:
        return
    await bot.approve_chat_join_request(pending_req.chat_id, cb.from_user.id)
    await cb.answer("Заявка одобрена!")

Боты, которые не прошли DM (запретили его), отсеиваются автоматически — send_message бросит исключение, и вы их не одобрите.

Связка с CRM

Каждая одобренная заявка — потенциальный лид. Шлите в CRM с источником из имени invite-ссылки:

async def push_lead(user, invite_link_name: str):
    await crm.create_lead({
        "name": user.full_name,
        "telegram_id": user.id,
        "username": user.username,
        "source": invite_link_name,  # "Реклама в Я.Директ"
        "status": "subscribed",
    })

В CRM сразу строится отчёт «подписки за период по источнику», а у менеджера появляется задача написать лиду через 24 часа.

Лимиты и подводные

  • approve_chat_join_request и decline_chat_join_request — мягкие лимиты ~30 в секунду на бота.
  • Если не одобрить и не отклонить заявку, она висит до 60 дней, потом автоматически отклоняется.
  • chat_join_request приходит только если бот админ с правом приглашать.
  • В супергруппах с включёнными заявками работает то же API.
  • Юзер может отозвать заявку до одобрения — в Bot API такого апдейта нет, узнать об этом не получится.

Аналитика заявок

Полезные метрики:

МетрикаКак считать
Подано заявоксчётчик chat_join_request
Одобреносчётчик approve_chat_join_request
Отклонено антифродомразница
% с DMсколько успешно получили send_message
Конверсия в платёжjoin → /start → payment
Стоимость подписчикабюджет / одобренных

Топ-5 ошибок

  1. Бот не подписан на chat_join_request в allowed_updates — апдейты не приходят.
  2. Не обрабатывают TelegramForbiddenError при отправке DM — крашится весь хендлер.
  3. Одобряют всех подряд — канал засоряется ботами.
  4. Не различают invite_link.name — нет атрибуции трафика.
  5. Забывают, что chat_join_request тоже подпадает под лимит 30 запросов/сек.

Итого

Chat join requests превращают подписку в управляемый процесс: вы фильтруете ботов, ловите DM-разрешение, размечаете трафик и сразу шлёте в CRM. Это +30–40% к качеству базы и х2 к конверсии в платящего клиента по сравнению с открытой ссылкой. Включайте заявки на любых каналах от 1000 подписчиков и обязательно подключайте бота для автоматизации.

Частые вопросы

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

Да, циклом по всем pending-заявкам. Telegram не отдаёт список через Bot API — нужно ловить chat_join_request в момент подачи и складывать в свою очередь. Альтернатива — клиент-сессия (Telethon) с методом messages.GetChatInviteImporters, который возвращает все ожидающие заявки.

Сколько заявок могут висеть одновременно?

Жёсткого лимита нет, но через 60 дней без действия Telegram автоматически отклоняет заявку. На практике 50–100 тысяч pending-заявок — рабочий объём, после этого UI админа начинает тормозить.

Что если юзер запретил DM?

send_message вернёт Forbidden: bot can't initiate conversation. Заявку всё равно одобряем (запретов на это нет), просто помечаем в CRM dm_blocked: true. Такие юзеры всё ещё подписаны на канал и видят посты.

Как проверить, что заявка пришла именно с конкретной ссылки?

В объекте ChatJoinRequest есть поле invite_link с заголовком и именем ссылки. Сравнивайте invite_link.name со своим списком источников. Это и даёт атрибуцию: «эта подписка — с креатива №42 в Telegram Ads».

Нужно ли спрашивать согласие на DM отдельно?

Технически нет — подача заявки = разрешение. Юридически по 152-ФЗ для маркетинговых рассылок нужно явное согласие на обработку ПД и рекламу. Поэтому первое DM делайте сервисным («одобрено, добро пожаловать»), а перед прогревной серией явно спрашивайте «разрешите присылать вам полезные материалы?» с кнопкой согласия.

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

Можно, но это даст ложноположительные срабатывания у 5–10% реальных юзеров (особенно у новичков в Telegram). Лучше комбинировать признаки: нет аватара + нет username + аккаунт младше 30 дней — отклоняем; иначе одобряем.

Как сегментировать аудиторию по разным invite-ссылкам?

Создайте по одной createChatInviteLink на каждый источник трафика с уникальным name. В обработчике chat_join_request берите invite_link.name и кладите в профиль юзера. Дальше воронка автоматически идёт через нужный сценарий — у каждого источника может быть своя цепочка прогрева.