node-telegram-bot-api is a free, open source ai interaction & interfaces project written in TypeScript and released under MIT. It has 9,206 GitHub stars, 1,644 forks and 1 open issues, and was last pushed 10 days ago. On this registry it ranks #39 of 76 tracked projects in AI Interaction & Interfaces, with 5 head-to-head comparisons available.

What is node-telegram-bot-api?

node-telegram-bot-api is an MIT-licensed TypeScript library that implements the Telegram Bot API for Node.js and other JavaScript runtimes, aimed at developers building Telegram bots and chatbots.

What it is

node-telegram-bot-api is a Telegram Bot API client and bot framework published to npm under the package name node-telegram-bot-api. It ships a Bot class for handler registration and a middleware pipeline around incoming updates, alongside an Api client that mirrors the wire API. Version 2 is a from-scratch redesign with no v1 compatibility, and the project tracks Bot API v10.3. It lives in the Node.js and JavaScript runtime ecosystem, is written in TypeScript, and carries the topics api, bot, bot-framework, chatbot, nodejs and telegram. The registry lists 9,206 stars, 1,643 forks and one open issue, with the last push dated 2026-09-07.

The concrete problem it solves is the boilerplate of talking to the Telegram Bot API by hand. Rather than hand-rolling HTTP requests, serializing keyboards, computing message entity offsets and managing the polling loop, a developer constructs new Bot(process.env.BOT_TOKEN!) and works through methods such as api.getMe() and api.sendMessage({ chat_id, text }). The Api class mirrors the wire API one-to-one: one method per Bot API method, each taking a single params object. The same client is reachable as bot.api and as ctx.api inside handlers.

Key capabilities

  • Commands, regex triggers and update types registered as middleware: bot.command("start", ...), bot.hears(/echo (.+)/, ...), bot.on("message", ...) and bot.on("callback_query", ...), where registration order wins.
  • Koa-style middleware chain wrapping every update, using await next() inside bot.use(async (ctx, next) => ...), with bot.catch((err, ctx) => ...) as the last-resort error handler.
  • Two run modes: run(bot) from node-telegram-bot-api/node, a managed runner that wires Ctrl-C to bot.stop(), and the core-only await bot.startPolling().
  • Runtime portability across Bun, modern Node.js, Deno, Cloudflare Workers and Vercel Functions.
  • Keyboard and formatting builders: InlineKeyboardBuilder, ReplyKeyboardBuilder and EntityBuilder, the last computing UTF-16 offsets for text and entities.
  • Structured fields accepted as plain typed objects, for example link_preview_options: { is_disabled: true }, serialized by the same pipeline as builder output.
  • Uploads where a bare string is always treated as a file_id or URL, and raw bytes are wrapped for upload with an explicit filename; the core performs no content sniffing and fromPath uses the basename.

Who uses it and how

  • Node.js developers building chatbots that respond to commands and pattern-matched text, then answer inline button taps through ctx.answerCallbackQuery() after a callback_query arrives.
  • Teams deploying Telegram bots to serverless platforms, since the library targets Cloudflare Workers and Vercel Functions as well as long-running processes.
  • Developers on Bun or Deno who want the same bot code outside the Node.js runtime.
  • Maintainers arriving from v1, who must follow the v1 to v2 migration guide in CHANGELOG.md because v2 drops backward compatibility.

Getting started

Install with npm install node-telegram-bot-api, then construct a bot from a token and start it with either await run(bot) or await bot.startPolling().

How it compares

The provided facts name no comparable or paid products that this project replaces, so it stands alone in this registry entry.

When to use it — and when not to

Choose it for a Telegram-only bot where handler middleware, keyboard builders and broad runtime support matter, and where the operator is content to supply a bot token and host the process or deploy it to a supported serverless runtime. Do not pick v2 if the codebase depends on v1 APIs without a migration pass, since v2 is a from-scratch redesign with no v1 compatibility. Note that the library is a client only: the facts describe no hosted service, and the README excerpt in this listing is cut off partway through the uploads documentation, so the uploads rules should be confirmed against the source README.

project readme (upstream, from github) — read inline

✨ A Modern Telegram Bot API Library ✨

Bot API npm package

https://telegram.me/node_telegram_bot_api https://t.me/+_IC8j_b1wSFlZTVk https://telegram.me/Yago_Perez

v2 is a from-scratch redesign, no v1 compatibility. Coming from v1? See the v1 -> v2 migration guide in the changelog.

📦 Install

npm install node-telegram-bot-api

**Runs on Bun, modern Node.js, Deno, Cloudflare Workers and Vercel Functions λ

🚀 Usage

import { Bot, InlineKeyboardBuilder } from "node-telegram-bot-api";
import { run } from "node-telegram-bot-api/node"; // managed runner: wires Ctrl-C to bot.stop()

const bot = new Bot(process.env.BOT_TOKEN!);

// commands, regex and update types are all middleware - registration order wins
bot.command("start", (ctx) => ctx.reply("Hi! Send me anything."));
bot.hears(/echo (.+)/, (ctx) => ctx.reply(ctx.match![1]!));

bot.on("message", (ctx) =>
  ctx.reply("Pick one:", {
    reply_markup: new InlineKeyboardBuilder()
      .text("👍", "up")
      .text("👎", "down")
      .build(),
  }),
);

// 🔘 a tapped inline button comes back as a callback_query
bot.on("callback_query", async (ctx) => {
  await ctx.answerCallbackQuery({ text: `You tapped ${ctx.callbackQuery!.data}` });
});

await run(bot); // core-only alternative that runs anywhere: await bot.startPolling()

📡 Calling the API directly

Api mirrors the wire API 1:1 - one method per Bot API method, each taking a single params object.

import { Api } from "node-telegram-bot-api";

const api = new Api(process.env.BOT_TOKEN!);
const me = await api.getMe();
await api.sendMessage({ chat_id: 12345, text: "hello" });
// the same client is also on bot.api and ctx.api

🧩 Middleware

koa-style middleware around every update; on/command/hears are filters in the same chain. Wrap downstream work with await next().

// ⏱️ time every update - and catch anything thrown downstream
bot.use(async (ctx, next) => {
  const start = Date.now();
  try {
    await next();
  } finally {
    console.log(`update took ${Date.now() - start}ms`);
  }
});

// 🧯 last-resort error handler
bot.catch((err, ctx) => console.error("handler failed", err));

⌨️ Keyboards & formatting

Structured fields are plain typed objects - pass a literal or use a fluent builder; the pipeline serializes either.

import { Bot, InlineKeyboardBuilder, ReplyKeyboardBuilder, EntityBuilder } from "node-telegram-bot-api";

const bot = new Bot(process.env.BOT_TOKEN!);

// 🎛️ inline keyboard as reply_markup
await bot.api.sendMessage({
  chat_id,
  text: "Choose:",
  reply_markup: new InlineKeyboardBuilder()
    .text("A", "a")
    .url("Docs", "https://core.telegram.org/bots/api")
    .row()
    .text("B", "b")
    .build(),
});

// ⌨️ reply keyboard as reply_markup
await bot.api.sendMessage({
  chat_id,
  text: "Yes or no?",
  reply_markup: new ReplyKeyboardBuilder()
    .text("Yes")
    .text("No")
    .build({ resize_keyboard: true }),
});

// ✍️ rich text - EntityBuilder computes UTF-16 offsets for you
const { text, entities } = new EntityBuilder()
  .plain("Hello ")
  .bold("world")
  .link("docs", "https://github.com/yagop/node-telegram-bot-api")
  .build();
await bot.api.sendMessage({ chat_id, text, entities });

// any structured field is just a plain object - no wrapper needed
await bot.api.sendMessage({ chat_id, text: "hi", link_preview_options: { is_disabled: true } });

📤 Uploads

A bare string is always a file_id or URL. Wrap raw bytes to upload them. Pass a filename with the right extension - the core does no content sniffing, so the name is what Telegram sees (fromPath uses the basename).

Uploads stream: bytes flow from their source straight into the request, so memory stays flat no matter the file size (fromPath re-opens a disk stream per attempt). A Blob or Uint8Array upload is re-streamed if the transport retries; a ReadableStream is one-shot - it is sent once and a failure surfaces immediately instead of retrying. To keep retries for an arbitrary stream source, pass a factory that returns a fresh stream:

// replayable streaming upload: the factory opens a new stream per attempt
await bot.api.sendVideo({
  chat_id,
  video: new InputFile(() => openVideoStream(), { filename: "video.mp4", contentType: "video/mp4" }),
});
import { Bot, InputFile, MediaGroupBuilder } from "node-telegram-bot-api";
import { fromPath } from "node-telegram-bot-api/node";

const bot = new Bot(process.env.BOT_TOKEN!);

// upload from disk (Node only)
await bot.api.sendPhoto({ chat_id, photo: await fromPath("./cat.jpg") });
// upload raw bytes (web-standard, runs anywhere)
await bot.api.sendDocument({ chat_id, document: new InputFile(bytes, { filename: "report.pdf" }) });

// a raw InputFile nested in a structure is auto-hoisted to an attach:// part
await bot.api.sendMediaGroup({
  chat_id,
  media: [
    { type: "photo", media: new InputFile(bytesA, { filename: "a.jpg" }), caption: "A" },
    { type: "photo", media: "https://telegram.org/example/photo.jpg" },
  ],
});

// MediaGroupBuilder: optional sugar for the same array
await bot.api.sendMediaGroup({
  chat_id,
  media: new MediaGroupBuilder()
    .photo({ media: new InputFile(bytesA, { filename: "a.jpg" }), caption: "A" })
    .photo({ media: "https://telegram.org/example/photo.jpg" })
    .build(),
});

Builders cover the other attach:// methods; each .build() returns the plain shape.

import {
  Bot,
  InputFile,
  StickerSetBuilder,
  StaticProfilePhotoBuilder,
  PhotoStoryBuilder,
} from "node-telegram-bot-api";

const bot = new Bot(process.env.BOT_TOKEN!);

// collect a sticker set
await bot.api.createNewStickerSet({
  user_id,
  name,
  title,
  stickers: new StickerSetBuilder()
    .add({ sticker: new InputFile(pngBytes, { filename: "sticker.png" }), format: "static", emoji_list: ["🙂"] })
    .build(),
});

// a single sticker is a plain InputSticker - no builder needed
await bot.api.addStickerToSet({
  user_id,
  name,
  sticker: { sticker: new InputFile(pngBytes, { filename: "sticker.png" }), format: "static", emoji_list: ["🙂"] },
});

// profile photo: Static / AnimatedProfilePhotoBuilder
await bot.api.setMyProfilePhoto({
  photo: new StaticProfilePhotoBuilder({ photo: new InputFile(pngBytes, { filename: "avatar.png" }) }).build(),
});

// story: Photo / VideoStoryBuilder
await bot.api.postStory({
  business_connection_id,
  active_period,
  content: new PhotoStoryBuilder({ photo: new InputFile(pngBytes, { filename: "story.png" }) }).build(),
});

🪝 Webhooks

The web-standard callback is a pure (Request) => Promise - one function for every serverless runtime.

Cloudflare Workers / Bun.serve / Deno Deploy / Vercel Edge:

import { Bot, webhookCallback } from "node-telegram-bot-api";

const bot = new Bot(TOKEN);
bot.on("message", (ctx) => ctx.reply("hi from the edge"));

export default {
  fetch: webhookCallback(bot, { secretToken: SECRET }),
};

By default the callback awaits your handler before 200. For slow handlers, opt into early-ACK:

export default {
  // ✅ return 200 immediately, then finish the handler in the background
  // waitUntil keeps the platform alive until it settles (fastAck: true = fire-and-forget)
  fetch: (req: Request, _env: unknown, ctx: { waitUntil(promise: Promise<unknown>): void }) =>
    webhookCallback(bot, { secretToken: SECRET, waitUntil: (p) => ctx.waitUntil(p) })(req),
};

Next.js App Router (app/api/bot/route.ts):

import { Bot, nextAppWebhook } from "node-telegram-bot-api";
const bot = new Bot(process.env.BOT_TOKEN!);
export const POST = nextAppWebhook(bot, { secretToken: process.env.SECRET });

Express (mount on an app you already have):

import express from "express";
import { Bot, registerExpressWebhook } from "node-telegram-bot-api";

const app = express();
const bot = new Bot(TOKEN);
registerExpressWebhook(bot, app, { path: "/telegram", secretToken: SECRET });
app.listen(3000);

Self-hosted Node server (node-telegram-bot-api/node):

import { Bot } from "node-telegram-bot-api";
import { createWebhookServer, startWebhook } from "node-telegram-bot-api/node";

// Low-level: you own the server and the port.
const server = 

readme truncated — read the full docs on github

Frequently asked questions

Is node-telegram-bot-api free to use?

node-telegram-bot-api is open source under the MIT licence. There is no licence fee and no seat count — you can self-host it or, where the project offers one, pay a vendor for a managed version instead.

What does node-telegram-bot-api do?

Telegram Bot API for NodeJS

What is node-telegram-bot-api written in?

node-telegram-bot-api is primarily written in TypeScript. Its source is publicly available at https://github.com/yagop/node-telegram-bot-api, and it has 9,206 GitHub stars.