API Guides

Telegram Bot API: How to Post to a Channel (2026)

Post to a Telegram channel with the Telegram Bot API 10.3: BotFather setup, chat IDs, sendMessage, albums, rich messages, limits, and errors. See the code.
October 16, 2026 by Jesse Eisenbart19 min read
Oil painting of a grassy trail climbing a ridge above a valley filled with clouds, with sunlit mountain peaks beyond

TLDR: A Telegram bot posts to a channel with one HTTPS call: create it with BotFather, add it as a channel administrator with Post Messages, then call sendMessage, sendPhoto, or sendMediaGroup. Text caps at 4,096 characters, captions at 1,024, rich messages at 32,768. No app review, no fee, and no scheduling parameter. Current to Bot API 10.3 as of October 16, 2026.

A bot with one administrator right can publish to a Telegram channel as a human admin would: formatted text with buttons, photos, videos, albums, and, since June 2026, long structured documents. The only credential is a token from BotFather. PostZen’s Telegram integration publishes through one shared bot; this is the guide we would hand a developer doing it directly, checked against Telegram’s reference pages on October 16, 2026.

What is the Telegram Bot API, and can a bot post to a channel?

The Telegram Bot API is an HTTPS interface in front of Telegram’s servers. Every call has the shape https://api.telegram.org/bot<token>/METHOD_NAME, GET and POST both work, and parameters travel as a query string, application/x-www-form-urlencoded, application/json, or multipart/form-data for file uploads.

Every response is JSON with a Boolean ok. On success the payload is in result; on failure description is a human-readable sentence, error_code is an integer that Telegram says may change, and some errors carry a parameters object your code can act on. That envelope is the whole error model.

Can a bot post to a channel? Yes: the posts appear as the channel, not as the bot (in groups they show under the bot’s name), and the condition is one administrator right, covered next.

The version matters because the Telegram Bot API is moving fast. The Bot API changelog lists 10.3 on August 24, 2026, 10.2 on July 14, 10.1 on June 11 with rich messages, and 10.0 on May 8; the 9.x line ran through 2025. Most pages ranking for this topic are forum threads from 2016 to 2021, and none mentions rich messages. In our comparison of social media APIs, Telegram sits with Bluesky as a network you can publish to with no review queue and no fee.

How do you create a bot and add it to your channel?

Open a chat with @BotFather, send /newbot, and answer two prompts: a display name and a username ending in bot. BotFather replies with the token, shaped like 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11. Anyone holding it can post as your bot, so keep it in an environment variable (TELEGRAM_BOT_TOKEN below) and let BotFather reissue it if it leaks. getMe is the quick check that it works:

curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/getMe"

Then give the bot the channel. In the channel’s settings, open Administrators, add the bot by username, and switch on Post Messages. In the API that right is can_post_messages on ChatAdministratorRights: the administrator can post messages in the channel, approve suggested posts, or access channel statistics, and the field exists for channels only. Without it every send method fails with a rights error; getChatAdministrators lists each admin with its rights if you want to check.

Two things trip people. The bot must be an administrator, not a subscriber. And the right is per channel, so a bot posting to three channels is added to all three. In a group it only needs to be a member allowed to send messages.

How do you find a Telegram channel’s chat ID?

For a public channel you may not need a number. chat_id accepts an integer or a string, and @username works for public channels, supergroups, and bots: if your channel is t.me/yourchannel, then "chat_id": "@yourchannel" is enough. Private channels have no username, so you need the numeric id. Two ways to get it:

  1. Read it from an update. Add the bot as administrator, post anything in the channel, then call getUpdates. The post arrives as an Update with a channel_post field, and channel_post.chat.id is the number. Two caveats from the reference: getUpdates does not work while a webhook is set, and updates are kept at most 24 hours, so post and read in the same sitting.
  2. Ask getChat. For a public channel, getChat with @username returns a ChatFullInfo whose id is the numeric id. Store the number and a later username change cannot break you.
curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/getUpdates" \
  | jq '.result[] | select(.channel_post) | .channel_post.chat | {id, title}'

The number is negative and in practice starts with -100; our permalink code relies on that, building https://t.me/c/<id without the -100 prefix>/<message_id> for private channels and https://t.me/<username>/<message_id> for public ones. From the Chat reference: ids may exceed 32 bits but have at most 52 significant bits, so a signed 64-bit integer or a double holds them. JavaScript numbers are fine; with 32-bit integers, widen the column first.

How do you send a message to a channel with the Bot API?

The simplest post is one curl call to sendMessage:

curl -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" \
  -H "Content-Type: application/json" \
  -d '{
    "chat_id": "@yourchannel",
    "text": "<b>Bot API 10.3 is out.</b> Rich messages &amp; more.",
    "parse_mode": "HTML",
    "disable_notification": true
  }'

text takes 1 to 4,096 characters after entities parsing: tags do not count. The reply is the sent Message; keep its message_id to edit or delete the post later.

In Node.js 18 or later, as an ES module, with the envelope, HTML escaping, a disabled link preview, and an inline URL button:

// Node.js 18 or later, ES module. Bot API 10.3 as of October 16, 2026.
const TOKEN = process.env.TELEGRAM_BOT_TOKEN;
const API = `https://api.telegram.org/bot${TOKEN}`;
const CHANNEL = '@yourchannel'; // or -1001234567890 for a private channel

// Escape & first, otherwise you would escape the & inside &lt;.
export function escapeHtml(s) {
  return s
    .replaceAll('&', '&amp;')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;');
}

export async function callTelegram(method, body) {
  const res = await fetch(`${API}/${method}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  const json = await res.json().catch(() => ({}));
  if (!res.ok || json.ok !== true) {
    const err = new Error(json.description ?? `HTTP ${res.status}`);
    err.errorCode = json.error_code ?? res.status;
    err.parameters = json.parameters ?? {}; // retry_after, migrate_to_chat_id
    throw err;
  }
  return json.result;
}

export async function sendToChannel(title, body, url) {
  const text = `<b>${escapeHtml(title)}</b>\n\n${escapeHtml(body)}`;
  return callTelegram('sendMessage', {
    chat_id: CHANNEL,
    text,
    parse_mode: 'HTML',
    link_preview_options: { is_disabled: true },
    reply_markup: {
      inline_keyboard: [[{ text: 'Read the changelog', url }]],
    },
  });
}

const sent = await sendToChannel(
  'Bot API 10.3 is out',
  'Rich messages, ephemeral messages & more. Details behind the button.',
  'https://core.telegram.org/bots/api-changelog',
);
console.log(sent.message_id);

The same in Python with requests:

# Python 3.10 or later. Bot API 10.3 as of October 16, 2026.
import os
import requests

TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
API = f"https://api.telegram.org/bot{TOKEN}"
CHANNEL = "@yourchannel"  # or -1001234567890 for a private channel


def escape_html(s: str) -> str:
    # Escape & first, otherwise you would escape the & inside &lt;.
    return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")


def result_or_raise(res: requests.Response):
    try:
        body = res.json()
    except ValueError:
        body = {}
    if not res.ok or body.get("ok") is not True:
        code = body.get("error_code", res.status_code)
        desc = body.get("description", res.reason)
        err = RuntimeError(f"Telegram {code}: {desc}")
        err.error_code = code
        err.parameters = body.get("parameters", {})
        raise err
    return body["result"]


def call_telegram(method: str, **params):
    res = requests.post(f"{API}/{method}", json=params, timeout=30)
    return result_or_raise(res)


def send_to_channel(title: str, text: str, url: str) -> dict:
    html = f"<b>{escape_html(title)}</b>\n\n{escape_html(text)}"
    button = {"text": "Read the changelog", "url": url}
    return call_telegram(
        "sendMessage",
        chat_id=CHANNEL,
        text=html,
        parse_mode="HTML",
        link_preview_options={"is_disabled": True},
        reply_markup={"inline_keyboard": [[button]]},
    )

The parameters worth knowing on sendMessage, most of which exist on the media methods too:

Parameter What it does
chat_id @username for a public channel, or the numeric id
parse_mode HTML, MarkdownV2, or legacy Markdown; omit it for plain text, or send explicit entities instead
link_preview_options is_disabled turns the preview off; url picks which link to preview; show_above_text moves it above the text
disable_notification Sends silently: subscribers get the message with no sound
protect_content Blocks forwarding and saving of the message
reply_markup An inline keyboard; in a channel, URL buttons are the useful kind
reply_parameters Makes the post a reply to an earlier message in the channel
message_thread_id A forum topic; for forum supergroups and bot private chats with topics, not channels
allow_paid_broadcast Lifts the broadcast limit to 1,000 messages per second for 0.1 Stars each, withdrawn from the bot’s balance
message_effect_id A message effect; private chats only, so no use in a channel

Nothing in that list is a publish time; more on that below.

Should you format with HTML or MarkdownV2?

Telegram’s formatting options give you three parse modes plus explicit entities. Our recommendation, and the one in our own docs, is HTML, because it only requires escaping three characters.

HTML supports <b> or <strong>, <i> or <em>, <u> or <ins>, <s>, <strike>, or <del>, <tg-spoiler> or <span class="tg-spoiler">, <a href="...">, <code>, <pre> with an optional nested <code class="language-python">, <blockquote> and <blockquote expandable>, <tg-emoji>, and <tg-time>; nothing else. Every <, >, and & outside a tag or entity must become &lt;, &gt;, or &amp;; numeric entities work, but the only named entities accepted are &lt;, &gt;, &amp;, and &quot;, so an &nbsp; from a CMS fails. That is what escapeHtml above does, in that order, to any text you did not write yourself.

MarkdownV2 is nicer to type and hostile to user-generated text. In normal text these characters need a preceding backslash: _, *, [, ], (, ), ~, the backtick, >, #, +, -, =, |, {, }, ., and !. Inside pre and code you escape backticks and backslashes; inside the (...) of a link, ) and backslashes. One bare period in a product name is enough: an unescaped reserved character rejects the whole post.

Markdown without the V2 is a legacy mode kept for backward compatibility. It cannot nest entities and has no underline, strikethrough, spoiler, or block quote. Do not start a new integration on it. In every mode, block quotes cannot nest and pre and code cannot contain other entities.

How do you post photos, videos, and albums?

The sending files section gives three ways to attach media, each with its own ceiling:

  • A file_id of a file already on Telegram’s servers. No size limit; the type cannot change on resend; file_ids belong to one bot and, per the Bots FAQ, can be treated as persistent, so cache them when you repost an image.
  • An HTTP URL that Telegram downloads. 5 MB for photos, 20 MB for everything else; the URL must serve the correct MIME type; sendDocument by URL works only for PDF and ZIP.
  • A multipart upload. 10 MB for photos, 50 MB for other files.

Those caps are why PostZen downloads every media file and uploads the bytes itself rather than handing Telegram the URL: the URL route stops at half the upload limit.

sendPhoto takes a photo of at most 10 MB, width plus height at most 10,000 pixels, ratio at most 20, with a caption of 0 to 1,024 characters after entities parsing plus show_caption_above_media and has_spoiler. sendVideo is for MPEG4 up to 50 MB, with an optional thumbnail (JPEG under 200 kB, no side over 320 pixels), a cover, supports_streaming, and the same caption limit.

sendMediaGroup builds an album of 2 to 10 items; photos and videos mix, while documents and audio only group with their own type. It returns one Message per item. New uploads are referenced from the media array with attach://<name> and sent as multipart fields under that name. The reference puts a caption on each item; in common practice, and in our integration, the first item’s caption shows as the album caption.

In Node, both calls use the built-in FormData and Blob:

import { readFile } from 'node:fs/promises';

async function postForm(method, form) {
  const res = await fetch(`${API}/${method}`, { method: 'POST', body: form });
  const json = await res.json().catch(() => ({}));
  if (!res.ok || json.ok !== true) {
    throw new Error(`${json.error_code ?? res.status}: ${json.description}`);
  }
  return json.result;
}

// One photo with a caption. multipart/form-data, not JSON.
export async function sendPhotoFile(path, caption) {
  const form = new FormData();
  form.set('chat_id', CHANNEL);
  form.set('caption', caption); // 0 to 1,024 characters
  form.set('parse_mode', 'HTML');
  const bytes = await readFile(path);
  form.set('photo', new Blob([bytes], { type: 'image/jpeg' }), 'photo.jpg');
  return postForm('sendPhoto', form);
}

// An album of 2 to 10 items. Each upload is a multipart field that the
// media array points at with attach://<field name>.
export async function sendAlbum(paths, caption) {
  const form = new FormData();
  form.set('chat_id', CHANNEL);
  const media = [];
  for (const [i, path] of paths.entries()) {
    const name = `photo${i + 1}`;
    const bytes = await readFile(path);
    form.set(name, new Blob([bytes], { type: 'image/jpeg' }), `${name}.jpg`);
    const item = { type: 'photo', media: `attach://${name}` };
    if (i === 0) Object.assign(item, { caption, parse_mode: 'HTML' });
    media.push(item);
  }
  form.set('media', JSON.stringify(media));
  return postForm('sendMediaGroup', form); // an array of Message
}

In Python, requests builds the multipart body from files=:

import json


def send_photo_file(path: str, caption: str) -> dict:
    with open(path, "rb") as fh:
        res = requests.post(
            f"{API}/sendPhoto",
            data={"chat_id": CHANNEL, "caption": caption, "parse_mode": "HTML"},
            files={"photo": ("photo.jpg", fh, "image/jpeg")},
            timeout=60,
        )
    return result_or_raise(res)


def send_album(paths: list[str], caption: str) -> list[dict]:
    files, media = {}, []
    for i, path in enumerate(paths, start=1):
        name = f"photo{i}"
        files[name] = (f"{name}.jpg", open(path, "rb"), "image/jpeg")
        item = {"type": "photo", "media": f"attach://{name}"}
        if i == 1:
            item.update(caption=caption, parse_mode="HTML")
        media.append(item)
    try:
        res = requests.post(
            f"{API}/sendMediaGroup",
            data={"chat_id": CHANNEL, "media": json.dumps(media)},
            files=files,
            timeout=120,
        )
    finally:
        for _, fh, _ in files.values():
            fh.close()
    return result_or_raise(res)

The media field is a JSON string inside a multipart form, not a JSON request body; mixing the two is the usual reason an album call returns a 400.

What are rich messages, and when should you use them?

Rich messages arrived in Telegram Bot API 10.1 on June 11, 2026, and 10.2 and 10.3 extended them. They are a second kind of text post with a different ceiling and grammar: headings, lists, tables, media, block quotations, collapsible blocks, footnotes, and formulas, in one message of up to 32,768 UTF-8 characters. For release notes or digests, that is the difference between a 4,096-character squeeze and a document.

The method is sendRichMessage. It takes the same chat_id, disable_notification, protect_content, allow_paid_broadcast, reply_markup, and message_thread_id as sendMessage, plus a required rich_message. InputRichMessage holds exactly one of markdown, html, or blocks, and may add media (a list of InputRichMessageMedia the text references with links such as tg://photo?id=...), is_rtl, and skip_entity_detection.

The Markdown dialect is close to GitHub Flavored Markdown where it can be: # headings, **bold**, *italic*, ~~strike~~, ==marked==, ||spoiler||, fenced code, ordered, unordered, and task lists, block quotes, pipe tables, footnotes, and math with $x^2$. A release-notes post with a heading, a list, and a two-column table, using callTelegram from above:

const markdown = `
## Release notes, October 16

**What changed**

- Faster album uploads
- New silent-post option
- Retries that honour retry_after

| Limit | Value |
|:------|------:|
| Text message | 4,096 |
| Caption | 1,024 |
| Rich message | 32,768 |
`;

await callTelegram('sendRichMessage', {
  chat_id: CHANNEL,
  rich_message: { markdown },
  disable_notification: true,
});

The limits: 32,768 UTF-8 characters including custom emoji alternative text and formula source; 500 blocks, counting nested blocks, list items, table rows, quotation blocks, and details blocks; 16 levels of nesting; 50 media attachments; 20 columns per table.

Two cautions. sendRichMessageDraft is not for channels: it streams a partial rich message to one user as a 30-second ephemeral preview for AI replies in private chats, and the final message must still go out through sendRichMessage. And the reference says nothing about how older clients display a rich message, so neither will we; test with an old install first. PostZen sends standard messages and does not support rich messages as of October 16, 2026.

What are the Telegram Bot API limits?

Everything above in one place, from the Telegram Bot API reference and the Bots FAQ, as of October 16, 2026:

Limit Value
Text message (sendMessage) 1 to 4,096 characters after entities parsing
Caption (sendPhoto, sendVideo, album items) 0 to 1,024 characters after entities parsing
Rich message (sendRichMessage) 32,768 UTF-8 characters, 500 blocks, 16 nesting levels, 50 media, 20 table columns
Album (sendMediaGroup) 2 to 10 items; documents and audio only with their own type
Photo 10 MB uploaded, 5 MB by URL; width plus height at most 10,000; ratio at most 20
Video and other files 50 MB uploaded, 20 MB by URL; getFile downloads only up to 20 MB
File by file_id No size limit; cannot change type; per bot
Video thumbnail JPEG under 200 kB, no side over 320 pixels
Broadcast rate About 30 messages per second across users, then 429
Paid broadcast Up to 1,000 per second; 0.1 Stars per message above 30; needs 100,000 Stars on balance and 100,000 monthly active users
Update retention Until received, at most 24 hours; getUpdates and webhooks are mutually exclusive
Local Bot API server Uploads up to 2,000 MB, downloads with no size limit, local file paths, HTTP webhooks on any port

The last row is the escape hatch for large video: run a local Bot API server, point requests at it instead of api.telegram.org, and uploads go up to 2,000 MB.

The broadcast row needs care, because the number most guides quote is not the one Telegram documents today. Older guides cite per-chat limits: one message per second in a single chat, twenty per minute in a group. The current Bots FAQ does not list those numbers. It says bots message users at no cost but with broadcast limits, that “the API will not allow bulk notifications to more than ~30 users per second”, and that past it you get 429 errors; its suggestion for a free bot is to spread a broadcast over 8 to 12 hours. Our reading, an inference rather than a documented rule: that ceiling counts messages your bot sends, so one channel post reaching a million subscribers is one send. A bot that also messages each subscriber individually is the one that meets it.

How do you schedule posts to a Telegram channel?

You cannot ask Telegram to do it. No send method in the Telegram Bot API, sendMessage included, has a scheduled-time parameter; the table above is everything there is.

So scheduling is your job: store the post (text, media, target chat, publish time), run something that wakes at the right moment, and call the send method then. For one channel, a cron job that queries for due posts every minute is enough. Past that you want a queue with one worker claiming each post so two never publish the same one, the returned message_id stored so a retry can check whether the post already went out, and a rule for what to do when the send fails, which is the next section. Our social media scheduling API guide walks through those pieces.

What errors does the Bot API return?

The envelope is small: ok is false, error_code is an integer code whose contents Telegram says may change, description says what happened in English, and parameters sometimes carries one of two fields: retry_after, the seconds to wait after flood control, or migrate_to_chat_id, meaning a group became a supergroup and your stored chat id is stale. Match on error_code and parameters, and on fragments of description only when you must; neither the code nor the text is a contract.

What our publisher’s error mapping handles, matching fragments of Telegram’s descriptions case-insensitively:

Situation What Telegram returns What to do
Bot removed, never added, or lacking rights HTTP 403, or a description containing “bot was kicked”, “bot is not a member”, “not enough rights”, “have no rights to send a message”, or “CHAT_WRITE_FORBIDDEN” Re-add the bot as administrator with Post Messages
Wrong or stale chat id 400 with “chat not found” Re-check the id; private channels need the numeric one
Formatting error 400 when MarkdownV2 or HTML does not parse; one unescaped reserved character rejects the whole message Escape properly, or switch to HTML
Flood control 429 with parameters.retry_after in seconds Wait that long, then retry once
Group upgraded to supergroup 400 with parameters.migrate_to_chat_id Store the new id and resend
5xx after the request reached Telegram, or no response at all Ambiguous Do not auto-retry; log it and check the channel

The last row is the one that bites schedulers. A timeout or a 502 after you sent the request says nothing about whether the message was posted, and a blind retry can put the same post in the channel twice. PostZen treats that outcome as unknown and never retries it automatically: exactly one Bot API write happens per publish, after every check that can fail. A missing post is a cheaper mistake than a duplicate one; our Instagram Graph API error codes guide shows what a retry loop that discards the provider’s answer costs.

One detail the reference does not spell out: the wait arrives in the JSON body, not in an HTTP Retry-After header, which is why we read it from the body. A retry helper in Node, on top of callTelegram from above:

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

export async function callWithRetry(method, body) {
  try {
    return await callTelegram(method, body);
  } catch (err) {
    const wait = err.parameters?.retry_after;
    if (err.errorCode === 429 && typeof wait === 'number') {
      // Flood control. The wait is in the JSON body, not a Retry-After
      // header. Sleep that long, then try exactly once more.
      await sleep((wait + 1) * 1000);
      return callTelegram(method, body);
    }
    if (err.parameters?.migrate_to_chat_id) {
      // The group became a supergroup. Persist the new id, then resend.
      return callTelegram(method, {
        ...body, chat_id: err.parameters.migrate_to_chat_id,
      });
    }
    // 5xx, a network error after the send, or any 4xx: do NOT retry.
    // The message may already be in the channel. Log it and look.
    throw err;
  }
}

It retries in exactly two cases, the two where Telegram’s own response tells you how to repeat the request; everything else propagates to your scheduler with the description attached.

The shorter path: one request to every platform

Building directly on the Telegram Bot API is genuinely easy, and if Telegram is your only network you should: the code above is most of it. PostZen earns its place when Telegram is one of several networks, when you want scheduling without running a scheduler, or when your own users connect their own channels and you do not want to hold their bot tokens.

That last case is how our integration works. There is one shared bot, @PostZenScheduleBot, and no token changes hands: GET /v1/connect/telegram returns an authUrl for a hosted page that mints a single-use access code, PZ-XXXXXX, valid for 15 minutes. The user adds the bot as an administrator with Post Messages, then sends the code to the bot with the channel’s username, or forwards a message from the channel. The sender must be an administrator of that chat, and we check during connect.

One request then publishes to Telegram and two other networks:

curl -X POST https://api.postzen.dev/v1/posts \
  -H "Authorization: Bearer pzn_live_..." \
  -H "Content-Type: application/json" \
  -H "x-request-id: telegram-guide-2026-10-21" \
  -d '{
    "content": "New guide: posting to a Telegram channel with the Bot API.",
    "scheduledFor": "2026-10-21T09:00:00Z",
    "mediaItems": [{ "url": "https://cdn.example.com/telegram-cover.jpg" }],
    "platforms": [
      {
        "platform": "telegram",
        "accountId": "acc_telegram",
        "settings": { "parseMode": "html", "disableNotification": true }
      },
      { "platform": "twitter", "accountId": "acc_x" },
      { "platform": "linkedin", "accountId": "acc_linkedin" }
    ]
  }'
  • platforms holds one target per network. Telegram’s settings take parseMode (html or markdownv2; omit for plain text), disableNotification, disableLinkPreview for text-only posts, and protectContent.
  • scheduledFor is an ISO timestamp at least 60 seconds ahead, or send publishNow: true. x-request-id makes the create idempotent.
  • mediaItems are URLs we download and upload as bytes, so the URL caps never apply: photos to 10 MB, videos to 50 MB, 1 to 10 items per post, photos and videos mixed, a GIF on its own.

PostZen handles the connect flow, the 4,096 and 1,024 limits before the request leaves, one Bot API write per publish, retry_after from the body, the ambiguous-outcome rule, and post.published and post.failed webhooks. It does not do rich messages as of October 16, 2026, and it has no Telegram analytics.

The settings are in the Telegram integration docs, the Telegram integration page has the connect walkthrough, and the guide to posting everywhere at once shows one request reaching all ten platforms. The @postzen/node and postzen-sdk Python SDKs wrap the API, AI agents publish through the Telegram MCP server and our agent tooling, and request limits by plan are on the pricing page. Build directly if a channel is all you need; if it is one of several, that is what our social media API is for.

Frequently asked questions

Can a Telegram bot post to a channel?

Yes. Add the bot as an administrator of the channel with the Post Messages right, then call sendMessage, sendPhoto, sendVideo, or sendMediaGroup with the channel as chat_id, either its @username or its numeric id. The post appears as the channel, not as the bot. There is no app review, no fee, and the only credential is the token BotFather gave you.

How do I get the chat ID of a Telegram channel?

For a public channel you do not need one: pass @username as chat_id. For a private channel, add the bot as an administrator, post anything in the channel, call getUpdates, and read channel_post.chat.id from the result. It is a negative number that in practice starts with -100. getUpdates only works while no webhook is set, and Telegram keeps updates for at most 24 hours.

Is the Telegram Bot API free?

Yes, as of October 16, 2026. Creating a bot with BotFather and calling the Bot API costs nothing, and there is no review process. The only paid feature is paid broadcasting, which lifts the free limit of about 30 messages per second to 1,000 for 0.1 Telegram Stars per extra message, and it requires a 100,000 Star balance and 100,000 monthly active users.

What is the Telegram Bot API message limit?

A text message sent with sendMessage holds 1 to 4,096 characters after entities parsing. A caption on a photo, video, or album item holds 0 to 1,024 characters. A rich message sent with sendRichMessage, available since Bot API 10.1, holds up to 32,768 UTF-8 characters and up to 500 blocks. Albums take 2 to 10 items, photo uploads 10 MB, and other files 50 MB.

Can you schedule posts with the Telegram Bot API?

Not through the API itself. No send method has a scheduled-time parameter, so a bot cannot ask Telegram to publish later. You schedule on your side: store the post and its target, run a cron job or queue worker that fires at the right moment, and call sendMessage then. A scheduling API such as PostZen does that for you and reaches other networks in the same request.

What are Telegram rich messages?

Rich messages arrived in Bot API 10.1 on June 11, 2026. The sendRichMessage method takes a rich_message object with Markdown, HTML, or explicit blocks, and supports headings, lists, tables, block quotes, collapsible details, footnotes, media, and math. The ceiling is 32,768 UTF-8 characters, far above the 4,096 of sendMessage, with up to 500 blocks and 20 table columns.