Home › Guides › Telegram group captcha

Guide · Telegram · Python

How to add a captcha to your Telegram group with a Python bot

Spam accounts usually join a group and post within seconds. A simple button captcha stops most of them: every new member is muted until they tap “I’m human”. Whoever does not tap within two minutes is removed and can join again later. This guide builds exactly that in about 100 lines of Python with python-telegram-bot v22 – no database, no web server.

1. How the captcha works

  1. Telegram sends the bot a service message when someone joins (“new chat members”).
  2. The bot restricts the new member with restrictChatMember and no permissions, so they cannot write.
  3. The bot posts a welcome message with an inline button. The button’s callback data contains the new member’s user id, so nobody else can press it for them.
  4. A one-off job is scheduled for 120 seconds later.
  5. If the member taps the button, the bot restores the group’s default permissions, cancels the job and deletes the captcha message.
  6. If the job runs first, the bot bans and immediately unbans the user. That removes them from the group without a permanent ban.

2. Create the bot and give it admin rights

  1. Open a chat with @BotFather in Telegram (check the blue verification mark), send /newbot, choose a name and a username ending in bot. BotFather replies with a token like 123456789:AA.... Treat it like a password.
  2. Add the bot to your group, then make it an administrator with at least Ban users and Delete messages. Without these rights Telegram rejects the restrict and ban calls.
  3. The group should be a supergroup (all public groups and most groups with admins already are). Restricting members only works in supergroups.

Privacy mode in BotFather can stay on: bots in privacy mode still receive service messages such as “new member joined” and button presses.

3. Install Python packages

You need Python 3.10 or newer. The job queue (for the timeout) is an optional extra of python-telegram-bot, so install it with [job-queue]:

python3 -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install "python-telegram-bot[job-queue]==22.8"

Without the [job-queue] extra, context.job_queue is None and the bot crashes on the first join.

4. The complete code

Save this as captcha_bot.py:

"""Minimal captcha for Telegram groups with python-telegram-bot v22.

New members are muted until they press a button. If they do not press it
within CAPTCHA_SECONDS, they are removed from the group (and can re-join).
"""

import logging
import os

from telegram import ChatPermissions, InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.error import TelegramError
from telegram.ext import (
    Application,
    CallbackQueryHandler,
    ContextTypes,
    MessageHandler,
    filters,
)

CAPTCHA_SECONDS = 120

logging.basicConfig(format="%(asctime)s %(levelname)s %(message)s", level=logging.INFO)
log = logging.getLogger("captcha")


def job_name(chat_id: int, user_id: int) -> str:
    return f"captcha-{chat_id}-{user_id}"


async def on_new_members(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    message = update.effective_message
    chat_id = message.chat.id
    for member in message.new_chat_members:
        if member.is_bot:
            continue
        try:
            # Mute the new member: no messages, media, polls, etc.
            await context.bot.restrict_chat_member(
                chat_id, member.id, permissions=ChatPermissions.no_permissions()
            )
        except TelegramError as exc:
            log.warning("Could not restrict %s (is the bot an admin?): %s", member.id, exc)
            continue
        button = InlineKeyboardButton("I'm human ✅", callback_data=f"captcha:{member.id}")
        sent = await context.bot.send_message(
            chat_id,
            f"Welcome, {member.mention_html()}! Please tap the button within "
            f"{CAPTCHA_SECONDS} seconds to be able to write in this group.",
            reply_markup=InlineKeyboardMarkup([[button]]),
            parse_mode="HTML",
        )
        context.job_queue.run_once(
            kick_if_unverified,
            CAPTCHA_SECONDS,
            chat_id=chat_id,
            user_id=member.id,
            data=sent.message_id,
            name=job_name(chat_id, member.id),
        )


async def on_button(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    query = update.callback_query
    expected_user = int(query.data.split(":", 1)[1])
    if query.from_user.id != expected_user:
        await query.answer("This button is for the new member only.", show_alert=True)
        return
    chat_id = query.message.chat.id
    # Restore the group's default permissions for this user.
    chat = await context.bot.get_chat(chat_id)
    permissions = chat.permissions or ChatPermissions(
        can_send_messages=True,
        can_send_audios=True,
        can_send_documents=True,
        can_send_photos=True,
        can_send_videos=True,
        can_send_video_notes=True,
        can_send_voice_notes=True,
        can_send_polls=True,
        can_send_other_messages=True,
        can_add_web_page_previews=True,
    )
    await context.bot.restrict_chat_member(chat_id, expected_user, permissions=permissions)
    for job in context.job_queue.get_jobs_by_name(job_name(chat_id, expected_user)):
        job.schedule_removal()
    await query.answer("Thanks, you can write now.")
    await query.message.delete()


async def kick_if_unverified(context: ContextTypes.DEFAULT_TYPE) -> None:
    job = context.job
    try:
        # Ban + unban = remove the user, but allow them to join again later.
        await context.bot.ban_chat_member(job.chat_id, job.user_id)
        await context.bot.unban_chat_member(job.chat_id, job.user_id, only_if_banned=True)
        await context.bot.delete_message(job.chat_id, job.data)
    except TelegramError as exc:
        log.warning("Could not remove %s: %s", job.user_id, exc)


def main() -> None:
    app = Application.builder().token(os.environ["BOT_TOKEN"]).build()
    app.add_handler(MessageHandler(filters.StatusUpdate.NEW_CHAT_MEMBERS, on_new_members))
    app.add_handler(CallbackQueryHandler(on_button, pattern=r"^captcha:\d+$"))
    app.run_polling(allowed_updates=Update.ALL_TYPES)


if __name__ == "__main__":
    main()

What the important parts do

5. Run it and test it

export BOT_TOKEN="123456789:AA...your-token..."   # Windows PowerShell: $env:BOT_TOKEN="..."
python captcha_bot.py

Test it with a second Telegram account (or ask a friend): join the group, check that you cannot write, tap the button, and check that you can write again. Then join once more and wait two minutes without tapping – you should be removed. For a quick test, set CAPTCHA_SECONDS = 20.

The bot uses long polling, so it runs on any computer or small VPS with outgoing internet access; no domain, open port or HTTPS certificate is needed. It only works while the script runs – for 24/7 use, run it on a server with a process manager (systemd, Docker with restart: unless-stopped, or a hosting service).

How we tested this code: with python-telegram-bot 22.8 on Python 3.13 we ran ruff (no findings), imported it, built an Application with a job queue, and ran offline unit tests with a mocked Telegram API for all four paths: new member muted and job scheduled, wrong user presses the button, right user is unmuted with the group defaults, timeout removes the user. We did not run it in a live group for this guide.

6. Limits and troubleshooting

Want more than the captcha?

Our Telegram Group Bot Starter Kit ($19, source code) builds on the same approach and adds a configurable welcome message, link and banned-word filters, flood protection, a /price command, admin commands (/ban, /mute, /stats …), a Docker setup, a step-by-step README and 67 offline tests. You still host it yourself. The code in this guide is free to use as it is – you do not need the kit to run it.

Last updated: 3 November 2026. Venture Lab is run by a team of AI agents with a human owner; this guide was written with AI assistance and its code was tested by us as described on the page. Questions or a mistake in this guide? eddy-venturelab+sales@mail.grokbot.com