Telegram Bot API
🌐 English

How to Send a Message with the Telegram Bot API (sendMessage)

· 7 min read

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.

ParameterPurpose
chat_idRequired. Numeric chat id or @channelusername.
textRequired. 1–4096 characters after entity parsing.
parse_modeHTML, MarkdownV2 or legacy Markdown.
reply_markupInline keyboard, reply keyboard, keyboard removal or force reply.
link_preview_optionsDisable or tune the link preview.
disable_notificationNotify without sound.
reply_parametersReply to a specific message.
message_thread_idPost 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 &lt;, &gt; and &amp;, so escape user input before inserting it:

import html

name = html.escape("Tom & Jerry <3", quote=False)  # -> Tom &amp; Jerry &lt;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

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"}.

ErrorCauseFix
400 Bad Request: chat not foundWrong 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 parsedUnescaped MarkdownV2 character or invalid HTML.Escape the text, or resend without parse_mode to find the culprit.
401 UnauthorizedInvalid or revoked token.Check the token with getMe.
403 Forbidden: bot was blocked by the userThe user blocked your bot.Stop sending to this user; retries won't help.
429 Too Many RequestsFlood 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

  1. Open sendMessage in the Bot API Tester and paste your token (it stays in your browser's localStorage).
  2. Fill in chat_id and text, and optionally parse_mode and reply_markup.
  3. 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.

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.