| name | using-mtcute |
| description | Use when working with mtcute, @mtcute/* packages, Telegram MTProto in TypeScript, TL types, Telegram API methods, or building Telegram bots/clients in TypeScript. Not for Bot API wrapper libraries (grammy, telegraf, node-telegram-bot-api).
|
mtcute — TypeScript MTProto Library
What is mtcute?
mtcute is a modern, type-safe TypeScript library for Telegram's MTProto protocol. Unlike Bot API wrappers (grammy, telegraf, node-telegram-bot-api), mtcute speaks Telegram's native binary protocol directly, giving access to the full Telegram API — not just the bot subset.
Architecture overview
Core packages (you likely won't use these directly):
- @mtcute/core: MTProto client, high-level methods, TL schema, types
- @mtcute/html-parser: HTML ↔ Telegram entities
- @mtcute/markdown-parser: Markdown ↔ Telegram entities
- @mtcute/file-id: Bot API file_id parsing
Platform packages (use the one that matches your target platform):
- @mtcute/node, @mtcute/bun, @mtcute/deno: Runtime bindings for server-side JavaScript
- @mtcute/web: Browser bindings (IndexedDB, WebCrypto, WebSocket)
Extra packages (only use these for the specific functionality they cover):
- @mtcute/dispatcher: Update routing, filters, bot framework
- @mtcute/convert: Importing sessions from other libraries
- @mtcute/test: Helpers for unit tests
Always install and import everything from the platform package, not from @mtcute/core:
import { TelegramClient } from '@mtcute/node'
import { TelegramClient } from '@mtcute/core'
Basic usage
Creating a client
import { TelegramClient } from '@mtcute/node'
const tg = new TelegramClient({
apiId: Number(process.env.API_ID),
apiHash: process.env.API_HASH,
storage: 'my-account',
})
Client lifecycle
When you create a TelegramClient, it starts in the disconnected state.
In most cases, you will want to use the start method to connect and authenticate:
const self = await tg.start({ botToken: process.env.BOT_TOKEN })
const self = await tg.start({
phone: () => input('Phone: '),
code: () => input('Code: '),
password: () => input('2FA password: '),
})
However in some cases you may want to manually control the lifecycle:
await tg.connect()
await tg.disconnect()
await tg.destroy()
Calling high-level methods
The TelegramClient class has 300+ high-level convenience methods. Some of the most commonly used ones are listed below:
- messages:
sendText, sendMedia, getHistory, editMessage, deleteMessages, forwardMessages, searchMessages, ...
- chats:
getChat, getChatMember, banChatMember, setChatTitle, setChatPhoto, ...
- users:
getUsers, getMe, setMyProfilePhoto, ...
- files:
downloadFile, uploadFile, downloadAsStream, ...
- dialogs:
getDialogs, getPeerDialogs, ...
- bots:
answerInlineQuery, answerCallbackQuery, setMyCommands, ...
- forums:
createForumTopic, editForumTopic, ...
- stickers:
getStickerSet, createStickerSet, ...
- stories:
sendStory, getStories, ...
- auth:
start, logOut, sendCode, ...
- password:
enable2fa, change2fa, ...
await tg.sendText('me', 'Hello!')
await tg.sendText(chatId, html`Hello, <b>${name}</b>!`)
await tg.sendMedia(chatId, {
type: 'photo',
file: 'file:./photo.jpg',
caption: 'Check this out',
})
const chat = await tg.getChat('username')
await tg.downloadToFile('path/to/save.jpg', photo)
Peer resolution
High-level methods in mtcute accept InputPeerLike for chat/user arguments, which can be:
- Numeric ID:
123456789
- Username string:
'username'
'me' / 'self' for the current user
- A
tl.TypeInputPeer object
- A high-level object (
User, Chat, Message, etc.)
Handling updates with Dispatcher
import { Dispatcher, filters } from '@mtcute/dispatcher'
const dp = Dispatcher.for(tg)
dp.onNewMessage(filters.chat('private'), async (msg) => {
await msg.replyText('Hello!')
})
dp.onNewMessage(filters.command('start'), async (msg) => {
await msg.replyText('Welcome!')
})
dp.onCallbackQuery(filters.dataPrefix('btn_'), async (query) => {
await query.answer({ text: 'Clicked!' })
})
await tg.startUpdatesLoop()
Raw TL method calls
For methods not wrapped by high-level API, use .call():
const result = await tg.call({
_: 'messages.getEmojiKeywords',
langCode: 'en',
})
TL type system
mtcute generates TypeScript types from Telegram's TL schema. All types live under the tl namespace:
import { tl } from '@mtcute/core'
| TL concept | TypeScript name | Example |
|---|
Object user | tl.RawUser | { _: 'user', id: number, ... } |
Namespaced object messages.chats | tl.messages.RawChats | |
Method users.getUsers | tl.users.RawGetUsersRequest | |
| Union type | tl.TypeUser | tl.RawUser | tl.RawUserEmpty |
Every TL object has a _ discriminant field with its constructor name (e.g., _: 'user').
Type guards are available: tl.isAnyPeer(), tl.isAnyMessage(), etc.
The full TL schema is available as JSON at @mtcute/node/tl/api-schema.json (or any platform package). Format:
{
"l": 223,
"e": [
{
"kind": "class" | "method",
"name": "user",
"id": 826896937,
"type": "User",
"arguments": [{
"name": "id",
"type": "int53",
"typeModifiers"?: {
"predicate"?: "flags.0",
"isVector"?: true,
}
}, ...],
"comment": "..."
},
...
],
"u": { "User": "..." }
}
When typeModifiers.predicate is set (e.g. "flags.3"), the field is optional and only present when bit 3 of the flags field is set. When isVector is true, the actual type is Vector<type>.
Looking up types and method signatures
Three bundled tools read from the installed @mtcute/* packages in node_modules. All support fuzzy matching and auto-correct typos.
When to use which:
- Need a raw TL type/method definition or its TS interface →
get-constructor
- Need a high-level client method signature (params, return type, JSDoc) →
get-method
- Need a high-level wrapper class/interface definition (Message, Chat, etc.) →
get-class
- Need the TL parameters a raw API call expects →
get-constructor --with-references
- Not sure which method/class to use →
get-method --search or get-class --search
get-constructor: TL schema lookup
Shows TL definition, TypeScript type, union membership, and return type.
node .claude/skills/using-mtcute/tools/get-constructor.js <name>
node .claude/skills/using-mtcute/tools/get-constructor.js --with-references <name>
Accepts any of these input formats:
- TL names:
user, messages.sendMessage
- TS type names:
RawUser, TypeInputUser, tl.messages.RawChats, messages.RawGetHistoryRequest
Use --with-references when you need to understand the types referenced by a constructor's arguments or return type — it appends all referenced constructors/unions to the output.
get-method: high-level client method lookup
Shows the full JSDoc + method signature from TelegramClient.
node .claude/skills/using-mtcute/tools/get-method.js <method-name>
node .claude/skills/using-mtcute/tools/get-method.js --search <keyword>
node .claude/skills/using-mtcute/tools/get-method.js --list
- Default mode: exact/fuzzy match on method name, falls back to substring in names then descriptions
--search: search in both method names and descriptions, lists all matches
--list: print all available methods with one-line descriptions
get-class: high-level type/class lookup
Shows the full class/interface/type definition with all members from highlevel/types/.
node .claude/skills/using-mtcute/tools/get-class.js <name>
node .claude/skills/using-mtcute/tools/get-class.js --no-members <name>
node .claude/skills/using-mtcute/tools/get-class.js --search <keyword>
node .claude/skills/using-mtcute/tools/get-class.js --list
- Default mode: exact/fuzzy match on export name, shows full definition with all members
--no-members: only show the declaration line + JSDoc, not the body
--search: search in both names and descriptions, lists all matches (e.g., --search keyboard)
--list: print all available exports with one-line descriptions
High-level types
mtcute wraps raw TL objects in convenient classes with helper methods:
Message — wraps tl.RawMessage, has .replyText(), .editText(), .delete(), .forward(), etc.
Chat — wraps channel/group/user chat info
User — wraps tl.RawUser
Document, Photo, Video, Audio, Voice, Sticker — media types
InlineQuery, CallbackQuery, ChatJoinRequest — update types
These are in node_modules/@mtcute/core/dist/highlevel/types/.
Text formatting
mtcute supports tagged template literals for formatting:
import { html, md } from '@mtcute/node'
await tg.sendText(chat, html`<b>Bold</b> and <i>italic</i>`)
await tg.sendText(chat, md`**Bold** and *italic*`)
The html and md tags handle escaping automatically when interpolating variables.
Common patterns
Error handling
import { tl } from '@mtcute/node'
try {
await tg.sendText(chatId, 'hello')
} catch (e) {
if (tl.RpcError.is(e, 'FLOOD_WAIT_%d')) {
console.log(`Rate limited, wait ${e.seconds}s`)
}
}
Sending media
import { InputMedia } from '@mtcute/node'
await tg.sendMedia(chatId, InputMedia.document('file:/path/to/file.pdf', {
fileName: 'report.pdf',
}))
await tg.sendMedia(chatId, InputMedia.photo(buffer))
await tg.sendMedia(chatId, InputMedia.photo('https://example.com/image.jpg'))
Keyboards
import { BotKeyboard } from '@mtcute/node'
await tg.sendText(chatId, 'Pick one:', {
replyMarkup: BotKeyboard.inline([
[BotKeyboard.callback('Yes', 'btn_yes'), BotKeyboard.callback('No', 'btn_no')],
[BotKeyboard.url('Docs', 'https://mtcute.dev')],
]),
})
await tg.sendText(chatId, 'Choose:', {
replyMarkup: BotKeyboard.reply([
[BotKeyboard.text('Option A'), BotKeyboard.text('Option B')],
]),
})
Pagination / iteration
Many methods have an iter* counterpart returning AsyncIterableIterator:
for await (const msg of tg.iterHistory(chatId, { chunkSize: 100 })) {
console.log(msg.text)
}
for await (const msg of tg.iterSearchMessages({ query: 'hello', chatId })) {
console.log(msg.id)
}
const page = await tg.getHistory(chatId, { limit: 50 })
const nextPage = await tg.getHistory(chatId, { limit: 50, offset: page.next })
Client events
The client exposes Emitter instances for connection lifecycle and errors. Subscribe with .add(), unsubscribe with .remove():
tg.onConnectionState.add((state) => {
console.log('Connection:', state)
})
tg.onError.add((err) => {
console.error('Error:', err)
})
tg.onRawUpdate.add((update) => { ... })
Proxy / custom transport (Node.js)
import { TelegramClient } from '@mtcute/node'
import {
HttpProxyTcpTransport,
SocksProxyTcpTransport,
MtProxyTcpTransport,
proxyTransportFromUrl,
} from '@mtcute/node'
const tg = new TelegramClient({
transport: proxyTransportFromUrl('socks5://user:pass@1.2.3.4:1080'),
})
Further reading
Full documentation and LLM-friendly reference: https://mtcute.dev/llms.txt