Skip to content
Murali Krishnan A
All writing

18 Sept 2026

Jellyfin from scratch, part 3: a Telegram bot, and why it needs its own API server

Part three of the Jellyfin series. Creating a Telegram bot with BotFather, hitting the 20 MB download wall almost immediately, and running your own local Telegram Bot API server so files up to 2 GB are just a path on disk instead of a capped download.

6 min read · jellyfin, selfhosted, telegram, beginners

This is part 3 of a five-part guide to running Jellyfin on your own server and connecting it to a Telegram bot. Parts 1 and 2 got Jellyfin itself running behind HTTPS. This part is entirely on the Telegram side; the two meet in part 4.

The idea is simple enough that it sounds like it should take an afternoon: message a Telegram bot with a file, the server downloads it, saves it where Jellyfin is watching, and tells Jellyfin to rescan. The first version I wrote did take an afternoon. It also silently failed on anything over 20 MB, which for video is essentially everything. This part is about why, and the fix, before any of it becomes the script in part 4.

Creating the bot

Telegram bots are created by talking to another bot, @BotFather, which is Telegram's own. In the Telegram app:

  1. Search for @BotFather, start a chat.
  2. Send /newbot.
  3. Give it a display name, then a username - has to end in bot, has to be globally unique.
  4. BotFather replies with a token.

The whole exchange, start to finish, looks like this:

You:       /newbot
BotFather: Alright, a new bot. How are we going to call it?
           Please choose a name for your bot.

You:       Media Server Upload Bot
BotFather: Good. Now let's choose a username for your bot.
           It must end in `bot`. Like this, for example: TetrisBot or tetris_bot.

You:       my_media_upload_bot
BotFather: Done! Congratulations on your new bot. You will find it at
           t.me/my_media_upload_bot. You can now add a description, about
           section and profile picture for your bot, see /help for a list
           of commands.

           Use this token to access the HTTP API:
           123456789:AAF-fakeExampleTokenDoNotUseThisOne

           Keep your token secure and store it safely, it can be used by
           anyone to control your bot.

That token is a bearer credential. Anyone who has it can send messages as your bot and read everything sent to it. Treat it like a password - specifically, don't hardcode it into a script you commit to a public repository, which is a mistake worth naming plainly because I've made it myself. Part 4 covers where it actually belongs.

You also want your own numeric Telegram user ID, so the bot only ever acts on messages from you. Message @userinfobot and it replies with your ID immediately:

You:        /start
userinfobot: Id: 123456789
             First: Your Name
             Username: @your_username
             Language: en

The Id field is the number part 4's ALLOWED_USER_ID checks against.

Hitting the wall

A minimal bot that saves whatever document you send it, using the popular python-telegram-bot library, is short:

from telegram.ext import Application, MessageHandler, filters

async def save_file(update, context):
    doc = update.message.document
    tg_file = await doc.get_file()
    await tg_file.download_to_drive(f"/home/ubuntu/files/ott/{doc.file_name}")

app = Application.builder().token("YOUR_BOT_TOKEN").build()
app.add_handler(MessageHandler(filters.Document.ALL, save_file))
app.run_polling()

Send it a 200 MB rip of something and get_file() raises. The error is some variation of "file is too big," and it is not a bug in the library. Telegram's standard, cloud-hosted Bot API - the one every bot talks to at api.telegram.org by default - will not hand a bot a file over 20 MB through getFile, full stop, regardless of your plan or account type.

The fix: run your own copy of the Bot API server

Telegram publishes the actual server that api.telegram.org runs - tdlib/telegram-bot-api - as open source, specifically so you can run your own instance instead of the shared one. Point your bot at your own server and two things change: the file size ceiling goes from 20 MB to 2000 MB, and getFile stops returning a URL to download and starts returning a path on disk, because the file is already sitting right there on your machine - no second download needed at all.

  Standard:  bot code ─► api.telegram.org ─► "here's a URL, 20 MB max"
  Local:     bot code ─► your own server  ─► "here's a path on disk, up to 2 GB"

Getting an API ID and hash

Running your own server needs credentials for the underlying Telegram client protocol, separate from the bot token. Go to my.telegram.org, log in with your phone number, and under API development tools, register an application - the name and description can be anything. You get back an api_id (a number) and an api_hash (a string). These identify your server to Telegram's network, not your bot specifically.

Running the server

The easiest path on Ubuntu is the aiogram/telegram-bot-api Docker image, a prebuilt copy of the official server:

sudo apt install -y docker.io
sudo mkdir -p /home/ubuntu/telegram-bot-api-data

sudo docker run -d \
  --name telegram-bot-api \
  --restart always \
  -p 127.0.0.1:8081:8081 \
  -e TELEGRAM_API_ID=<your api_id> \
  -e TELEGRAM_API_HASH=<your api_hash> \
  -e TELEGRAM_LOCAL=true \
  -v /home/ubuntu/telegram-bot-api-data:/var/lib/telegram-bot-api \
  aiogram/telegram-bot-api:latest

A few of these flags matter more than they look:

Check it's up:

$ curl http://127.0.0.1:8081
{"ok":false,"error_code":404,"description":"Not Found"}

That 404 is expected and good - it means something is listening and answering in Telegram's API shape. It just doesn't have a route for a bare /.

Pointing your bot at it

One change to the minimal script from earlier: tell the Application builder to use your server instead of Telegram's.

from telegram.ext import Application

app = (
    Application.builder()
    .token("YOUR_BOT_TOKEN")
    .base_url("http://127.0.0.1:8081/bot")
    .base_file_url("http://127.0.0.1:8081/file/bot")
    .build()
)

That's the entire fix. Same bot token, same BotFather setup, same filters.Document.ALL handler from before - just talking to a server that lives on your own box instead of Telegram's, with a 2 GB ceiling instead of a 20 MB one. Part 4 is what the handler actually does with the path it gets back.

What people get wrong

Next in the series

Part 4 writes the actual handler: taking the path the local server hands back, copying it into Jellyfin's library folder, fixing a permissions issue that will bite you on the first real file, and calling Jellyfin's refresh endpoint so the new file shows up without you lifting a finger.