How to Send a Message with the Telegram Bot API (sendMessage)
To send a message with the Telegram Bot API, call sendMessage at https://api.telegram.org/bot<token>/sendMessage with two required parameters: chat_id (the recipient) and text (1–4096 characters). This guide gives copy-paste examples in curl, Python and JavaScript, then covers HTML and MarkdownV2 formatting, inline buttons, groups and channels, and the errors you will actually run into.
Telegram Bot API sendMessage: request format
Every Bot API call is an HTTPS request to https://api.telegram.org/bot<token>/METHOD_NAME. Note the literal bot prefix right before the token, with no slash in between. Both GET and POST work, and parameters can be passed as a query string, application/x-www-form-urlencoded, application/json or multipart/form-data.
| Parameter | Purpose |
|---|---|
chat_id | Required. Numeric chat id or @channelusername. |
text | Required. 1–4096 characters after entity parsing. |
parse_mode | HTML, MarkdownV2 or legacy Markdown. |
reply_markup | Inline keyboard, reply keyboard, keyboard removal or force reply. |
link_preview_options | Disable or tune the link preview. |
disable_notification | Notify without sound. |
reply_parameters | Reply to a specific message. |
message_thread_id | Post into a forum topic. |
No token or chat id yet? See how to get a bot token and how to find a chat id. Keep in mind that bots can't start conversations: the user has to message your bot first.
Send a message with curl
The quickest way to send a message to Telegram via the API from a terminal, cron job or CI pipeline is a GET request with URL-encoded parameters:
curl "https://api.telegram.org/bot123456789:AAEexampleTokenReplaceWithYours/sendMessage?chat_id=123456789&text=Hello%20from%20curl"
For anything beyond plain text, POST a JSON body:
curl -X POST "https://api.telegram.org/bot123456789:AAEexampleTokenReplaceWithYours/sendMessage" \
-H "Content-Type: application/json" \
-d '{"chat_id": 123456789, "text": "Hello from curl"}'
A successful call returns the sent Message (shortened):
{
"ok": true,
"result": {
"message_id": 42,
"chat": {"id": 123456789, "first_name": "Alex", "type": "private"},
"date": 1793266200,
"text": "Hello from curl"
}
}
Store message_id if you want to edit or delete the message later.
Send a message with Python
Using the requests library:
import requests
TOKEN = "123456789:AAEexampleTokenReplaceWithYours"
API = f"https://api.telegram.org/bot{TOKEN}"
def send_message(chat_id, text, **params):
payload = {"chat_id": chat_id, "text": text, **params}
data = requests.post(f"{API}/sendMessage", json=payload, timeout=10).json()
if not data["ok"]:
raise RuntimeError(f'{data["error_code"]}: {data["description"]}')
return data["result"]
msg = send_message(123456789, "Hello from Python")
print("sent", msg["message_id"])
Send a message with JavaScript fetch
Works in Node.js 18+ (as an ES module) or any runtime with fetch:
const TOKEN = "123456789:AAEexampleTokenReplaceWithYours";
async function sendMessage(chatId, text, extra = {}) {
const res = await fetch(`https://api.telegram.org/bot${TOKEN}/sendMessage`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ chat_id: chatId, text, ...extra }),
});
const data = await res.json();
if (!data.ok) throw new Error(`${data.error_code}: ${data.description}`);
return data.result;
}
const msg = await sendMessage(123456789, "Hello from JavaScript");
console.log("sent", msg.message_id);
Run this on a server: anyone who has the token controls the bot, so never ship it in front-end code or a public repository.
Formatting with parse_mode HTML
With "parse_mode": "HTML" you can use only these tags: <b>, <strong>, <i>, <em>, <u>, <ins>, <s>, <strike>, <del>, <tg-spoiler> (or <span class="tg-spoiler">), <a href>, <code>, <pre>, <pre><code class="language-python">, <blockquote> and <blockquote expandable>, plus <tg-emoji> for custom emoji and <tg-time> for formatted dates and times.
Every <, > and & that is not part of a tag must be written as <, > and &, so escape user input before inserting it:
import html
name = html.escape("Tom & Jerry <3", quote=False) # -> Tom & Jerry <3
send_message(
123456789,
f"<b>Order #1024</b> for {name} is <i>shipped</i>\n"
'<a href="https://example.com/track/1024">Track parcel</a>',
parse_mode="HTML",
)
Formatting with parse_mode MarkdownV2
MarkdownV2 uses *bold*, _italic_, __underline__, ~strikethrough~, ||spoiler||, [text](https://example.com), `code`, triple backticks for code blocks and > for quotes. The catch: anywhere outside of entities these characters must be escaped with a backslash:
_ * [ ] ( ) ~ ` > # + - = | { } . !
The backslash itself must be escaped too. Inside pre and code only ` and \ need escaping; inside the (…) part of a link, only ) and \. A forgotten dot in "v1.2" or a dash in a date is the classic cause of a 400 error about parsing entities. Escape dynamic text with a helper:
import re
def escape_md2(text):
return re.sub(r"([_*\[\]()~`>#+\-=|{}.!\\])", r"\\\1", text)
send_message(123456789, f"*Release {escape_md2('v1.2-beta')}* is out\\!",
parse_mode="MarkdownV2")
const escapeMd2 = (s) => s.replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, "\\$&");
Add inline buttons with reply_markup
An inline keyboard is a list of button rows under the message. A url button opens a link; a callback_data button (1–64 bytes) sends that string back to your bot in a callback_query update.
curl -X POST "https://api.telegram.org/bot123456789:AAEexampleTokenReplaceWithYours/sendMessage" \
-H "Content-Type: application/json" \
-d '{
"chat_id": 123456789,
"text": "What next?",
"reply_markup": {"inline_keyboard": [[
{"text": "Open site", "url": "https://example.com"},
{"text": "Press", "callback_data": "pressed"}
]]}
}'
In a JSON body reply_markup is a nested object; in a query string or form data, pass it as a JSON-encoded string. After a tap on a callback button, Telegram shows a progress indicator until you call answerCallbackQuery, so always answer, even without text:
cq = update["callback_query"]
requests.post(f"{API}/answerCallbackQuery",
json={"callback_query_id": cq["id"], "text": "Done!"}, timeout=10)
Send a message to a group, channel or topic
- Groups: add the bot to the group and use its id. Basic groups have negative ids; supergroups look like
-1001234567890. - Channels: the bot must be an administrator allowed to post. Public channels accept
"chat_id": "@channelusername"; private channels need the numeric-100…id. - Forum topics: add
message_thread_idto post into a specific topic.
When a basic group is upgraded to a supergroup, its id changes. The error response then contains parameters.migrate_to_chat_id: save the new id and retry.
Silent messages, link previews and replies
{
"chat_id": 123456789,
"text": "Release notes: https://example.com/changelog",
"disable_notification": true,
"protect_content": true,
"link_preview_options": {"is_disabled": true},
"reply_parameters": {"message_id": 42}
}
disable_notification delivers the message with a soundless notification, and protect_content blocks forwarding and saving. link_preview_options replaced the old disable_web_page_preview: use is_disabled to hide the preview, or url, prefer_large_media and show_above_text to shape it.
Common sendMessage errors and fixes
A failed call returns "ok": false with error_code and description, for example {"ok": false, "error_code": 400, "description": "Bad Request: chat not found"}.
| Error | Cause | Fix |
|---|---|---|
| 400 Bad Request: chat not found | Wrong chat_id, the user never started the bot, or the bot isn't in the group or channel. | Re-check the id with getUpdates, ask the user to press Start, add the bot to the chat. |
| 400, entities can't be parsed | Unescaped MarkdownV2 character or invalid HTML. | Escape the text, or resend without parse_mode to find the culprit. |
| 401 Unauthorized | Invalid or revoked token. | Check the token with getMe. |
| 403 Forbidden: bot was blocked by the user | The user blocked your bot. | Stop sending to this user; retries won't help. |
| 429 Too Many Requests | Flood limit exceeded. | Wait parameters.retry_after seconds, then retry. |
A minimal retry for 429:
import time
def send_with_retry(chat_id, text, **params):
while True:
payload = {"chat_id": chat_id, "text": text, **params}
r = requests.post(f"{API}/sendMessage", json=payload, timeout=10).json()
if r["ok"]:
return r["result"]
if r["error_code"] != 429:
raise RuntimeError(r["description"])
time.sleep(r.get("parameters", {}).get("retry_after", 1))
For broadcasts, a retry is not enough: read Telegram Bot API limits to build a throttled queue.
Try sendMessage in your browser
- Open sendMessage in the Bot API Tester and paste your token (it stays in your browser's localStorage).
- Fill in
chat_idandtext, and optionallyparse_modeandreply_markup. - Click Execute or press Ctrl+Enter and inspect the JSON response.
All parameters, including the rarely used ones, are listed in the sendMessage reference.
Frequently asked questions
How do I send a message to Telegram via the API?
Call https://api.telegram.org/bot<token>/sendMessage with GET or POST and pass chat_id and text. On success you get "ok": true and the sent Message object in result.
How do I send a Telegram message with curl?
Run curl -X POST "https://api.telegram.org/bot<token>/sendMessage" -H "Content-Type: application/json" -d '{"chat_id": 123456789, "text": "Hello"}' with your own token and chat id.
Which characters must be escaped in MarkdownV2?
Outside of entities escape _ * [ ] ( ) ~ ` > # + - = | { } . ! and the backslash itself with a preceding \. Inside pre and code only ` and \ must be escaped.
Can a bot send a message to a user who never started it?
No. Bots can't start conversations: the user has to message the bot first or add it to a group. Until then sendMessage fails, typically with 400 chat not found.
How do I send a message to a Telegram channel with a bot?
Make the bot a channel administrator that can post messages, then use "chat_id": "@channelusername" for a public channel or the numeric id such as -1001234567890 for a private one.
Why does sendMessage return 429 Too Many Requests?
You exceeded a flood limit (about 1 message per second in one chat, 20 per minute in a group, ~30 per second in bulk). Wait parameters.retry_after seconds, retry, and throttle your sending queue.
Related guides
How to Create a Telegram Bot: Step by Step with BotFather and Python
How to create a Telegram bot step by step: register it with @BotFather, get the token, test it with getMe and send a first message with curl or Python.
Telegram Bot API 10.0–10.3: Rich Messages, Ephemeral Messages, Guest Mode and More
What's new in Telegram Bot API 10.0–10.3: guest mode, rich messages, streamed drafts, ephemeral messages, join request queries, with JSON examples.
Telegram Bot API Limits: Rate Limits, 429 Errors, File Sizes and Message Length
Telegram Bot API rate limits explained: 30 msg/s, 1 msg/s per chat, 20/min in groups, handling 429 retry_after, plus file size and message length limits.
Telegram Bot Token: How to Get It, Find It Again, Check It and Keep It Safe
Telegram bot token guide: get one from @BotFather, find the token of an existing bot, check it with getMe, revoke a leaked token and store it safely.
How to Set Up a Telegram Bot Webhook: setWebhook, Secret Token and Troubleshooting
Set up a Telegram bot webhook: setWebhook URL requirements, secret_token header check, getWebhookInfo debugging, deleteWebhook and fixes for n8n.
How to Get a Telegram Chat ID for a User, Group, Channel or Topic
How to get a Telegram chat ID for a user, group, supergroup, channel or forum topic with getUpdates, plus fixes for an empty result and 409 Conflict.
How to Test a Telegram Mini App: From Browser Mock to Real Telegram
Test a Telegram Mini App step by step: browser mock mode, localhost via HTTPS tunnel, test environment, DevTools per platform, initData validation.