Quick Start
Build an end-to-end userbot that logs in, listens for messages, replies, and persists the session — all in about ten minutes.
Set up#
A fresh project, the package, and a runner that can execute TypeScript directly. tsx is the path of least resistance; node --import tsx or ts-node work equally well.
mkdir my-bot && cd my-bot
npm init -y
npm install teleproto
npm install -D tsx typescript @types/nodeDrop your credentials into .env (and add .env to .gitignore before you forget):
TG_API_ID=1234567
TG_API_HASH=0123456789abcdef0123456789abcdefSign in once#
The full login flow lives in a single client.start() call. teleproto walks you through the SMS-code prompt, asks for the 2FA password if you have one, and registers the device. We're using StoreSession here instead of StringSession — it writes the auth key to disk under ./bot-session/, which is what you want for ongoing development. (Use StringSession when you need to ship the session as an env var to a one-off container.)
import "node:process";
import { TelegramClient } from "teleproto";
import { StoreSession } from "teleproto/sessions";
import { NewMessage } from "teleproto/events";
import { createInterface } from "node:readline/promises";
const apiId = Number(process.env.TG_API_ID);
const apiHash = process.env.TG_API_HASH!;
// Disk-backed session — persists under ./bot-session/
const session = new StoreSession("bot-session");
const client = new TelegramClient(session, apiId, apiHash, {
connectionRetries: 5,
});
const rl = createInterface({ input: process.stdin, output: process.stdout });
await client.start({
phoneNumber: () => rl.question("Phone: "),
phoneCode: () => rl.question("Code from Telegram: "),
password: () => rl.question("2FA password (if set): "),
onError: (err) => {
console.error(err);
return false;
},
});
rl.close();
console.log("Signed in as", (await client.getMe()).username);Listen + reply#
Handlers are plain callbacks registered through addEventHandler. Pair the callback with an event builder like NewMessage to filter what reaches you. Below: only incoming messages that start with !echo, with an early exit on our own outgoing messages to avoid replying to ourselves.
client.addEventHandler(async (event) => {
// ignore our own outgoing messages
if (event.message.out) return;
const text = (event.message.message || "").trim();
await event.message.reply({ message: `you said: ${text}` });
}, new NewMessage({ pattern: /^!echo\b/ }));
// Keep the process alive. client.disconnected is a boolean getter,
// not a Promise — don't await it.
await new Promise<void>((resolve) => {
process.once("SIGINT", () => resolve());
});
await client.disconnect();There is no middleware pipeline, no next(), no dispatcher. Register more handlers for more events; each one runs independently when its builder matches.
Run it#
Load the env file and run the script:
# load env vars from .env (use dotenv-cli, direnv, or your shell)
export $(grep -v '^#' .env | xargs)
npx tsx bot.tsFirst run: answer the phone-number and code prompts. The bot then sits idle and replies to anything matching !echo. Hit Ctrl-C to exit cleanly.
One footgun worth naming: client.disconnected is a boolean getter, not a Promise. Writing await client.disconnected resolves immediately and drops you out of the process. Keep the process alive with a real wait — a new Promise(() => {}), a SIGINT listener like the one above, or whatever your process supervisor expects.
Where to next#
Sessions
StringSession vs StoreSession vs MemorySession — when to use which, and the version-prefixed string format.
Authentication
QR login, bot tokens, 2FA, email verification, and reCaptcha callbacks.
Events
Every event builder: NewMessage, EditedMessage, CallbackQuery, Album, ChatAction, Raw.
Raw TL API
Drop down to client.invoke(new Api.x.Y(...)) when the high-level methods don't cover what you need.