How to Send Telegram Messages from a Linux Server (Alerts from Any Script in One curl)

ntfy (tutorial 93) is the self-hosted way to reach your phone. Telegram is the no-setup way: a bot takes two minutes to create, works from anywhere without exposing anything, supports photos and files, and nearly every tool in this series already has a Telegram option. The only parts people get stuck on are the chat ID and the first curl.
Quick reference
| Step | What |
|---|---|
| Make a bot | message @BotFather → /newbot → copy the token |
| Chat ID | message the bot, then curl https://api.telegram.org/bot/getUpdates |
| Send text | curl -s -d chat_id=ID -d text="hello" https://api.telegram.org/bot/sendMessage |
| Send a photo | -F photo=@door.jpg to /sendPhoto |
| Send a file | -F document=@backup.log to /sendDocument |
| Group | add the bot to the group; the chat ID is negative |
1. Create the bot
In Telegram, open @BotFather, send /newbot, give it a name ("Home Server") and a username ending in bot. It replies with a token like 123456789:AAH.... That token is a password; it goes in .env or a chmod 600 file, never in a script you'll paste anywhere.
2. Find your chat ID
Send the bot any message (it can't message you first). Then on the server:
curl -s "https://api.telegram.org/bot<TOKEN>/getUpdates" | python3 -m json.tool | grep -A2 '"chat"'
The "id" under "chat" is your chat ID. For a family group: add the bot to the group, send a message there, and the ID appears as a negative number.
3. First message
curl -s -X POST "https://api.telegram.org/bot<TOKEN>/sendMessage" \
-d chat_id=<CHAT_ID> -d text="Hello from $(hostname)"
Phone buzzes. That's the API.
4. A notify function for every script
sudo nano /etc/telegram.env
TG_TOKEN=123456789:AAH...
TG_CHAT=987654321
sudo chmod 600 /etc/telegram.env. Then at the top of any script:
source /etc/telegram.env
notify() {
curl -s -X POST "https://api.telegram.org/bot$TG_TOKEN/sendMessage" \
-d chat_id="$TG_CHAT" -d parse_mode=HTML \
--data-urlencode text="<b>$(hostname)</b>: $1" > /dev/null
}
notify "Backup starting"
if /usr/local/bin/backup.sh; then
notify "✅ Backup finished"
else
notify "❌ Backup FAILED — check /var/log/backup.log"
fi
--data-urlencode handles spaces, symbols and multi-line text; parse_mode=HTML allows , and .
5. Photos and files
curl -s -F chat_id=$TG_CHAT -F photo=@/tmp/front_door.jpg -F caption="Someone at the door" \
"https://api.telegram.org/bot$TG_TOKEN/sendPhoto"
curl -s -F chat_id=$TG_CHAT -F document=@/var/log/backup.log \
"https://api.telegram.org/bot$TG_TOKEN/sendDocument"
A Frigate snapshot (tutorial 91), df -h output as a file, a Grafana panel rendered to PNG — anything.
6. A silent message and a reply keyboard
Night-time reports without a buzz: add -d disable_notification=true. A message with buttons that run something when pressed needs a listener; for that, n8n's Telegram trigger (tutorial 77) is the simple route — "Reboot?" with Yes/No that runs a command.
7. Everything else uses the same bot
Uptime Kuma, Watchtower, Grafana, Healthchecks, CrowdSec, NUT, Home Assistant's telegram_bot integration, Frigate via HA — all take the same token and chat ID. One bot, one chat, every alert in one thread; or a bot per purpose if you want separate chats with different notification sounds.
Troubleshooting
getUpdates returns an empty result. You haven't messaged the bot yet, or a webhook is set (another tool registered one — deleteWebhook clears it, but then that tool stops receiving).
"Bad Request: chat not found". Wrong chat ID, or the bot was removed from the group.
Messages with & or < fail. HTML parse mode: escape them (&, <), or drop parse_mode.
Nothing arrives from cron. The script can't read the env file (permissions/path), or no outbound internet from that host. Run it by hand with bash -x.
Wrapping up
BotFather, a token in a 600 file, getUpdates for the chat ID, a seven-line notify function. Every script, cron job and monitor on the server can now tap you on the shoulder with text, a photo or a log file — and the same bot plugs into every tool you've installed since tutorial 35.
