npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

xzcgram

v0.0.7

Published

A Telegraf-style command/hears router built on top of GramJS (telegram)

Readme

npm version License Downloads

Donation site

xzcgram

A tiny Telegraf-style router for GramJS (telegram).

Keeps GramJS's raw power (MTProto client, full TL API) but gives you a familiar, minimal API on top of it:

bot.command("start", async (ctx) => {
  await ctx.reply("Hello!");
});

instead of manually wiring client.addEventHandler + NewMessage + parsing the command yourself.

Install

npm install xzcgram

That's it — telegram (GramJS) ships as a direct dependency of this package, so you don't need to install or import it separately.

Quick start

const { clientStart } = require("xzcgram");

(async () => {
  const { bot, sessionString } = await clientStart({
    apiId: 123456,
    apiHash: "your_api_hash",
    session: process.env.SESSION || "", // saved session string, skips login
    loginOptions: {
      phoneNumber: async () => "+1234567890",
      password: async () => "your2FApassword", // only if 2FA is enabled
      phoneCode: async () => "12345",
      onError: (err) => console.error(err),
    },
  });

  console.log("Save this session string for next time:", sessionString);

  bot.command("start", async (ctx) => {
    await ctx.reply("Hello!");
  });

  bot.hears(/hi|hello/i, async (ctx) => {
    await ctx.reply("Hey there 👋");
  });

  bot.on("message", async (ctx) => {
    console.log("Unhandled message:", ctx.text);
  });

  await bot.launch();
})();

clientStart handles the GramJS client creation and login internally — no (including interactive login prompts).

If you already manage your own TelegramClient elsewhere, you can still use Bot directly instead of clientStart — see "Advanced usage" below.

API

clientStart(options)

The recommended entry point. Creates a TelegramClient, logs it in, and returns a ready-to-use Bot.

| Option | Required | Description | | ------------------- | -------- | ---------------------------------------------------------- | | apiId | yes | Your api_id from my.telegram.org | | apiHash | yes | Your api_hash from my.telegram.org | | botToken | no | A bot token from @BotFather — logs in as that bot instead of a user account (skips loginOptions entirely) | | sessionType | no | "string" (default) or "store" — see below | | session | no | Saved session string, used when sessionType is "string" | | sessionName | no | Session file name, used when sessionType is "store" (default "xzcgram") | | clientOptions | no | Extra options merged into the underlying TelegramClient | | loginOptions | no | Callbacks forwarded to client.start() (phoneNumber, password, phoneCode, onError) |

Resolves to { bot, client, sessionString }.

Session strategies:

  • sessionType: "string" (default) — session is a portable string you save yourself (env var, database, etc.) and pass back in as session next run. sessionString in the return value holds it.

    const { bot, sessionString } = await clientStart({
      apiId, apiHash,
      sessionType: "string",
      session: process.env.SESSION || "",
      loginOptions: { /* ... */ },
    });
    console.log("Save this:", sessionString);
  • sessionType: "store" — session is written to a local file automatically (GramJS's StoreSession), so there's no string to copy around. Good for local scripts/servers where the file stays on disk.

    const { bot } = await clientStart({
      apiId, apiHash,
      sessionType: "store",
      sessionName: "sessions", // creates my-bot-sessions.session
      loginOptions: { /* ... */ },
    });

    sessionString will be null in this mode since the session already lives on disk.

Logging in as a bot instead of a user:

Pass botToken (from @BotFather) and skip loginOptions entirely — no phone number, code, or password needed:

const { bot } = await clientStart({
  apiId,
  apiHash,
  botToken: "123456:ABC-your-bot-token-from-BotFather",
  sessionType: "store",
  sessionName: "my-bot",
});

bot.command("start", async (ctx) => {
  await ctx.reply("Hello! I'm running as a bot account.");
});

await bot.launch();

Everything else (bot.command, bot.hears, bot.action, ctx.reply*, etc.) works the same either way. A couple of things differ because Telegram treats bot accounts differently from user accounts:

  • Bots can only see messages sent directly to them or in chats/groups they've been added to — they can't read arbitrary chats like a user session can.
  • Some ctx.* methods that act on "your own account" (updateProfile, updateUsername, setOnlineStatus, getPrivacySettings, blockUser, etc.) either behave differently or aren't meaningful for bots — those are really aimed at user-account sessions.
  • Admin-type actions (banUser, promoteUser, etc.) still work, but the bot itself needs the relevant admin rights in that chat first.

new Bot(client)

Lower-level constructor for when you already manage your own GramJS TelegramClient (e.g. it's shared with other code). Doesn't connect or log in for you — do that first with client.start(...).

bot.command(name, handler)

Registers a handler for a command. name can be with or without the leading slash ("start" or "/start" both work). Matches @botname suffixes too (/start@mybot).

bot.start(handler) / bot.help(handler)

Shortcuts for bot.command("start", handler) and bot.command("help", handler) — the two commands almost every bot has, so they get their own one-liner:

bot.start(async (ctx) => {
  // ctx.args[0] holds the deep-link payload, e.g. "/start ref_123" -> "ref_123"
  await ctx.reply(ctx.args[0] ? `Welcome! Ref: ${ctx.args[0]}` : "Welcome!");
});

bot.help(async (ctx) => {
  await ctx.reply("Commands: /start, /help");
});

bot.hears(pattern, handler)

Registers a handler that fires when the message text matches pattern. pattern can be a plain string (substring match) or a RegExp.

bot.onText(pattern, handler)

Like bot.command(), but without the / requirement — and like bot.hears(), but stricter and with regex capture groups handed to your handler. Useful when you want command-like, single-purpose triggers users can type as plain words ("help", "menu", "harga 50rb") instead of slash commands.

  • A string pattern must match the message exactly — onText("help") fires on "help", not on "/help" or "help me".
  • A RegExp pattern is run against the full text with .exec(), and the resulting match (with any capture groups) is passed as the handler's second argument.
bot.onText("help", async (ctx) => {
  await ctx.reply("How can I help?");
});

bot.onText(/^harga (\d+)rb$/i, async (ctx, match) => {
  await ctx.reply(`Itu Rp${Number(match[1]) * 1000}`);
});

Checked right after bot.command and before bot.hears, so a /xxx message that doesn't match a registered command still falls through to onText/hears/on("message") as usual.

bot.on("message", handler)

Fallback handler, fires for any message that didn't match a command or a hears pattern.

bot.editedMessage(handler)

Fires whenever a message (yours or someone else's, depending on the chat) gets edited. handler receives a normal Context — the same one passed to command/hears/on, but reflecting the message's edited state.

bot.editedMessage(async (ctx) => {
  console.log(`Message ${ctx.message.id} edited to:`, ctx.text);
});

bot.deletedMessage(handler)

Fires whenever one or more messages are deleted. Telegram only reports where a deletion happened for channels/supergroups, so ctx.channelId is null for private chats and basic groups.

bot.deletedMessage(async (ctx) => {
  console.log("Deleted message ids:", ctx.deletedIds, "in channel:", ctx.channelId);
});

DeletedMessageContext:

| Property | Description | | ---------------- | ---------------------------------------------------------------- | | ctx.deletedIds | Array of message ids that were deleted | | ctx.channelId | Channel id the deletion happened in, if known (channels/supergroups only) |

bot.album(handler)

Fires once per album — a set of media messages (e.g. several photos) sent together in one go — with every message in the group collected onto ctx.messages.

bot.album(async (ctx) => {
  console.log(`Album of ${ctx.messages.length} items in chat ${ctx.chatId}`);
  await ctx.reply(`Got your album (${ctx.messages.length} items)!`);
});

AlbumContext:

| Property / method | Description | | ------------------- | ------------------------------------------------------- | | ctx.messages | The individual messages that make up this album | | ctx.chatId | Chat the album was sent in | | ctx.groupedId | Shared grouped-media id linking all messages in this album | | ctx.reply(text, opts?) | Send a new message in the same chat as the album |

Handlers for editedMessage, deletedMessage, and album depend on GramJS exposing the corresponding event class (EditedMessage, DeletedMessage, Album). If your installed telegram version doesn't, bot.launch() logs a warning and the handler simply never fires — everything else keeps working.

bot.launch()

Attaches the internal NewMessage event listener. Call once, after registering your handlers.

Context (passed to command/hears/on handlers)

| Property / method | Description | | ------------------------------------------------ | ------------------------------------------------------------ | | ctx.client | The underlying TelegramClient | | ctx.event / ctx.message | Raw GramJS event / message object | | ctx.chatId | Chat/peer id of the message | | ctx.text | Full message text | | ctx.args | Command arguments as an array (space-split) | | Metadata | | | ctx.senderId | Id of whoever sent the message | | ctx.date / ctx.editDate | Unix timestamp sent / last edited | | ctx.viaBotId | Id of the inline bot used to send this, if any | | ctx.viaBusinessBotId | Id of the Business-connected bot used to send this, if any | | ctx.isPrivate / ctx.isGroup / ctx.isChannel| What kind of chat this is | | ctx.replyToMsgId / ctx.isReply | Id of the message being replied to, if any | | ctx.replyMessage | Promise resolving to the replied-to message(s) ([] if not a reply) — await it | | ctx.fwdFrom / ctx.isForwarded | Forward header info, if the message was forwarded | | ctx.entities | Message entities (bold, links, mentions, etc.) | | ctx.media / ctx.hasMedia / ctx.mediaType | Raw attached media, whether it exists, and its type | | ctx.silent / ctx.out / ctx.pinned | Sent silently / sent by us / currently pinned | | ctx.groupedId | Album id, if this message is part of a media group | | ctx.peer() | Resolves the correct input peer for the current chat (used internally, but callable directly) | | Text | | | ctx.reply(text, opts?) | Send a message to the same chat | | ctx.replyQuote(text, opts?) | Reply directly to the triggering message | | ctx.editMessage(messageId, text, opts?) | Edit a message you previously sent | | Media | | | ctx.replyWithPhoto(file, opts?) | Send a photo | | ctx.replyWithVideo(file, opts?) | Send a video (streamable) | | ctx.replyWithVideoNote(file, opts?) | Send a round video note | | ctx.replyWithAudio(file, opts?) | Send an audio track | | ctx.replyWithVoice(file, opts?) | Send a voice note | | ctx.replyWithDocument(file, opts?) | Send any file as a document | | ctx.replyWithSticker(file, opts?) | Send a sticker | | ctx.replyWithAnimation(file, opts?) | Send a GIF | | ctx.replyWithMediaGroup(files, opts?) | Send an album (array of files) | | Interactive | | | ctx.replyWithButtons(text, rows, opts?) | Send a message with inline buttons | | ctx.replyWithKeyboard(text, rows, opts?) | Send a message with a plain reply keyboard | | ctx.replyWithTable(headers, rows, opts?) | Send a plain-text table, monospaced via <pre> (e.g. ctx.replyWithTable(["Name","Score"], [["Alice",42],["Bob",7]])) | | ctx.replyWithPoll(question, answers, opts?) | Send a poll or quiz (opts: multipleChoice, quiz, correctAnswers, solution, publicVoters, closePeriod, closeDate, closed) | | ctx.replyWithLocation(lat, long, opts?) | Send a location | | ctx.replyWithContact(phone, firstName, last?) | Send a contact card | | ctx.replyWithDice(emoji?, opts?) | Send an animated dice/emoji (default 🎰; also 🎲🎯🏀⚽🎳) | | Chat management | | | ctx.deleteMessage(id) | Delete a message by id (revokes for all) — id required | | ctx.forwardMessage(toChatId, id) | Forward a message by id elsewhere — id required | | ctx.pinMessage(id, opts?) | Pin a message by id — id required | | ctx.unpinMessage(id) | Unpin a message by id — id required | | ctx.sendChatAction(action?) | Show "typing…" / "uploading photo…" etc | | ctx.getSender() | Resolve the full sender entity | | ctx.getChat() | Resolve the full chat entity | | ctx.replyViaBot(bot, query?, opts?) | Send a message "via @bot" using an inline query result | | ctx.queryInlineBot(bot, query?, opts?) | Fetch a bot's inline query results without sending one — inspect/pick as a user, see below | | More media / interaction | | | ctx.replyWithVenue(lat, long, title, address, opts?) | Send a venue (pinned location + name/address) | | ctx.replyWithLiveLocation(lat, long, period?, opts?) | Send a live-updating location (period in seconds, 60–86400) | | ctx.react(id, emoji?, opts?) | React to a message — id required; empty/null emoji removes it | | ctx.copyMessage(toChatId, opts?) | Re-send the triggering message's content elsewhere, no "Forwarded from" header | | ctx.markAsRead(peer?) | Mark a chat as read (defaults to the current chat) | | ctx.downloadMedia(opts?) | Download the triggering message's attached media (Buffer, or see saveMediaMessage) | | ctx.editMessageMedia(messageId, file, opts?) | Swap the media on a message you previously sent | | Voice / text / poll | | | ctx.transcribeVoice(id) | Transcribe a voice note by message id — id required | | ctx.translateText(text, toLang) | Translate arbitrary text (pass the text explicitly, e.g. ctx.text) | | ctx.votePoll(id, options) | Vote on a poll message — id required | | ctx.retractVote(id) | Retract your vote on that poll — id required | | Contacts / history | | | ctx.blockUser(userId) / ctx.unblockUser(userId) | Block/unblock a user — userId required | | ctx.getHistory(peer?, limit?, opts?) | Fetch recent messages (defaults to the current chat) | | ctx.searchMessages(query, opts?) | Search messages in the current chat | | ctx.unpinAllMessages(peer?) | Unpin every pinned message (defaults to the current chat) | | ctx.getParticipants(opts?) | List members of the current group/channel | | ctx.getPinnedMessages(opts?) | List every pinned message in the current chat | | ctx.getEntityByUsername(username) | Get metadata for a user/chat/channel by @username | | Channel/group management (supergroup/channel unless noted) | | | ctx.banUser(userId, opts?) | Ban a user (can't rejoin unless unbanned) | | ctx.unbanUser(userId) | Clear all restrictions on a user | | ctx.kickUser(userId) | Remove a user (they CAN rejoin) — supergroup/channel or basic group | | ctx.promoteUser(userId, rank?, rights?) | Promote a user to admin (fairly permissive defaults; override via rights) | | ctx.demoteUser(userId) | Strip a user of all admin rights | | ctx.inviteToChannel(channelId, userIds) | Invite one or more users to a channel/supergroup | | ctx.kickUser(userId) | Remove a user, but they CAN rejoin (works in basic groups too) | | ctx.promoteUser(userId, rights?, rank?) | Make a user admin | | ctx.demoteUser(userId) | Strip a user's admin rights | | ctx.inviteToChannel(userIds) | Invite one or more users to the current channel/supergroup | | ctx.setChatTitle(id, title) | Rename a chat — pass null/undefined as id to target the current chat (see note below) | | ctx.setChatPhoto(id, file) | Change a chat's photo — same id rule as setChatTitle | | ctx.deleteChatPhoto(id) | Remove a chat's photo — same id rule as setChatTitle | | ctx.joinChannel(id) | Join a channel/group by @username, id, or entity | | ctx.joinByInvite(hash) | Join a private chat via invite hash (the part after t.me/+) | | ctx.leaveChannel(id?) | Leave a channel/group (defaults to the current chat) | | Stickers & GIFs | | | ctx.searchStickers(emoji) | Find stickers matching an emoji | | ctx.getStickerSet(shortName) | Get a sticker set's full info + documents | | ctx.searchGifs(query, offset?) | Search Telegram's global GIF results | | ctx.saveGif(document, unsave?) | Save/unsave a GIF to "Saved GIFs" | | Privacy & account settings | | | ctx.getPrivacySettings(key?) | Get your privacy rules for a key (phoneNumber, lastSeen, etc.) | | ctx.updateProfile(opts?) | Update your first/last name and bio | | ctx.updateUsername(username) | Change your @username | | ctx.setOnlineStatus(online?) | Manually set your online/offline status | | Advanced file uploads | | | ctx.replyWithSpoiler(file, opts?) | Send a photo/video with the spoiler blur overlay | | ctx.replyWithThumb(file, thumb, opts?) | Send a file with a custom thumbnail | | ctx.uploadWithProgress(file, onProgress) | Upload a file with a progress callback, without sending it yet | | ctx.saveMediaMessage(messageId, opts?) | Download media + return { buffer, metadata, filename } — messageId required | | ctx.getMe() | Get the full User entity for the logged-in account | | ctx.setEmojiStatus(documentId?, untilDate?) | Set (or clear) your profile's emoji status | | ctx.getDialogs(opts?) | List your open chats, most recent first | | ctx.archiveChat(id?) / ctx.unarchiveChat(id?) | Move a chat in/out of "Archived Chats" (defaults to the current chat) | | ctx.getContacts() | Get your saved contact list | | ctx.addContact(phone, firstName, lastName?) | Add/update a contact by phone number | | ctx.importContacts(contacts) | Bulk-import [{ phone, firstName, lastName? }] | | ctx.deleteContact(userId) | Remove one or more users from your contact list | | ctx.getCommonChats(userId) | List chats you have in common with a user | | ctx.exportChatInvite(id?, opts?) | Create a new invite link (defaults to the current chat) | | ctx.getExportedInvites(id?, opts?) | List invite links you've created for a chat | | ctx.replySchedule(text, when, opts?) | Schedule a message for later (when: Date or unix seconds) | | ctx.getScheduledMessages() | List messages scheduled but not yet sent in the current chat | | ctx.deleteScheduledMessages(ids) | Cancel one or more scheduled messages | | ctx.reportSpam(id?) | Report a chat as spam to Telegram | | ctx.setMessagesTTL(periodSeconds, id?) | Set/clear the auto-delete timer for new messages in a chat | | ctx.getReadParticipants(id) | See who has read a message (small groups only) — id required | | ctx.getMessageById(messageId) | Fetch a specific message from the current chat — messageId required | | ctx.forwardMessages(toChatId, ids) | Forward one or more specific messages — ids required | | ctx.editMessageReplyMarkup(messageId, rows?) | Replace/clear a message's inline buttons — messageId required | | ctx.getMessageLink(messageId, id?) | Get a shareable link to a message — messageId required | | ctx.getUserPhotos(userId, opts?) | Get a user's profile photos — userId required | | ctx.getUserStatus(userId) | Get a user's last-seen/online status — userId required | | ctx.muteChat(id?, untilDate?) / ctx.unmuteChat(id?) | Mute/unmute notifications for a chat (defaults to the current chat) | | ctx.getAdmins(id?) | List administrators of a supergroup/channel (defaults to the current chat) | | ctx.setSlowMode(seconds, id?) | Set the slow-mode delay on a supergroup (defaults to the current chat) | | ctx.replyWithInvoice(opts) | Send an invoice message (Telegram Payments / Stars) — see below | | ctx.getPaymentForm(messageId) | Get the payment form for an invoice message — messageId required | | ctx.getPaymentReceipt(messageId) | Get the receipt for a paid invoice message — messageId required | | ctx.exportInvoice(invoiceMedia) | Export a shareable link for an invoice — invoiceMedia required | | ctx.getSavedPaymentInfo() | Get your saved shipping/payment info | | ctx.clearSavedPaymentInfo(opts?) | Clear saved shipping info and/or card credentials | | ctx.getStarsStatus(id?) | Get your (or a bot's) Telegram Stars balance | | ctx.getStarsTransactions(opts?) | Get your (or a bot's) Telegram Stars transaction history | | Bot command menu (meaningful only when logged in as a bot) | | | ctx.setBotCommands(commands, opts?) | Set the command list shown in Telegram's "/" menu (commands: [{command, description}]) | | ctx.getBotCommands(opts?) | Get the currently configured bot command list | | ctx.setBotMenuButton(opts?) | Set the menu button next to the message box (plain "Commands", or {text, url}) | | Blocklist & invites | | | ctx.getBlockedUsers(opts?) | List users you've blocked | | ctx.getChatInviteImporters(id?, opts?) | See who joined a chat via invite links (defaults to the current chat) | | Rich Messages (Bot API 10.1+, bot accounts only) | | | ctx.sendRichMessage(chatId, markdown, opts?) | Send a structured Rich Message (tables, headings, dividers, blockquotes, etc.) from GitHub-flavored markdown — see below |

Heads up — a couple of gotchas in the current signatures:

  • setChatTitle/setChatPhoto take the target chat first: to target the current chat you must call ctx.setChatTitle(null, "New Title"), not ctx.setChatTitle("New Title") (that would treat the title as the id).
  • votePoll(id, options) — id is required, e.g. ctx.votePoll(ctx.message.id, [0]) to vote on the triggering message's poll, or any other message id.

replyViaBot runs an inline query against a bot that supports inline mode (the same thing as typing @botname query... in the message box) and sends one of its results — this is the only way to get the "via @bot" label on a message, it can't be set manually on a normal reply/sendMessage call:

// same as typing "@pic cats" and sending the first result
await ctx.replyViaBot("pic", "cats");

// pick a specific result, or hide the "via @bot" label
await ctx.replyViaBot("pic", "cats", { resultIndex: 2, hideVia: true });

If you want to look at what a bot's inline mode returns before sending anything — or let the user pick — use queryInlineBot instead. It's the same underlying request as replyViaBot, minus the auto-send: this is how a user account can respond to/act on an inline query's results (a bot account gets the query itself via bot.inlineQuery(); a user account can only be the one asking a bot for results, never the one receiving the query):

const results = await ctx.queryInlineBot("pic", "cats");
console.log(results.map((r) => r.title));

// send whichever one you picked, whenever you're ready
await results[2].click({ entity: await ctx.peer() });

Buttons (replyWithButtons / replyWithKeyboard) take rows of button descriptors:

await ctx.replyWithButtons("Pick one:", [
  [{ text: "Yes", data: "confirm_yes" }, { text: "No", data: "confirm_no" }],
  [{ text: "Telegram", url: "https://t.me/XazepysK" }],
]);

bot.action(pattern, handler) (alias: bot.callbackQuery(pattern, handler))

Handles inline button presses (callback queries). pattern is matched against the button's decoded data string — a plain string (exact match) or a RegExp.

callbackQuery is the exact same handler under the raw GramJS/MTProto event name (CallbackQuery) instead of the Telegraf-style action — pick whichever reads better, both register onto the same list and get the same CallbackContext:

bot.callbackQuery(/^confirm_/, async (ctx) => {
  await ctx.answer("Got it!");
});
bot.action(/^confirm_/, async (ctx) => {
  await ctx.answer("Got it!");           // toast shown to the user
  await ctx.editMessageText(`You picked: ${ctx.data}`);
});

CallbackContext passed to action handlers:

| Property / method | Description | | -------------------------------- | ------------------------------------------------ | | ctx.data | Decoded callback data (string) | | ctx.dataRaw | Raw callback data (Buffer) | | ctx.chatId | Chat where the button was pressed | | ctx.senderId | Id of the user who pressed the button | | ctx.messageId | Id of the message the button is attached to | | ctx.chatInstance | Opaque chat instance id (useful for game callbacks) | | ctx.queryId | Id of the callback query itself | | ctx.answer(text?, opts?) | Answer the callback query (toast/alert) | | ctx.editMessageText(text, opts?) | Edit the message the button is attached to | | ctx.reply(text, opts?) | Send a new message in that chat |

Anything not covered here is still reachable through ctx.client — GramJS's full API (raw TL requests, client.invoke(new Api...), etc.) is always available.

bot.inlineQuery(pattern?, handler)

Handles inline queries — fires when someone types @yourbot query... in any chat. Only works when logged in as a bot with inline mode enabled (/setinline in @BotFather). pattern is matched against the query text (string substring match or RegExp); omit it to match every inline query.

bot.inlineQuery(/cat/i, async (ctx) => {
  await ctx.answerArticles([
    { id: "1", title: "Cat fact", text: `You searched: ${ctx.query}` },
  ]);
});

// match everything
bot.inlineQuery(async (ctx) => {
  await ctx.answerArticles([{ id: "1", title: "Hi", text: "Hello!" }]);
});

InlineQueryContext passed to inlineQuery handlers:

| Property / method | Description | | -------------------------------------- | ---------------------------------------------------- | | ctx.query | The text typed after your bot's @username | | ctx.senderId | Id of the user typing the query | | ctx.offset | Pagination offset, if the user scrolled for more | | ctx.geo | Geo point, if shared and your bot requested it | | ctx.answerArticles(items, opts?) | Shortcut: answer with simple text "article" results | | ctx.answer(results, opts?) | Answer with raw Api.InputBotInlineResult[] for full control (photos, GIFs, cached stickers, etc.) |

answerArticles item shape: { id, title, description?, text?, url?, thumbUrl?, buttons? }. buttons takes the same row format as ctx.replyWithButtons — an inline button attached to the message gets sent when someone picks that result:

bot.inlineQuery(/confirm/i, async (ctx) => {
  await ctx.answerArticles([
    {
      id: "1",
      title: "Confirm?",
      text: "Tap a button below.",
      buttons: [[{ text: "Yes", data: "yes" }, { text: "No", data: "no" }]],
    },
  ]);
});

// the resulting message's buttons still go through bot.action() as usual
bot.action(/yes|no/, async (ctx) => {
  await ctx.answer(`You picked: ${ctx.data}`);
});

For anything beyond plain text articles (photos, videos, cached stickers/GIFs), build Api.InputBotInlineResult objects yourself and pass them to ctx.answer() — see GramJS's messages.SetInlineBotResults docs for the full result shape.

Method Reference

One-by-one reference for every ctx.* method, in the same order and grouping as src/context.js. The quick-scan table above covers the same ground in a single line each; this section spells out parameters, return shapes, and gotchas individually. bot.*, CallbackContext, and InlineQueryContext are already documented inline under their own headings earlier in this file.

General

ctx.peer()

Resolves the correct peer to send to/act on. Uses message.getInputChat() first, since it carries the access_hash from the incoming update — this avoids "Could not find the input entity" errors that happen when using a raw numeric chatId for a user/chat the client hasn't cached yet (e.g. first DM from someone new). Falls back to the raw chatId if getInputChat() can't resolve anything.

Text

ctx.reply(text, opts = {})

Reply in the same chat the message came from.

ctx.sendMessage(to, content, opts = {})

Send content — text, media, or a poll — to ANY chat/user/channel, not limited to the current chat. Use the specific ctx.replyWith* methods instead if you just want to send in the current chat. The content key itself says what's being sent — no type field needed:

| Param | Description | | --- | --- | | to string|number|object | target entity: @username, numeric id, phone number (if in contacts), or an already-resolved peer/entity | | content string|object | a plain string → sent as text - { text: "..." } - { photo: file \| { url\|buffer\|file\|path, caption? } } (same shape for video, audio, voice, document, sticker, animation) - { poll: { question, answers, ...pollOpts } } (same opts as replyWithPoll) | | [opts] object | — |

await ctx.sendMessage(chatId, { text: "XazepysK" });
await ctx.sendMessage(chatId, { photo: { url: "https://...", caption: "nice" } });
await ctx.sendMessage(chatId, { photo: someBuffer });
await ctx.sendMessage(chatId, { poll: { question: "Pizza?", answers: ["Yes", "No"] } });

ctx.replyQuote(text, opts = {})

Reply directly to the triggering message (quote-style reply).

ctx.editMessage(messageId, text, opts = {})

Edit a message previously sent in this chat (must be your own message).

Media

ctx.replyWithPhoto(file, opts = {})

Send a photo. file can be a path, Buffer, URL, or existing file id.

ctx.replyWithVideo(file, opts = {})

Send a video. Pass opts.supportsStreaming = true for streamable playback.

ctx.replyWithVideoNote(file, opts = {})

Send a round "video note" (the circular video bubble).

ctx.replyWithAudio(file, opts = {})

Send an audio file (music, shown with player + duration/title).

ctx.replyWithVoice(file, opts = {})

Send a voice note (the waveform bubble).

ctx.replyWithDocument(file, opts = {})

Send any file as a generic document.

ctx.replyWithSticker(file, opts = {})

Send a sticker (.webp/.tgs file or existing file reference).

ctx.replyWithAnimation(file, opts = {})

Send an animated GIF.

ctx.replyWithMediaGroup(files, opts = {})

Send multiple files as an album/media group. files is an array.

Interactive / structured content

ctx.replyWithButtons(text, rows, opts = {})

Send a message with inline buttons.

| Param | Description | | --- | --- | | text string | — | | {Array<Array<{text: string, data?: string, url?: string}>>} rows | | | | [opts] object | — |

ctx.replyWithKeyboard(text, rows, opts = {})

Send a plain (non-inline) reply keyboard.

| Param | Description | | --- | --- | | text string | — | | {Array<Array<{text: string}>>} rows | | | | [opts] object | — |

ctx.replyWithTable(headers, rows, opts = {})

Send a simple table as a monospace block. Telegram has no native table type — this pads each column to equal width and wraps everything in an HTML <pre> block so it renders in a fixed-width font.

| Param | Description | | --- | --- | | headers string[] | — | | rows Array<Array<string|number>> | — | | [opts] object | — |

await ctx.replyWithTable(
  ["Name", "Score"],
  [["Alice", 42], ["Bob", 7]]
);

ctx.replyWithPoll(question, answers, opts = {})

Send a poll, or a quiz (pass opts.quiz: true).

| Param | Description | | --- | --- | | question string | — | | answers string[] | — | | [opts] object | — | | {boolean} [opts.multipleChoice=false] - not allowed together with quiz | | | | {boolean} [opts.publicVoters=true] - ignored for quiz polls (always public) | | | | {boolean} [opts.quiz=false] | | | | [opts.correctAnswers] number|number[] | required if quiz — 0-based index(es) into answers | | [opts.solution] string | explanation shown after answering a quiz | | [opts.closePeriod] number | seconds until the poll auto-closes (5–600) | | [opts.closeDate] number | unix timestamp when the poll auto-closes | | {boolean} [opts.closed=false] - create the poll already closed | | |

ctx.replyWithLocation(latitude, longitude, opts = {})

Send a geographic location.

ctx.replyWithContact(phoneNumber, firstName, lastName = "", opts = {})

Send a contact card.

ctx.replyWithDice(emoji = "🎰", opts = {})

Send an animated dice/emoji reaction (🎲 🎯 🏀 ⚽ 🎰 🎳).

ctx.deleteMessage(id)

Delete a message by id (revokes for everyone). id is required.

ctx.forwardMessage(toChatId, id)

Forward a message by id to another chat. id is required.

ctx.pinMessage(id, opts = {})

Pin a message by id in the current chat. id is required.

ctx.unpinMessage(id)

Unpin a message by id in the current chat. id is required.

ctx.sendChatAction(action = "typing")

Show the "typing..." / "sending photo..." indicator.

ctx.getSender()

Get the full entity (User/Chat/Channel) of whoever sent the message.

ctx.getChat()

Get the full entity (User/Chat/Channel) of the current chat.

ctx.replyViaBot(bot, query = "", opts = {})

Send a message "via @bot" — i.e. runs an inline query against a bot that supports inline mode, then sends one of its results. This is the only way to get the "via @bot" label on a message; you can't set viaBotId manually on a normal sendMessage/sendFile call.

| Param | Description | | --- | --- | | bot string | the bot's @username (with or without the @) | | {string} [query=""] - the inline query text to send to the bot | | | | [opts] object | — | | {number} [opts.resultIndex=0] - which result to pick from the list | | | | {boolean} [opts.hideVia=false] - hide the "via @bot" label | | | | {number} [opts.retries=2] - retry count if the bot times out (BOT_RESPONSE_TIMEOUT) or is briefly unreachable — this is a bot-side issue, not something the query itself can prevent | | |

// equivalent of typing "@pic cats" and sending the first result
await ctx.replyViaBot("pic", "cats");

ctx.queryInlineBot(bot, query = "", opts = {})

Run an inline query against a bot and return the raw results, WITHOUT sending any of them — the "respond to an inline query as a user" counterpart to bot.inlineQuery() (which only fires when this client is logged in as a bot). Use this from a user/userbot session to see what a bot's inline mode returns for a query, inspect/filter the results yourself, and optionally send one later via result.click() (each item is a GramJS InlineResult, same shape used internally by replyViaBot).

| Param | Description | | --- | --- | | bot string | the bot's @username (with or without the @) | | {string} [query=""] - the inline query text to send to the bot | | | | [opts] object | — | | {number} [opts.retries=2] - retry count if the bot times out (BOT_RESPONSE_TIMEOUT) or is briefly unreachable | | |

Returns: Promise<import("telegram").InlineResult[]>

const results = await ctx.queryInlineBot("pic", "cats");
console.log(results.map((r) => r.title));
// send the one you actually want, whenever you're ready:
await results[2].click({ entity: await ctx.peer() });

More media / interaction helpers

ctx.replyWithVenue(latitude, longitude, title, address, opts = {})

Send a venue — a location pinned with a name and address (e.g. a restaurant or landmark), like the "Location > Venue" picker in-app.

ctx.replyWithLiveLocation(latitude, longitude, period = 900, opts = {})

Send a live location that updates in real time for period seconds (Telegram allows 60–86400). Use client.editMessage(...) with a new InputMediaGeoLive afterwards to push position updates.

ctx.react(id, emoji = "👍", opts = {})

React to the triggering message with an emoji (❤️ 👍 🔥 🎉 etc). Pass an empty string or null to remove your reaction instead.

ctx.copyMessage(toChatId, opts = {})

Re-send the triggering message's content (text or media) to another chat, WITHOUT the "Forwarded from" header — unlike forwardMessage(). Closest equivalent to Bot API's "copy message".

ctx.markAsRead(userId)

Mark the current chat as read, up to (and including) this message.

ctx.downloadMedia(opts = {})

Download the media attached to the triggering message (photo, video, document, voice note, etc). Resolves to a Buffer by default, or writes straight to disk if you pass opts.outputFile: "path/to/save".

ctx.editMessageMedia(messageId, file, opts = {})

Edit the media of a message you previously sent (e.g. swap out a photo).

Voice / text / poll utilities

ctx.transcribeVoice(id)

Transcribe the voice note attached to the triggering message using Telegram's built-in transcription. Resolves to { text, transcriptionId, pending, trialRemainsNum, trialRemainsUntilDate }. If pending is true, the final text arrives later via an UpdateTranscribedAudio update rather than this call.

ctx.translateText(text, toLang)

Translate the triggering message's text into another language (e.g. "en", "id").

ctx.votePoll(id, options)

Vote on the poll attached to the triggering message.

| Param | Description | | --- | --- | | options number|number[] | option index (or indexes, for multiple-choice polls), 0-based in the order the poll was created. |

ctx.retractVote(id)

Retract your vote on the triggering message's poll.

Contacts / history

ctx.blockUser(userId)

Block a user by id/username. userId is required.

ctx.unblockUser(userId)

Unblock a user by id/username. userId is required.

ctx.getHistory(userId, limit = 20, opts = {})

Fetch the most recent messages in the current chat.

ctx.searchMessages(query, opts = {})

Search for messages containing query in the current chat.

ctx.unpinAllMessages(userId)

Unpin every pinned message in the current chat.

ctx.getParticipants(opts = {})

List participants/members of the current chat (groups/channels only).

ctx.getPinnedMessages(opts = {})

List every pinned message in the current chat.

ctx.getEntityByUsername(username)

Get metadata for a user/chat/channel by @username. Resolves both the basic entity (id, name, username, etc.) and the "full" info (bio for users; description/member count for chats/channels).

| Param | Description | | --- | --- | | username string | with or without the leading @ |

Returns: Promise<{ entity: object, full: object|null }>

const { entity, full } = await ctx.getEntityByUsername("durov");
console.log(entity.firstName, full.fullUser.about);

Channel / group management (supergroups & channels — see notes below)

ctx.banUser(userId, opts = {})

Ban a user from the current supergroup/channel (they can't rejoin unless unbanned). For basic (non-super) groups, Telegram doesn't support per-user restrictions — use kickUser() instead.

| Param | Description | | --- | --- | | userId string|number | — | | {object} [opts] e.g. { untilDate: unixTimestamp } for a timed ban | | |

ctx.unbanUser(userId)

Clear all restrictions on a user in the current supergroup/channel.

ctx.kickUser(userId)

Kick a user (remove them, but they CAN rejoin) from a supergroup/channel. For basic groups, this uses messages.DeleteChatUser instead.

ctx.promoteUser(userId, rank = "", rights = {})

Promote a user to admin in the current supergroup/channel. rights overrides the (fairly permissive) defaults below.

ctx.demoteUser(userId)

Strip a user of all admin rights in the current supergroup/channel.

ctx.inviteToChannel(channelId, userIds)

Invite one or more users to the current supergroup/channel.

ctx.setChatTitle(id, title)

Rename the current chat/supergroup/channel.

ctx.setChatPhoto(id, file)

Change the current chat/supergroup/channel's profile photo.

ctx.deleteChatPhoto(id)

Remove the current chat/supergroup/channel's profile photo.

Stickers & GIFs

ctx.searchStickers(emoji)

Look up stickers matching an emoji (e.g. "😂").

ctx.getStickerSet(shortName)

Get full info + document list for a sticker set by its short name.

ctx.searchGifs(query, offset = "")

Search Telegram's global GIF results for a query.

ctx.saveGif(document, unsave = false)

Save (or unsave) a GIF document to the user's "Saved GIFs".

| Param | Description | | --- | --- | | document object | an Api.Document, e.g. from searchGifs() results | | {boolean} [unsave=false] | | |

Privacy & account settings

ctx.getPrivacySettings(key = "phoneNumber")

Get your current privacy rules for a given key.

| Param | Description | | --- | --- | | [key] "phoneNumber"|"lastSeen"|"chatInvite"|"phoneCall"|"profilePhoto"|"forwards" | — |

ctx.updateProfile(opts = {})

Update your account's first/last name and bio ("about").

ctx.updateUsername(username)

Change your account's @username.

ctx.setOnlineStatus(online = true)

Manually set your account's online/offline status.

Advanced file uploads

ctx.replyWithSpoiler(file, opts = {})

Send a photo/video with the "spoiler" blur overlay.

ctx.replyWithThumb(file, thumb, opts = {})

Send a file with a custom thumbnail image.

ctx.uploadWithProgress(file, onProgress)

Upload a file with progress tracking, without sending it yet. Returns an uploaded-file handle you can pass as file to any replyWith* method — useful for showing upload progress on large files.

| Param | Description | | --- | --- | | file * | — | | onProgress (uploaded: number, total: number) => void | — |

Channel / Group Actions

ctx.joinChannel(id)

Join channel/group by username, id, or invite link

| Param | Description | | --- | --- | | channel string | number | "@username", id, or entity |

ctx.joinByInvite(hash)

Join private group/channel via invite hash

| Param | Description | | --- | --- | | hash string | hash from t.me/+HASH |

ctx.leaveChannel(id = null)

Leave channel/group

| Param | Description | | --- | --- | | channel string | number | "@username", id, or entity. Default: chatId |

ctx.saveMediaMessage(messageId, opts = {})

Get buffer + metadata from media with messageId

| Param | Description | | --- | --- | | [messageId] number | messageId | | {object} [opts={}] - opts downloadMedia | | |

Returns: Promise<{buffer: Buffer, metadata: object, filename: string}>

Account / self

ctx.getMe()

Get the full User entity for the account this client is logged into.

ctx.setEmojiStatus(documentId = null, untilDate = null)

Set (or clear) your profile's emoji status. Pass null/omit to clear.

Dialogs & chat folders

ctx.getDialogs(opts = {})

List your open conversations (chats/groups/channels), most recent first.

ctx.archiveChat(id = null)

Move a chat into the "Archived Chats" folder.

ctx.unarchiveChat(id = null)

Move a chat back out of "Archived Chats" into the default folder.

Contacts

ctx.getContacts()

Get your saved contact list.

ctx.addContact(phone, firstName, lastName = "")

Add (or update) a contact by phone number.

| Param | Description | | --- | --- | | phone string | phone number, with country code (e.g. "+62812...") |

ctx.importContacts(contacts)

Bulk-import a list of contacts.

| Param | Description | | --- | --- | | {Array<{phone: string, firstName: string, lastName?: string}>} contacts | | |

ctx.deleteContact(userId)

Remove one or more users from your contact list.

ctx.getCommonChats(userId)

List other chats you have in common with a given user.

Invite links

ctx.exportChatInvite(id = null, opts = {})

Create a new invite link for the current (or given) chat/channel.

ctx.getExportedInvites(id = null, opts = {})

List invite links previously created for the current (or given) chat/channel.

Scheduled messages

ctx.replySchedule(text, when, opts = {})

Schedule a message to be sent later instead of immediately.

| Param | Description | | --- | --- | | when Date|number | a Date, or a unix timestamp (seconds) |

ctx.getScheduledMessages()

List messages currently scheduled (not yet sent) in the current chat.

ctx.deleteScheduledMessages(ids)

Cancel one or more scheduled messages before they're sent.

Misc chat utilities

ctx.reportSpam(id = null)

Report a chat/message as spam to Telegram.

ctx.setMessagesTTL(period, id = null)

Set (or clear with 0) the auto-delete timer for new messages in a chat, in seconds.

ctx.getReadParticipants(id)

Who has read a given message so far (only works in small groups).

More message-specific utilities (messageId/userId are required below)

ctx.getMessageById(messageId)

Fetch a specific message by id from the current chat. messageId is required.

ctx.forwardMessages(toChatId, ids)

Forward one or more specific messages (by id) to another chat. ids is required.

ctx.editMessageReplyMarkup(messageId, rows = [])

Replace (or clear, with an empty array) the inline buttons on a message you previously sent. messageId is required.

| Param | Description | | --- | --- | | messageId number | — | | {Array<Array<{text: string, data?: string, url?: string}>>} [rows=[]] | | |

ctx.getMessageLink(messageId, id = null)

Get a link to a message (works for public chats/channels, or t.me/c/... otherwise). messageId is required.

More user-specific utilities (userId is required below)

ctx.getUserPhotos(userId, opts = {})

Get a user's profile photos. userId is required.

ctx.getUserStatus(userId)

Get a user's last-seen/online status. userId is required.

ctx.muteChat(id = null, untilDate = 0x7fffffff)

Mute notifications for a chat/group/channel (defaults to the current chat).

ctx.unmuteChat(id = null)

Unmute a chat/group/channel (defaults to the current chat).

ctx.getAdmins(id = null)

List administrators of the current supergroup/channel (defaults to the current chat).

ctx.setSlowMode(seconds, id = null)

Set slow mode delay (seconds between messages per user) on a supergroup (defaults to the current chat).

Payments

ctx.replyWithInvoice(opts = {})

Send an invoice message (Telegram Payments) to the current chat.

| Param | Description | | --- | --- | | opts object | — | | opts.title string | — | | opts.description string | — | | opts.currency string | ISO 4217 code, e.g. "USD" (use "XTR" for Telegram Stars) | | {Array<{label: string, amount: number}>} opts.prices - amount in the smallest currency unit (e.g. cents) | | | | opts.payload string | your own opaque payload, returned to you once paid | | {string} [opts.provider=""] - payment provider token from @BotFather (omit for Stars invoices) | | | | {object} [opts.providerData={}] - arbitrary JSON passed through to the provider | | | | [opts.photoUrl] string | — | | {boolean} [opts.test=false] - use the provider's test mode | | |

ctx.getPaymentForm(messageId)

Get the payment form for an invoice message. messageId is required.

ctx.getPaymentReceipt(messageId)

Get the receipt for an already-paid invoice message. messageId is required.

ctx.exportInvoice(invoiceMedia)

Export a shareable t.me/$slug-style link for an invoice. invoiceMedia is required (an Api.InputMediaInvoice, as built internally by replyWithInvoice).

ctx.getSavedPaymentInfo()

Get your saved shipping/payment info, if you've allowed Telegram to remember it.

ctx.clearSavedPaymentInfo(opts = {})

Clear saved shipping info and/or saved card credentials.

ctx.getStarsStatus(id = null)

Get your (or a bot's, via id) Telegram Stars balance and status.

ctx.getStarsTransactions(opts = {})

Get your (or a bot's, via opts.id) Telegram Stars transaction history.

Bot command menu (only meaningful when logged in as a bot)

ctx.setBotCommands(commands, opts = {})

Set the command list shown in Telegram's "/" menu button for this bot.

| Param | Description | | --- | --- | | {Array<{command: string, description: string}>} commands | | | | [opts] object | — | | {string} [opts.langCode=""] - language code, empty = default for all languages | | |

ctx.getBotCommands(opts = {})

Get the currently configured bot command list.

ctx.setBotMenuButton(opts = {})

Set the menu button next to the message box for this bot — either a plain "Commands" button (default) or a custom text + URL button.

| Param | Description | | --- | --- | | [opts] object | — | | [opts.text] string | button label (omit for the default "Commands" button) | | [opts.url] string | URL/web app to open when tapped |

Blocklist & invite links

ctx.getBlockedUsers(opts = {})

List users you've blocked.

ctx.getChatInviteImporters(id, opts = {})

See who joined the current chat via invite links (optionally a specific one).

Rich Messages (Bot API 10.1, June 2026) — BOT ACCOUNTS ONLY

ctx.sendRichMessage(chatId, markdown, opts = {})

Send a Rich Message (tables, headings, dividers, blockquotes, etc.) — Telegram's Bot API 10.1 sendRichMessage feature. IMPORTANT: this is a bot-account-only feature. It calls Telegram's HTTP Bot API directly (api.telegram.org/bot<token>/sendRichMessage), which fundamentally requires a bot token — there is no equivalent for regular user (MTProto) sessions logged in via loginOptions. If this client was started without a botToken, this throws instead of silently failing or sending something broken.

| Param | Description | | --- | --- | | chatId string|number | — | | markdown string | GitHub-flavored markdown: # headings, \| \| \| tables, --- dividers, > blockquotes, etc. | | [opts] object | — |

await ctx.sendRichMessage(ctx.chatId, [
  "# 📊 Data Player",
  "",
  "| Nama | Score |",
  "|------|-------|",
  "| Player 1 | 12,500 |",
].join("\n"));

Payments

bot.command("buy", async (ctx) => {
  await ctx.replyWithInvoice({
    title: "Premium Sticker Pack",
    description: "50 exclusive stickers",
    currency: "USD",          // use "XTR" for a Telegram Stars invoice
    prices: [{ label: "Sticker pack", amount: 500 }], // 500 = $5.00 (smallest unit)
    payload: "sticker-pack-42",
    provider: process.env.PROVIDER_TOKEN, // omit entirely for Stars invoices
  });
});

bot.on("message", async (ctx) => {
  // once the user pays, fetch the receipt using the paid message's id
  const receipt = await ctx.getPaymentReceipt(ctx.message.id);
});

For quizzes:

await ctx.replyWithPoll("2 + 2 = ?", ["3", "4", "5"], {
  quiz: true,
  correctAnswers: 1,       // 0-based index into the answers array
  solution: "2 + 2 = 4",
});

Rich Messages (Bot API 10.1+)

ctx.sendRichMessage wraps Telegram's Bot API 10.1 Rich Messages feature — tables, headings, dividers, blockquotes, and other structured formatting rendered natively in the client, built from a plain GitHub-flavored markdown string:

await ctx.sendRichMessage(ctx.chatId, [
  "# Leaderboard",
  "",
  "| Name | Score |",
  "|------|-------|",
  "| Alice | 12,500 |",
  "| Bob | 9,800 |",
].join("\n"));

This is a bot-account-only feature — it calls Telegram's HTTP Bot API (api.telegram.org/bot<token>/sendRichMessage) directly, which requires a bot token. There's no MTProto equivalent for regular user sessions, so calling it on a client started without botToken throws instead of failing silently.

Full GramJS coverage: ctx.api

Every ctx.xxx() helper above is a convenience shortcut for a common case. For anything else — the other 800+ raw GramJS/MTProto methods across messages, channels, account, contacts, users, photos, bots, payments, phone, stories, stats, premium, folders, upload, help, auth, langpack, and more — use ctx.api (also available as bot.api outside handlers, and on CallbackContext/InlineQueryContext).

It's a dynamic proxy over the entire Api namespace, so every GramJS method is available with no extra setup and no risk of going stale when GramJS adds new methods:

// Same call shape as raw GramJS: client.invoke(new Api.<ns>.<Method>(params))
await ctx.api.messages.SendMessage({ peer, message: "hi", randomId: BigInt(Date.now()) });
await ctx.api.channels.EditTitle({ channel, title: "New name" });
await ctx.api.account.GetAuthorizations({});
await ctx.api.stories.SendStory({ peer, media, randomId: BigInt(Date.now()) });
await ctx.api.bots.SetBotCommands({ scope, langCode: "en", commands });
await ctx.api.payments.GetPaymentForm({ invoice });

// Outside a handler:
await bot.api.help.GetConfig({});

Params match GramJS's own TL request classes exactly — check GramJS's TL reference for each method's shape. createApiProxy(client) is also exported from the package root if you want this on a raw TelegramClient you're managing yourself.

Advanced usage

If you already create/manage the TelegramClient yourself (custom connection options, shared across multiple modules, etc.), skip clientStart and use Bot directly:

const { Bot } = require("xzcgram");
// require("telegram") yourself only if you need this lower-level control
const { TelegramClient } = require("telegram");
const { StringSession } = require("telegram/sessions");

const client = new TelegramClient(new StringSession(session), apiId, apiHash, {});
await client.start({ /* ... */ });

const bot = new Bot(client);
bot.command("start", async (ctx) => ctx.reply("Hello!"));
await bot.launch();

Why

GramJS is a full MTProto client, not a bot framework — there's no built-in concept of commands or routing, you handle raw events yourself. This package adds just enough structure to make bot-style code readable, without hiding GramJS underneath an opinionated abstraction.

License

MIT