A Telegram bot you can run on the Free plan. Telegram delivers each message to the app as an HTTPS request, so the bot sleeps while the chat is quiet and wakes up for the next message. Python, with three small dependencies.
Deploy it on velixir, paste in a token from @BotFather, and you have a working bot in a few minutes.
It answers /start, /help and /id, and repeats anything else you send it in a private
chat. That is deliberately little. It is the plumbing you would otherwise write first, with a
clear place to put your own commands.
- Registers its own webhook. On every boot it tells Telegram where to deliver updates:
https://<your-app>.velixir.run/telegram, or your own domain. - Checks every update. Telegram sends a secret header with each delivery. The app derives that secret from the bot token, compares it in constant time, and rejects anything without it, so nobody else can post fake messages to the URL.
- Answers commands from a small command table that is easy to extend.
- Never crash-loops on bad configuration. With no token it serves a page with the BotFather steps. A wrong token, Telegram being unreachable, a reply that fails: each is logged and shown on the status page, and the app keeps running.
- A status page at
/with the bot's username, the webhook state, what Telegram itself reports about deliveries, and the last error. It never shows the token. /healthzfor the platform health check.
A Telegram bot either keeps asking Telegram for new messages (long polling) or has Telegram send them over (a webhook). Polling needs a process that never stops. A webhook needs nothing at all between messages.
velixir's Free plan puts an app to sleep after 15 minutes without HTTP traffic and wakes it on the next request. For this bot every message is a request, so it sleeps when the chat goes quiet and wakes when someone writes. It also uses little memory (about 30 MB under load), so 256 MB is plenty.
One thing to expect: the first message after a sleep is answered a few seconds later than the rest, while the app starts. If a delivery fails in that window, Telegram tries it again, so the message is answered late rather than lost. If you want instant replies around the clock, move the app to Hobby, which never sleeps. The code stays the same.
- Create a bot. In Telegram, open @BotFather, send
/newbotand choose a name and a username ending inbot. BotFather replies with a token like123456789:AAH.... - Deploy this template with the button above.
- Add the token. On the app's Environment tab, add
TELEGRAM_BOT_TOKENwith the token as its value, and tick Treat as secret. - Redeploy, with the Redeploy button on the live release in the Deploys tab.
Open the app's URL: the status page should say the webhook is registered. Then send your bot
/start.
Until the token is set, the app serves those same steps as its home page rather than crashing,
and /healthz stays green.
By default Telegram delivers to https://<slug>.velixir.run/telegram. To use your own domain:
- Add the domain on the app's Domains tab and set up DNS as it describes. Wait until it shows as verified: Telegram only delivers over HTTPS with a valid certificate.
- On the Environment tab, set
PUBLIC_URLto the address, such ashttps://bot.example.com(no path). - Redeploy. On boot the bot registers the new address with Telegram.
The velixir.run address keeps serving the status page, but a bot has only one webhook, so from then on every update arrives through your domain.
python -m venv .venv
. .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python bot.pyThen open http://localhost:8080. With no token set, you get the setup page.
To receive messages on your own machine, Telegram needs an HTTPS address that reaches it. Run a
tunnel (such as cloudflared tunnel --url http://localhost:8080), then:
TELEGRAM_BOT_TOKEN=... PUBLIC_URL=https://<your-tunnel-address> python bot.pyUse a second bot for this. A bot has exactly one webhook, so a local run with your live bot's token takes the webhook away from the deployed app until that app next boots. BotFather will make you a test bot in a minute.
velixir deployCommands live in the COMMANDS table near the top of bot.py. Each one is an async
function that gets the message and the text after the command, and returns the reply as
Telegram HTML:
import random
async def dice(message: dict, args: str) -> str:
return f"You rolled a {random.randint(1, 6)}."
COMMANDS["dice"] = ("Roll a die", dice)It shows up in /help straight away, and in Telegram's / menu after the next deploy. Pass
anything that came from a user through html.escape before it goes into a reply.
To react to more than text messages (the buttons of an inline keyboard, edited messages), add
the update type to ALLOWED_UPDATES and handle it in handle_update. Telegram only sends the
types you ask for, which on the Free plan also means fewer wake-ups.
- Nothing is stored. The counters on the status page live in memory and start again on every restart, and the container's disk is wiped on every deploy. For anything the bot should remember (subscribers, settings, a to-do list), bind a managed Postgres and keep it there.
- One webhook per bot. Whichever copy of the bot registered last gets the messages, which is why local development belongs on a separate test bot. If another copy takes over, the status page says so.
- The secret comes from the token. It is an HMAC of the token, so it stays the same across restarts and replicas with nothing to configure. Revoke the token in BotFather, redeploy with the new one, and the secret changes with it.
- Groups. In a group Telegram only shows a bot the commands meant for it (privacy mode),
and this bot only repeats messages in private chats. Use
/setprivacyin BotFather if your bot needs to read the whole conversation. - Telegram's rate limits apply. It asks bots to stay around one message a second per chat, 20 a minute per group and 30 a second overall. A bot that broadcasts needs a queue.
- Outbound traffic is web only. Calls to other HTTPS APIs work. SMTP does not, so send email through an HTTP API such as Resend or Postmark.
| Variable | Required | Meaning |
|---|---|---|
TELEGRAM_BOT_TOKEN |
yes | The token from @BotFather. Without it, the app serves its setup page. |
PUBLIC_URL |
no | Your own domain, such as https://bot.example.com. Defaults to https://<slug>.velixir.run. |
TELEGRAM_API_BASE |
no | Where the Bot API lives. Defaults to https://api.telegram.org. Point it at a mock server to test. |
| Method | Path | Does |
|---|---|---|
GET |
/ |
The status page, or the setup steps when no token is set |
POST |
/telegram |
Where Telegram delivers updates. Anything without the right secret header gets a 401 |
GET |
/healthz |
200 while the process runs, with the bot's state in the body |
MIT.