A Telegram bot doesn't need a VPS. On cPanel hosting with Setup Node.js App or Setup Python App it runs around the clock for a few dollars a month — as long as it uses a webhook instead of polling. This guide shows both versions, a Node.js (Express) bot and a Python (Flask) bot, and how to fix the errors people usually hit.
Why a webhook, not polling
Most tutorials start the bot with polling (bot.launch(), run_polling(), getUpdates in a loop). That needs a process that runs forever. On cPanel, apps are started by Phusion Passenger when a request arrives and can be stopped when idle, so a polling loop goes quiet. With a webhook it's the other way round: Telegram sends every message to an HTTPS address of yours, that request starts the app if needed, and your bot answers. Exactly what Passenger is made for.
1. Create the app on its own subdomain
- In cPanel create a subdomain such as
bot.example.com(free SSL is issued automatically on most hosts — wait until it shows a valid certificate). - Open Setup Node.js App (Node 18 or newer) or Setup Python App (Python 3.10+), set the Application URL to that subdomain and the Application root to a folder outside
public_html, e.g.telegram-bot. - Add two environment variables in the same screen:
BOT_TOKEN(from @BotFather) andWEBHOOK_SECRET(a random string of letters, digits,_and-). Never put the token in your code.
2a. Node.js version (Express)
Set the startup file to app.js, add express to package.json, run NPM install from the app screen, and use:
const express = require('express');
const app = express();
app.use(express.json());
const TOKEN = process.env.BOT_TOKEN;
const SECRET = process.env.WEBHOOK_SECRET;
app.post('/telegram', async (req, res) => {
if (req.get('X-Telegram-Bot-Api-Secret-Token') !== SECRET) return res.sendStatus(403);
res.sendStatus(200); // answer Telegram first
const msg = req.body.message;
if (msg && msg.text) {
await fetch(`https://api.telegram.org/bot${TOKEN}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: msg.chat.id, text: 'You said: ' + msg.text }),
});
}
});
app.listen(process.env.PORT || 3000);
2b. Python version (Flask)
Install flask in the app's virtual environment (the Run pip install box or pip install flask over SSH) and put this in passenger_wsgi.py:
import os, json, urllib.request
from flask import Flask, request, abort
app = Flask(__name__)
TOKEN = os.environ['BOT_TOKEN']
SECRET = os.environ['WEBHOOK_SECRET']
@app.post('/telegram')
def telegram():
if request.headers.get('X-Telegram-Bot-Api-Secret-Token') != SECRET:
abort(403)
msg = (request.get_json(silent=True) or {}).get('message') or {}
if msg.get('text'):
data = json.dumps({'chat_id': msg['chat']['id'], 'text': 'You said: ' + msg['text']}).encode()
req = urllib.request.Request(f'https://api.telegram.org/bot{TOKEN}/sendMessage',
data=data, headers={'Content-Type': 'application/json'})
urllib.request.urlopen(req, timeout=10)
return 'ok'
application = app # Passenger looks for "application"
Restart the app from the cPanel screen after every change.
3. Tell Telegram where your bot lives
Run this once (from your computer or the cPanel Terminal), with your own token, address and secret:
curl https://api.telegram.org/bot<TOKEN>/setWebhook \
-d url=https://bot.example.com/telegram \
-d secret_token=<WEBHOOK_SECRET>
Then check it with https://api.telegram.org/bot<TOKEN>/getWebhookInfo. A healthy bot shows your URL, pending_update_count close to 0 and no last_error_message. Send your bot a message — it should answer within a second.
Common errors
- getWebhookInfo says "Wrong response from the webhook: 503" — Passenger can't start the app: wrong startup file, a missing package, or a crash on start (often a missing environment variable). Open the app's log in cPanel; see also fixing a Node.js 503 on cPanel.
- "404 Not Found" — the URL in setWebhook doesn't match the route (
/telegram), or the app is mounted on a sub-path instead of the subdomain's root. - "SSL error" / "certificate verify failed" — the subdomain's certificate isn't issued yet or is self-signed. Run AutoSSL in cPanel and set the webhook again.
- Replies are slow or repeated — answer Telegram with 200 first and do slow work afterwards; if Telegram doesn't get a reply in time, it sends the same update again.
- "Conflict: can't use getUpdates method while webhook is active" — you still have a polling version running somewhere. Stop it; a bot uses either polling or a webhook.
What about Discord bots?
Discord bots built on discord.js or discord.py keep a permanent connection to Discord's gateway, so they need a process that never stops — not a request-driven app. Slash-command bots that use Discord's Interactions endpoint URL work with a webhook just like Telegram. If you're not sure which kind you have, ask us before ordering.
Want it set up for you?
Our Node.js and Python hosting runs webhook bots like this one on cPanel with free SSL, and if your bot won't start, we fix it at a fixed price.