Skip to main content

django-telegram-bot-api

A typed Telegram Bot API client for Django.

Every Bot API method is a plain Python method, every Telegram type is a Pydantic model, and responses come back as typed objects instead of raw dictionaries.

pip install django-telegram-bot-api
from django_telegram import Telegram

bot = Telegram()  # reads BOT_TOKEN from your Django settings
message = bot.send_message(chat_id=123456, text="Hello!")
print(message.chat.id)

The package is installed as django-telegram-bot-api and imported as django_telegram.

Features

  • Every Bot API method on a single Telegram client: send messages, answer callback queries, set webhooks, and the rest of the API.
  • Every Telegram type as a Pydantic model, with validation and editor autocompletion.
  • Typed responses. Use message.chat.id or update.callback_query.data instead of digging through dictionaries.
  • A small dispatcher. Write one class per kind of update (commands, messages, button presses, inline queries) and the library routes incoming updates to it.
  • Built for Django. Configuration comes from your Django settings, and you keep your own views and URLs.
  • Synchronous. No event loop or async runtime required.

Contents

Installation

pip install django-telegram-bot-api

or with uv:

uv add django-telegram-bot-api

You do not need to add django_telegram to INSTALLED_APPS.

Requirements are listed in pyproject.toml (Python 3.13+ at the time of writing).

Quick start

This walks you from zero to a bot that answers /start. It has five steps:

  1. Configure your settings
  2. Create the bot client
  3. Write a handler
  4. Add the webhook view and URL
  5. Tell Telegram where your webhook is

1. Configure your settings

Create a bot with @BotFather to get a token, then add a TELEGRAM dictionary to your Django settings:

# settings.py
TELEGRAM = {
    "BOT_TOKEN": "your-telegram-bot-token",
    "TM_WEBHOOK_URL": "https://api.telegram.org",
}
Key Required Default Description
BOT_TOKEN Yes n/a Your bot token from @BotFather.
TM_WEBHOOK_URL Yes n/a Base URL of the Telegram Bot API. Use https://api.telegram.org, or the address of your own local Bot API server.
PROXY_SOCKS No "" SOCKS proxy for outgoing requests, e.g. 127.0.0.1:1080.

If a required key is missing, ImproperlyConfigured is raised and the message tells you which one.

2. Create the bot client

Create a Django app for your bot (this guide calls it bot):

python manage.py startapp bot

Add it to INSTALLED_APPS, then create one shared client that all your handlers will import:

# bot/client.py
from django_telegram import Telegram

bot = Telegram()  # reads BOT_TOKEN and TM_WEBHOOK_URL from settings

Your app will end up looking like this:

bot/
├── __init__.py
├── client.py
├── urls.py
├── views.py
└── handlers/
    ├── __init__.py
    └── command_handlers.py

3. Write a handler

Handlers are classes. You inherit from one base class per kind of update and implement handle(self, update). Commands (any message that starts with /) go to CommandHandler:

# bot/handlers/command_handlers.py
from django_telegram import CommandHandler, Update

from bot.client import bot


class Commands(CommandHandler):

    def handle(self, update: Update):
        chat_id = update.message.chat.id
        text = update.message.text

        if text.startswith("/start"):
            bot.send_message(chat_id=chat_id, text="Hello!")

Now register the file by importing it in your app's __init__.py:

# bot/__init__.py
from .handlers import command_handlers  # noqa: F401

4. Add the webhook view and URL

The view receives Telegram's updates and passes them to the dispatcher:

# bot/views.py
import logging

from django_telegram import Dispatcher, Update
from rest_framework import status
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView

logger = logging.getLogger(__name__)


class TelegramWebhookView(APIView):
    authentication_classes = []
    permission_classes = []

    def post(self, request: Request) -> Response:
        try:
            update = Update(**request.data)
            Dispatcher(update).dispatch()
        except Exception:
            logger.exception("Error while processing Telegram update")

        # Always answer 200, otherwise Telegram keeps retrying the same update.
        return Response({"ok": True}, status=status.HTTP_200_OK)

Add the route to the app, then include the app in your project URLs:

# bot/urls.py
from django.urls import path

from .views import TelegramWebhookView

app_name = "bot"

urlpatterns = [
    path("webhook/", TelegramWebhookView.as_view(), name="webhook"),
]
# config/urls.py
from django.urls import include, path

urlpatterns = [
    path("bot/", include("bot.urls", namespace="bot")),
]

Your webhook is now served at https://your-domain.com/bot/webhook/.

5. Tell Telegram where your webhook is

Telegram needs to know which URL to send updates to. Register it once with a curl request to the Bot API (replace <BOT_TOKEN> with your bot token and the URL with your own):

curl -X POST "https://api.telegram.org/bot<BOT_TOKEN>/setWebhook" \
  -d "url=https://your-domain.com/bot/webhook/"

If it worked, Telegram answers:

{"ok": true, "result": true, "description": "Webhook was set"}

To check the current webhook at any time:

curl "https://api.telegram.org/bot<BOT_TOKEN>/getWebhookInfo"

If you use your own local Bot API server, replace https://api.telegram.org with its address (the same value as TM_WEBHOOK_URL in your settings).

Telegram only delivers webhooks over HTTPS to a public address. While developing locally, expose your server with a tunnel such as ngrok or cloudflared and use the tunnel URL.

Now send /start to your bot. It should answer Hello!.

Handling updates

Dispatcher looks at an incoming update, decides what kind it is, and calls handle(update) on your subclass of the matching base class.

What the user does Base class to inherit from
Sends a command such as /start CommandHandler
Sends anything else (text, photo, document, voice, ...) MessageHandler
Presses an inline button CallbackQueryHandler
Types @yourbot something in any chat InlineQueryHandler
Picks one of your inline results ChosenInlineResultHandler

Things to know:

  • The dispatcher does not look at the command name or the text. Every command goes to your CommandHandler subclass, and you decide what to do for /start, /help, and so on inside handle().
  • One class per kind is the intended setup. If you define several subclasses of the same base class, all of them are called, in the order they were defined.
  • Updates with no matching handler are ignored. If you do not write a CallbackQueryHandler, button presses are simply dropped.
  • Handlers run inside the web request. Hand slow work (long downloads, heavy computation) to a task queue such as Celery.

The next section shows one example of each.

More examples

All examples assume the shared client from step 2 (from bot.client import bot) and the layout below. Remember to import each new file in bot/__init__.py.

bot/handlers/
├── __init__.py              (empty)
├── command_handlers.py
├── message_handlers.py
└── callback_query_handlers.py
# bot/__init__.py
from .handlers import (
    callback_query_handlers,  # noqa: F401
    command_handlers,  # noqa: F401
    message_handlers,  # noqa: F401
)

Commands: several commands and a reply keyboard

# bot/handlers/command_handlers.py
from django_telegram import CommandHandler, Update
from django_telegram.telegram_models import KeyboardButton, ReplyKeyboardMarkup

from bot.client import bot


class Commands(CommandHandler):

    def handle(self, update: Update):
        chat_id = update.message.chat.id
        text = update.message.text

        if text.startswith("/start"):
            bot.send_message(chat_id=chat_id, text="Hello!")

        elif text.startswith("/help"):
            bot.send_message(
                chat_id=chat_id,
                text="Send /products to see what we sell.",
            )

        elif text.startswith("/products"):
            bot.send_message(
                chat_id=chat_id,
                text="Choose one:",
                reply_markup=ReplyKeyboardMarkup(
                    keyboard=[
                        [
                            KeyboardButton(text="product 1"),
                            KeyboardButton(text="product 2"),
                        ],
                        [
                            KeyboardButton(text="product 3"),
                            KeyboardButton(text="product 4"),
                        ],
                    ],
                    resize_keyboard=True,
                ),
            )

        else:
            bot.send_message(chat_id=chat_id, text="Unknown command.")

Commands: reading arguments

Telegram sends everything after the command in the same text, so /greet Reza arrives as "/greet Reza":

class Commands(CommandHandler):

    def handle(self, update: Update):
        chat_id = update.message.chat.id
        parts = update.message.text.split()

        command = parts[0]   # "/greet"
        args = parts[1:]     # ["Reza"]

        if command == "/greet":
            name = args[0] if args else "stranger"
            bot.send_message(chat_id=chat_id, text=f"Hello, {name}!")

Messages: reply keyboard text and an inline button

When the user presses a reply keyboard button, Telegram sends its text as a normal message, so you handle it in a MessageHandler. Here the bot answers with a message that has an inline button attached:

# bot/handlers/message_handlers.py
from django_telegram import MessageHandler, Update
from django_telegram.telegram_models import InlineKeyboardButton, InlineKeyboardMarkup

from bot.client import bot


class Messages(MessageHandler):

    def handle(self, update: Update):
        chat_id = update.message.chat.id
        text = update.message.text

        if text == "product 1":
            bot.send_message(
                chat_id=chat_id,
                text="product 1",
                reply_markup=InlineKeyboardMarkup(
                    inline_keyboard=[
                        [
                            InlineKeyboardButton(text="Details", callback_data="pro1"),
                            InlineKeyboardButton(text="Buy", callback_data="buy:1"),
                        ],
                        [
                            InlineKeyboardButton(text="Open website", url="https://example.com"),
                        ],
                    ],
                ),
            )

        else:
            bot.send_message(chat_id=chat_id, text=f"You said: {text}")

A button either has a callback_data (Telegram sends it back to your bot when pressed) or a url (Telegram opens the link and tells your bot nothing).

Messages: photos and documents

There is no separate handler for media. Photos, documents, voice messages, and the like arrive in the same MessageHandler, with message.text empty and the matching field filled in. Check which one is present inside handle():

# bot/handlers/message_handlers.py
from django_telegram import MessageHandler, Update

from bot.client import bot


class Messages(MessageHandler):

    def handle(self, update: Update):
        message = update.message
        chat_id = message.chat.id

        if message.photo:
            # Telegram sends several sizes; the last one is the largest.
            file_id = message.photo[-1].file_id
            bot.send_message(chat_id=chat_id, text=f"Nice photo! file_id: {file_id}")

        elif message.document:
            bot.send_message(
                chat_id=chat_id,
                text=f"Got your file: {message.document.file_name}",
            )

        elif message.voice:
            bot.send_message(chat_id=chat_id, text="Got your voice message.")

        else:
            bot.send_message(chat_id=chat_id, text=f"You said: {message.text}")

If your bot handles both text buttons and media, put all the branches in the same handle(), since there is one MessageHandler subclass.

Callback queries: alerts, toasts, and editing the message

When the user presses an inline button, Telegram sends a callback query instead of a message:

# bot/handlers/callback_query_handlers.py
from django_telegram import CallbackQueryHandler, Update

from bot.client import bot


class Callbacks(CallbackQueryHandler):

    def handle(self, update: Update):
        query = update.callback_query
        data = query.data

        if data == "pro1":
            # Pop-up alert with an OK button
            bot.answer_callback_query(
                callback_query_id=query.id,
                text="hi this is product 1",
                show_alert=True,
            )

        elif data.startswith("buy:"):
            product_id = data.split(":")[1]

            # Small toast at the top of the screen
            bot.answer_callback_query(
                callback_query_id=query.id,
                text=f"Product {product_id} added to your cart",
            )

            # Replace the text of the message that held the button
            bot.edit_message_text(
                chat_id=query.message.chat.id,
                message_id=query.message.message_id,
                text=f"Product {product_id} is in your cart.",
            )

Putting data in callback_data ("buy:1") is the usual way to tell buttons apart. Telegram limits it to 64 bytes.

Inline queries

When someone types @yourbot something in any chat, Telegram sends an inline query. Answer it with a list of results:

# bot/handlers/inline_query_handlers.py
from django_telegram import InlineQueryHandler, Update

from bot.client import bot


class InlineQueries(InlineQueryHandler):

    def handle(self, update: Update):
        query = update.inline_query

        bot.answer_inline_query(
            inline_query_id=query.id,
            results=[
                {
                    "type": "article",
                    "id": "1",
                    "title": f"Search for {query.query}",
                    "input_message_content": {"message_text": query.query},
                },
            ],
        )

Inline mode must be enabled for your bot in @BotFather (/setinline). Do not forget to import the file in bot/__init__.py.

Calling the API directly

You do not need handlers to use the client. Anywhere in your Django project (a view, a management command, a Celery task, a signal), you can call the API and work with the typed response:

from bot.client import bot

# Send a message and read the response
message = bot.send_message(chat_id=123456, text="Your order has shipped.")
print(message.message_id, message.chat.id, message.date)

# Send a message with formatting
bot.send_message(
    chat_id=123456,
    text="<b>Order #42</b> is on its way.",
    parse_mode="HTML",
)

Because the types are Pydantic models, you also get message.model_dump() and message.model_dump_json() for free.

A common use is notifying users from your own code, for example when an order changes status:

# orders/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver

from bot.client import bot
from .models import Order


@receiver(post_save, sender=Order)
def notify_customer(sender, instance, **kwargs):
    if instance.status == "shipped":
        bot.send_message(
            chat_id=instance.customer.telegram_id,
            text=f"Order #{instance.pk} has shipped.",
        )

Configuration reference

All settings live in the TELEGRAM dictionary in your Django settings.

Key Required Default Description
BOT_TOKEN Yes n/a Bot token from @BotFather.
TM_WEBHOOK_URL Yes n/a Base URL of the Telegram Bot API (https://api.telegram.org or your own local Bot API server).
PROXY_SOCKS No "" SOCKS proxy for outgoing requests, e.g. 127.0.0.1:1080.

Settings are read when the client is created, not when Django starts, and override_settings works as usual in your tests.

Metadata

Release files for django-telegram-bot-api 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-telegram-bot-api 0.1.2
File Size Uploaded Provenance Actions
django_telegram_bot_api-0.1.2.tar.gz 50.0 kB Without provenance Details for django_telegram_bot_api-0.1.2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for django-telegram-bot-api 0.1.2
File Interpreter ABI Platform Actions
django_telegram_bot_api-0.1.2-py3-none-any.whl Python 3 none any Details for django_telegram_bot_api-0.1.2-py3-none-any.whl

Total release size: 102.6 kB

Release files / django_telegram_bot_api-0.1.2.tar.gz

Download URL django_telegram_bot_api-0.1.2.tar.gz
Size 50.0 kB
Tags Source
SHA-256 checksum
How to use checksums
e54b8e926f44969f89c11bd011600eaee27c861c297ec01e872a004c066d2010
BLAKE2b-256 checksum
How to use checksums
bafa68f83aa7e9daa2c0f628c7341332b23f483f8f30c0655136581494dade8d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Arch Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / django_telegram_bot_api-0.1.2-py3-none-any.whl

Download URL django_telegram_bot_api-0.1.2-py3-none-any.whl
Size 52.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c65f3e5c3f767c1d2c8f8b74ca0c28ff2a72dfacb45583e8677cf255309952a1
BLAKE2b-256 checksum
How to use checksums
e185662d8664e2bcfe1a72d199ebca229441a02d246ad2fd454ce0e7d75d471b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Arch Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for django-telegram-bot-api 0.1.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page