- Alerts
- Node.js
- Getting started
Telegram bot notifications from Node.js: send alerts to a chat with the Bot API
Send Telegram bot notifications from Node.js: create a bot, find the chat ID, format with HTML, stay under rate limits and handle 400, 403 and 429 errors.
- Author
- Extenify Team
- Published
- Reading time
- 11 min read

On this page
Telegram bot notifications are one of the simplest ways to get alerts from your own code onto your phone. A bot costs nothing, sending a message is a single HTTPS request, and the same bot can post to you, to a team group or to a channel. That is why so many developers use it for deploy notices, failed cron jobs, signup pings and store alerts such as a new 1-star review or a competitor's update.
It is also easy to get wrong in ways that only show up in production: a chat ID that works in testing but not in the team group, a message that fails because of one underscore, or a burst of alerts that runs into Telegram's flood control. This guide walks through the whole setup in Node.js with no library, then covers formatting, rate limits and the errors you will actually see.
Key takeaways
- Create a bot with @BotFather, keep the token in an environment variable, and treat it like a password: Telegram says everyone who has it "will have full control over your bot".
- Get the chat ID by messaging the bot and calling
getUpdates. Group IDs are negative numbers, and a bot can only message a person who has started a chat with it.- Use
parse_mode: 'HTML'and escape&,<and>. MarkdownV2 requires escaping 18 characters, and one missed character rejects the whole message.- Telegram's limits: avoid more than one message per second per chat, at most 20 messages per minute in a group, and about 30 messages per second across all chats.
- On a 429, wait the number of seconds in
parameters.retry_after; on 403, stop sending to that chat until a person fixes it.
Step 1: Create a bot and protect the token
Open a chat with @BotFather in Telegram, send /newbot, and pick a display name and a username ending in bot. BotFather replies with a token that looks like 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11, the format Telegram's Bot API documentation uses as its example.
That token is the bot's only credential. Telegram's bot overview is blunt about it: "Everyone who has your token will have full control over your bot." So:
- Store it in an environment variable or your secret manager, never in source code or a client app.
- Never put it in a browser extension or a mobile app. Send alerts from your server and let clients call your server.
- If it leaks, use BotFather's
/revokecommand to generate a new token and invalidate the old one.
The platform is free for developers: Telegram describes the bot platform as "free for both users and developers", so there is no API key or plan to buy.
Step 2: Find the chat ID
Every message goes to a chat_id. You find it by talking to the bot first:
- Private chat. Open your bot in Telegram, press Start, and send any message.
- Group. Add the bot to the group and send a message that mentions it, such as
/start@your_bot. By default bots run in privacy mode in groups and, according to the Bot FAQ, only receive commands meant for them, replies to their messages and a few other cases. - Channel. Add the bot as an administrator. For a public channel you can also use its
@channelusernameas thechat_id.
Then ask Telegram what the bot has received:
curl -s "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/getUpdates" \
| jq '.result[] | (.message // .channel_post) | {id: .chat.id, type: .chat.type, title: .chat.title}'
Private chat IDs are positive numbers; group and channel IDs are negative. They can be larger than 32 bits, so store them as strings or 64-bit integers, never as 32-bit values. Two gotchas: getUpdates does not work while a webhook is set for the bot, and Telegram keeps pending updates for no longer than 24 hours, so send a fresh message right before you call it.
The order matters for private chats. A bot cannot open a conversation with a person; the person has to start it. Until they do, sendMessage fails with "Forbidden: bot can't initiate conversation with a user", one of the most common errors in automation forums such as this Make Community thread. If you build a product that sends Telegram alerts to customers, give each user a deep link like https://t.me/your_bot?start=<code>. Telegram's bot features page explains that the bot then receives /start <code>, which lets you connect that chat to the user's account; the parameter can be up to 64 characters of A-Z, a-z, 0-9, _ and -.
Step 3: Send your first notification
Every Bot API call is an HTTPS request to https://api.telegram.org/bot<token>/METHOD_NAME. A test from the terminal:
curl -s -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" \
-H 'content-type: application/json' \
-d "{\"chat_id\": \"$TELEGRAM_CHAT_ID\", \"text\": \"Hello from the server\"}"
The same in Node.js 18 or newer, with built-in fetch:
// telegram.mjs: send one Telegram bot notification.
const API = `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}`;
export async function sendTelegram(chatId, html, { silent = false } = {}) {
const res = await fetch(`${API}/sendMessage`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
chat_id: chatId,
text: html,
parse_mode: 'HTML',
link_preview_options: { is_disabled: true },
disable_notification: silent,
}),
});
const body = await res.json();
// Every response has a boolean "ok"; errors add "description",
// "error_code" and sometimes "parameters".
if (!body.ok) {
const err = new Error(`Telegram ${body.error_code}: ${body.description}`);
err.code = body.error_code;
err.parameters = body.parameters;
throw err;
}
return body.result;
}
Three options in that body are worth knowing:
link_preview_optionsreplaceddisable_web_page_previewin Bot API 7.0 (December 29, 2023). Turning previews off keeps alert messages compact.disable_notificationsends the message silently: users get a notification with no sound. Use it for low-priority alerts and at night.message_thread_idposts into a specific topic of a forum supergroup, so one group can hold separate topics for reviews, releases and errors.
Format messages with HTML, not MarkdownV2
Telegram supports three parse modes. The legacy Markdown mode is kept only for backward compatibility. MarkdownV2 is expressive but strict: the documentation lists 18 characters that must be escaped with a backslash outside formatting: underscore, asterisk, square brackets, parentheses, tilde, backtick, greater-than sign, hash, plus, minus, equals sign, vertical bar, curly braces, period and exclamation mark. Version numbers (2.4.1), ratings (4.5) and URLs are full of them.
When one is missed, Telegram rejects the entire message with a 400 such as "can't parse entities: Can't find end of the entity starting at byte offset 23". That exact error appears in a Node-RED forum thread, caused by underscores in a device name.
HTML mode needs far less care. The formatting rules say that <, > and & outside tags must be replaced with <, > and &, and only a short list of tags is supported, including <b>, <i>, <u>, <s>, <a href>, <code>, <pre> and <blockquote>, a few aliases such as <strong>, and Telegram-specific tags for spoilers and custom emoji. A small helper covers it:
// format.mjs: build an HTML alert body safely.
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
const escAttr = (s) => esc(s).replace(/"/g, '"');
export function alertHtml({ title, lines = [], url }) {
const parts = [`<b>${esc(title)}</b>`, '', ...lines.map(esc)];
if (url) parts.push('', `<a href="${escAttr(url)}">Open details</a>`);
return parts.join('\n');
}
// alertHtml({ title: 'Rating dropped: 4.6 -> 4.4', lines: ['Chrome Web Store, version 2.4.1'] })
Keep each message under the limit: text must be 1 to 4,096 characters after entities are parsed. For long digests, split on line breaks and send several messages, spaced out as described next.
Respect Telegram rate limits
The Bot FAQ gives three numbers:
| Scope | Limit |
|---|---|
| One chat | Avoid more than one message per second; short bursts may pass, then you get 429 errors |
| One group | Bots cannot send more than 20 messages per minute |
| All chats | About 30 messages per second, unless paid broadcasts are enabled |
When you exceed them, the API returns error 429 with parameters.retry_after, which the reference defines as "the number of seconds left to wait before the request can be repeated". A small per-chat queue that spaces messages and honours retry_after is enough for alerting:
// queue.mjs: one message at a time per chat, with flood-control retries.
import { sendTelegram } from './telegram.mjs';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const queues = new Map(); // chatId -> Promise chain
export function enqueue(chatId, html, opts) {
const prev = queues.get(chatId) ?? Promise.resolve();
const next = prev.then(() => deliver(chatId, html, opts));
queues.set(chatId, next.catch(() => {})); // keep the chain alive after a failure
return next;
}
async function deliver(chatId, html, opts, attempt = 1) {
try {
const result = await sendTelegram(chatId, html, opts);
await sleep(String(chatId).startsWith('-') ? 3100 : 1100); // 20/min in groups, 1/s in private chats
return result;
} catch (err) {
const wait = err.parameters?.retry_after;
if (err.code === 429 && wait && attempt <= 3) {
await sleep((wait + 1) * 1000);
return deliver(chatId, html, opts, attempt + 1);
}
throw err;
}
}
The 3.1-second gap for negative IDs keeps a group under 20 messages a minute. If you send to many chats at once, also cap the total rate, and remember that one digest beats twenty pings anyway.
Handle the errors that need a human
Some errors should never be retried. The API returns a human-readable description and an error_code that, in Telegram's words, is "subject to change in the future", so log the description and branch on the code carefully:
| Error | Usual cause | What to do |
|---|---|---|
| 400 "chat not found" | Wrong ID, missing minus sign for a group, or the bot is not in the group | Re-check the ID with getUpdates; add the bot to the group |
| 400 "can't parse entities" | Unescaped characters in Markdown or broken HTML | Switch to HTML mode and escape text |
400 with migrate_to_chat_id |
The group was upgraded to a supergroup and has a new ID | Store the new ID from parameters.migrate_to_chat_id and resend |
| 403 "bot can't initiate conversation" | The user never pressed Start | Ask the user to open the bot first |
| 403 "bot was blocked by the user" or "kicked" | Someone blocked or removed the bot | Disable the channel and tell the account owner |
| 429 "Too Many Requests" | Flood control | Wait retry_after seconds, then retry |
The migrate_to_chat_id case is easy to miss: when a regular group is upgraded to a supergroup, the old ID stops working. Handling it automatically saves a confused "alerts stopped" message from your team.
Design Telegram bot notifications people keep reading
A bot that posts every small change gets muted within a week. Group related changes into one message, send routine updates silently with disable_notification, and reserve loud alerts for things that need action today. Our guide to store change alerts your team will actually read covers how to sort changes into "act today", "look this week" and "good to know".
A typical alert for an extension or app team looks like this:
- Act today: rating dropped by 0.2 or more, a burst of 1-star reviews, the listing disappeared.
- This week: a competitor shipped a new version, weekly users moved more than usual.
- Digest: new reviews, small rating moves, version adoption.
To post new App Store and Google Play reviews somewhere your team works, the same pattern applies; our guide to App Store review alerts shows the reading side with the official APIs. For browser extensions, the Chrome Web Store stats API guide shows how to collect a daily snapshot you can diff and send. The same bot also works for backend health signals, such as a falling cache hit rate in an AI feature, which our prompt caching guide shows how to measure. If you are still choosing what to build, the Chrome extension ideas guide uses the same daily snapshots to watch a niche before you commit.
Or get store alerts in Telegram without writing the bot logic
If the alerts you want are about store listings, Extenify already does the collection, diffing and delivery. It is an app store rating alerts and extension monitoring dashboard that tracks installs, weekly users, ratings, reviews and versions on the Chrome Web Store, Edge Add-ons, Firefox Add-ons, Google Play and the App Store, for your products and your competitors. Add a Telegram channel with your own bot token and chat ID, confirm it with a six-digit code, and daily change alerts arrive in your chat, alongside email, Slack, Discord or Microsoft Teams. Every account starts with a 7-day free trial, and the pricing page lists the plans.
Frequently asked questions
How do I get my Telegram chat ID?
Send a message to your bot (or mention it in the group), then open https://api.telegram.org/bot<token>/getUpdates and read message.chat.id. Private chats have positive IDs; groups and channels have negative IDs. If the result is empty, check that no webhook is set and send a fresh message.
Why does Telegram return "Bad Request: chat not found"?
The bot cannot see that chat. The ID may be wrong or missing its minus sign, the bot may not be a member of the group or an admin of the channel, or the group may have been upgraded to a supergroup with a new ID.
Can a Telegram bot send a message to a user first?
No. A person must start the conversation, for example by pressing Start in the bot's chat. Until then, sending fails with a 403 "bot can't initiate conversation with a user" error.
How many messages can a Telegram bot send per second?
Telegram's FAQ says to avoid more than one message per second in a single chat, no more than 20 messages per minute in a group, and about 30 messages per second across all chats. Bots with paid broadcasts enabled can go up to 1,000 messages per second, paying 0.1 Stars per message above the free 30.
Is the Telegram Bot API free?
Yes. Creating a bot and sending messages is free for developers. Only optional features such as paid broadcasts cost Telegram Stars.
Should I use a webhook or getUpdates for notifications?
For outgoing notifications you need neither in production: sendMessage is a plain request. Use getUpdates once to find chat IDs, and set up a webhook only if your bot also needs to react to messages, such as commands or the /start deep link.
Summary
Telegram bot notifications need a bot token from BotFather, a chat ID from getUpdates, and one sendMessage request. Keep the token on your server, format messages in HTML and escape three characters, stay under one message a second per chat and 20 a minute per group, and retry only on 429 after retry_after. Treat 403 and "chat not found" as configuration problems for a person to fix, follow migrate_to_chat_id when groups are upgraded, and design messages so the important ones stand out.
Image credits
- Cover photo: Swifts on telephone cables by Sadie-Michaela Harris on the WordPress Photo Directory, CC0.


