Skip to main content

aiogram 3 extension that lifts Telegram's 64-byte callback_data limit by storing payloads in an external storage keyed by a short hash.

Project description

aiogram-callback-data

A small extension for aiogram 3 that lifts Telegram's 64-byte callback_data limit.

Instead of serializing your payload into the callback string itself, this library stores the full payload in an external storage (in-memory or Redis) and sends only a short lookup key to Telegram. This lets you pass arbitrarily large or deeply nested models through inline buttons.

Features

  • Drop-in CallbackData base class built on pydantic.BaseModel.
  • Storage backends: InMemoryStorage (dev) and RedisStorage (prod).
  • Optional TTL on every pack() call.
  • One-shot buttons — the entry is deleted on the first click.
  • Fully typed, compatible with aiogram's filter/DI system.

Installation (pip)

pip install aiogram-callback-data

# with Redis support
pip install "aiogram-callback-data[redis]"

Installation (uv)

uv add aiogram-callback-data

# With Redis support
uv add "aiogram-callback-data[redis]"

Python >= 3.10 is required.

Quick start

from __future__ import annotations

import asyncio

from aiogram import Bot, Dispatcher
from aiogram.types import Message, CallbackQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder

from aiogram_callback_data import CallbackData, setup
from aiogram_callback_data.storages import InMemoryStorage


bot = Bot(token="YOUR_TOKEN")
dp = Dispatcher()
storage = InMemoryStorage()


class UserAction(CallbackData, prefix="user"):
    user_id: int
    action: str
    # arbitrarily large payload — no 64-byte limit anymore
    context: dict[str, str] = {}


@dp.message()
async def send_menu(msg: Message) -> None:
    builder = InlineKeyboardBuilder()

    approve_key = await UserAction(
        user_id=msg.from_user.id,
        action="approve",
        context={"source": "menu", "version": "v2"},
    ).pack(ttl=300)

    reject_key = await UserAction(
        user_id=msg.from_user.id,
        action="reject",
    ).pack(ttl=300)

    builder.button(text="✅ Approve", callback_data=approve_key)
    builder.button(text="❌ Reject", callback_data=reject_key)

    await msg.answer("Choose:", reply_markup=builder.as_markup())


@dp.callback_query(UserAction.filter())
async def handle_action(call: CallbackQuery, callback_data: UserAction) -> None:
    await call.answer(
        f"User {callback_data.user_id}{callback_data.action}",
        show_alert=True,
    )


if __name__ == "__main__":
    setup(storage, dp)
    asyncio.run(dp.start_polling(bot))

One-shot buttons

Pass once=True to filter() to make a button usable exactly once. The storage entry is deleted atomically on the first match, so subsequent clicks raise HashNotFoundError.

@dp.callback_query(UserAction.filter(once=True))
async def handle_once(call: CallbackQuery, callback_data: UserAction) -> None:
    await call.answer("Accepted. This button is now dead.")

Redis backend

from aiogram_callback_data.storages import RedisStorage

storage = RedisStorage.from_url("redis://localhost:6379/0")
setup(storage, dp)

RedisStorage uses redis.asyncio under the hood and implements GETDEL for one-shot buttons.

How it works

  1. CallbackData.pack() dumps the model to JSON, hashes it (md5, 32 chars), builds a key of the form "<prefix>:<hash>", and saves the JSON under that key.
  2. Only the short key is sent to Telegram as callback_data.
  3. On incoming CallbackQuery, CallbackData.filter() matches the prefix, loads the JSON from storage, and validates it back into your model — which is then injected into the handler as callback_data.

Prefix length is capped at 31 characters (64-byte Telegram limit − ":" − 32-char hash) and is validated at class-declaration time.

Error handling

If the storage entry is missing or expired (e.g. the user clicks an old button), the filter raises HashNotFoundError:

from aiogram import Router
from aiogram.types import ErrorEvent
from aiogram_callback_data.errors import HashNotFoundError

router = Router()


@router.errors()
async def on_error(event: ErrorEvent) -> bool:
    if isinstance(event.exception, HashNotFoundError):
        call = event.update.callback_query
        if call:
            await call.answer("This button has expired.", show_alert=True)
        return True
    return False

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aiogram_callback_data-1.0.0.tar.gz (7.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aiogram_callback_data-1.0.0-py3-none-any.whl (11.1 kB view details)

Uploaded Python 3

File details

Details for the file aiogram_callback_data-1.0.0.tar.gz.

File metadata

  • Download URL: aiogram_callback_data-1.0.0.tar.gz
  • Upload date:
  • Size: 7.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.7

File hashes

Hashes for aiogram_callback_data-1.0.0.tar.gz
Algorithm Hash digest
SHA256 9354215eb5307ad0e7ef2ca66c8f5bbb2cf25889e217702438cfc6bc10420234
MD5 a430779e908b199abbf8a867da279c55
BLAKE2b-256 e2b5a5eee67e29f5f0f804acf145420c79dd0635a2ab4f46ad3c8b642d7d1f76

See more details on using hashes here.

File details

Details for the file aiogram_callback_data-1.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for aiogram_callback_data-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1f140831e95d4c0484d828236eed1fb8c70707cfddcb644224e35d35ceaadb37
MD5 6d8fd2fc61959c6902142392e948a3a8
BLAKE2b-256 c9d8ba659af85b79fe31bf55232fef1dd4adcb8cd223f84802ebc3263bdbcde8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page