Skip to content
teleproto

Authentication

Every teleproto session starts with client.start(...). That single call covers users, bots, QR linking, two-factor challenges, email codes, and reCAPTCHA. One page, one flow per section, real code for each.

User auth#

The interactive flow: phone number, login code, optional 2FA password, optional first and last name for brand-new accounts. Every field is a callback so you can wire it to readline, a web form, a desktop prompt — whatever fits.

login-user.ts
import { TelegramClient } from "teleproto";
import { StringSession } from "teleproto/sessions";
import { createInterface } from "node:readline/promises";

const rl = createInterface({ input: process.stdin, output: process.stdout });

const apiId = Number(process.env.API_ID);
const apiHash = process.env.API_HASH!;
const session = new StringSession(process.env.SESSION ?? "");

const client = new TelegramClient(session, apiId, apiHash, {
  connectionRetries: 5,
});

await client.start({
  phoneNumber:       () => rl.question("Phone: "),
  phoneCode:         () => rl.question("Login code: "),
  password:          (hint) => rl.question(`2FA password${hint ? ` (${hint})` : ""}: `),
  firstAndLastNames: async () => [
    await rl.question("First name: "),
    await rl.question("Last name: "),
  ],
  onError: (err) => { console.error(err); return false; },
});

console.log("Logged in as", (await client.getMe()).username);
console.log("Save this:", client.session.save());
rl.close();

firstAndLastNames only fires when Telegram needs to create a new account on this phone. Existing accounts skip it. onError returning true retries the step; returning false (or nothing) aborts.

Bot auth#

Bots have no phone, no 2FA, no captcha. One field, one line:

login-bot.ts
await client.start({ botAuthToken: process.env.BOT_TOKEN! });

// or pass a function, useful when the token is fetched lazily
await client.start({ botAuthToken: () => process.env.BOT_TOKEN! });

The token can be a string or a function returning a string — useful when you pull credentials from a secret manager at startup.

QR login#

QR login skips SMS entirely. teleproto produces a token, you render it as a QR code, and the user scans it from Telegram > Settings > Devices > Link Desktop Device. teleproto doesn't bundle a renderer — pick one. Below uses qrcode-terminal; install it yourself with npm i qrcode-terminal.

login-qr.ts
import qrcode from "qrcode-terminal"; // npm i qrcode-terminal

await client.signInUserWithQrCode(
  { apiId, apiHash },
  {
    qrCode: async ({ token, expires }) => {
      const url = `tg://login?token=${token.toString("base64url")}`;
      qrcode.generate(url, { small: true });
      console.log(`QR expires in ${expires}s — scan from Telegram > Devices > Link Desktop Device`);
    },
    password: (hint) => prompt2FA(hint),       // your own resolver
    onError: (err) => { console.error(err); return false; },
  },
);

The qrCode resolver is called every time Telegram rotates the token (roughly every 30 seconds). Re-render on each call. If the account has 2FA enabled, password still fires after the scan.

Two-factor#

Two-factor is the password resolver on UserAuthParams. teleproto passes through the server-side hint (if the user set one) so you can show it in your prompt.

login-2fa.ts
await client.start({
  phoneNumber: () => rl.question("Phone: "),
  phoneCode:   () => rl.question("Code: "),
  password:    (hint) => rl.question(`2FA password${hint ? ` (hint: ${hint})` : ""}: `),
  onError:     console.error,
});

// Manage 2FA after login
await client.updateTwoFaSettings({
  currentPassword: process.env.OLD_2FA,
  newPassword:     process.env.NEW_2FA,
  hint:            "favourite mountain",
  email:           "[email protected]",
});

Once logged in, manage 2FA with client.updateTwoFaSettings(...). Pass currentPassword on any account that already has 2FA; omit it to set 2FA for the first time. newPassword, hint, and email are all optional and updated independently.

Email verification#

On some signups — typically the first login from a new region — Telegram requires a recovery email during the auth flow. teleproto exposes two extra resolvers for that: emailAddress collects the address, emailVerification collects the proof. Both are optional and only fire when Telegram asks.

login-email.ts
await client.start({
  phoneNumber: () => rl.question("Phone: "),
  phoneCode:   () => rl.question("Login code: "),
  password:    (hint) => rl.question(`2FA password${hint ? ` (${hint})` : ""}: `),

  // Fired only when Telegram asks for a recovery email on this signup
  emailAddress: () => rl.question("Email address: "),

  // Then either the user types the code Telegram emails them...
  emailVerification: async () => ({ code: await rl.question("Email code: ") }),

  // ...or you complete an out-of-band flow and hand back a token instead:
  // emailVerification: async () => ({ token: await fetchTokenFromOAuth() }),

  onError: console.error,
});

The verification return shape is a union: { code: string } for the six-digit code Telegram emails the user, or { token: string } if you ran the verification out of band (OAuth-style) and have a one-shot token. Return whichever you have.

reCAPTCHA#

Recently Telegram has started gating certain signups behind a Google reCAPTCHA. teleproto plumbs that through as reCaptchaCallback(siteKey) — you receive the site key, run it through whatever solver you use (a browser, 2captcha, your own UI), and return the resulting token string. The callback exists on both TelegramClientParams and UserAuthParams; the per-flow one wins when both are set.

login-recaptcha.ts
import { TelegramClient } from "teleproto";

const client = new TelegramClient(session, apiId, apiHash, {
  connectionRetries: 5,
  // Client-wide fallback — used by any flow that needs a captcha
  reCaptchaCallback: (siteKey) => solveCaptcha(siteKey),
});

await client.start({
  phoneNumber: () => rl.question("Phone: "),
  phoneCode:   () => rl.question("Code: "),
  // Per-flow override wins if both are set
  reCaptchaCallback: async (siteKey) => {
    console.log("Telegram requested a reCAPTCHA for site key", siteKey);
    return await solveCaptcha(siteKey); // return the solved token string
  },
  onError: console.error,
});

Logging out#

There's no high-level logout() — drop down to the raw TL call. Then wipe the local session so a future StringSession(saved)can't resurrect it.

logout.ts
import { Api } from "teleproto";

await client.invoke(new Api.auth.LogOut());
await client.session.delete(); // wipe StoreSession / StringSession state
await client.disconnect();

auth.LogOut revokes the auth key on Telegram's side. session.delete() clears local persistence (the on-disk directory for StoreSession, the in-memory buffers for StringSession). Skip the second call and the next start will fail with an UnauthorizedError rather than silently re-logging in.

Resetting login email#

If you set a recovery email, then lost access to it, the account can still be recovered through the phone. resetLoginEmail starts that process — Telegram sends a confirmation code to the phone, and you feed it through the regular phoneCode resolver on the next start.

reset-login-email.ts
// You lost access to the email tied to 2FA.
// Telegram will text or call the phone to confirm.
await client.resetLoginEmail("+15551234567");