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-apiand imported asdjango_telegram.
Features
- Every Bot API method on a single
Telegramclient: 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.idorupdate.callback_query.datainstead 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
- Quick start
- Handling updates
- More examples
- Calling the API directly
- Configuration reference
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:
- Configure your settings
- Create the bot client
- Write a handler
- Add the webhook view and URL
- 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
CommandHandlersubclass, and you decide what to do for/start,/help, and so on insidehandle(). - 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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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.