Skip to content
teleproto

Raw TL API

Every method in Telegram's TL schema is generated into Api.* and callable through client.invoke(...). When the friendly methods don't cover what you need, drop to raw.

Friendly first, raw when stuck#

The mental ladder is short. Reach for client.sendMessage, client.getMessages, client.iterDialogs first — they cover almost everything you'll want to do day to day. The moment you need a parameter the friendly method doesn't expose (silent delivery, a custom randomId, scheduling, a TTL period, an alternative entity reference), drop to client.invoke(new Api.messages.SendMessage(...)). The friendly methods are wrappers; raw is the floor underneath them.

friendly-vs-raw.ts
import bigInt from "big-integer";
import { Api } from "teleproto";

// Friendly: covers 90% of cases.
await client.sendMessage("me", { message: "hi" });

// Raw: same outcome, every TL field at your disposal.
await client.invoke(
  new Api.messages.SendMessage({
    peer: await client.getInputEntity("me"),
    message: "hi",
    noWebpage: true,
    silent: true,
    randomId: bigInt(Date.now()),
  }),
);

Anatomy of a request#

A raw call is three things: a constructor for the TL method, an invoke, and a return type that's usually a union you need to narrow. Take messages.getHistory:

get-history.ts
import bigInt from "big-integer";
import { Api } from "teleproto";

const req = new Api.messages.GetHistory({
  peer: await client.getInputEntity("@telegram"),
  limit: 20,
  offsetId: 0,
  offsetDate: 0,
  addOffset: 0,
  maxId: 0,
  minId: 0,
  hash: bigInt.zero,
});

const result = await client.invoke(req);
// result is Api.messages.TypeMessages — a union. Narrow by className:
if (result instanceof Api.messages.Messages
  || result instanceof Api.messages.MessagesSlice
  || result instanceof Api.messages.ChannelMessages) {
  for (const msg of result.messages) {
    if (msg instanceof Api.Message) {
      console.log(msg.id, msg.message);
    }
  }
}

The return is Api.messages.TypeMessages — a union of Messages, MessagesSlice, ChannelMessages, and MessagesNotModified. Narrow with instanceof or by checking the className discriminator and TypeScript will let you in.

The 5-step recipe#

Telethon's classic five-step ladder, ported to teleproto:

  1. Open core.telegram.org/methods and find your method.
  2. Look at the params and what they expect (a Peer? an InputUser? an int? a Vector<int>?).
  3. Build the inputs with client.getInputEntity(...), client.getEntity(...), or by instantiating an Api.Input* constructor directly.
  4. Call await client.invoke(new Api.<namespace>.<Method>(params)).
  5. Handle the result. It's a TL union — narrow it with instanceof before reading fields.

Constructors you'll instantiate by hand#

Not every Api.* entry is a method. Half the namespace is TL types — peer references, input objects, update shapes — that you pass intorequests or match on inside event handlers. A few you'll see constantly:

constructors.ts
import bigInt from "big-integer";
import { Api } from "teleproto";

// Peer references — what messages and updates point at.
new Api.PeerUser({ userId: bigInt("12345") });
new Api.PeerChat({ chatId: bigInt("67890") });
new Api.PeerChannel({ channelId: bigInt("100123456") });

// Input peers — what TL methods accept as the "peer" param.
new Api.InputPeerSelf();
new Api.InputPeerUser({
  userId: bigInt("12345"),
  accessHash: bigInt.zero,
});

// Update shapes you'll match on inside event handlers.
// (Don't construct these yourself — Telegram sends them to you.)
//   Api.UpdateBotCallbackQuery
//   Api.UpdateNewMessage
//   Api.UpdateEditMessage

Rule of thumb: capitalised names with no namespace (Api.PeerUser, Api.InputPeerSelf) are types. Namespaced ones with verb-ish names (Api.messages.SendMessage, Api.channels.JoinChannel) are methods you pass to invoke.

BigInts#

Telegram IDs and access hashes don't fit in JavaScript's number, so teleproto uses the npm big-integer package — not the native BigInt. IDs come back as bigInt instances. Pass them straight to Api constructors. Call .toString() when you need to print or log them. Never hand a raw bigInt to JSON.stringify — it serialises as {} with no warning.

bigints.ts
import bigInt from "big-integer";
import { Api } from "teleproto";

const me = await client.getMe();
console.log("My ID:", me.id.toString());     // print: ".toString()"
console.log("Type:", typeof me.id);          // "object" — it's a bigInt instance

// Construct one when a TL field needs it.
const userId = bigInt("123456789");
new Api.PeerUser({ userId });

// Don't do this — JSON has no idea what bigInt is.
JSON.stringify({ id: me.id });               // -> '{"id":{}}'  (silently empty)
JSON.stringify({ id: me.id.toString() });    // -> '{"id":"123456789"}'  (good)

Wrap an Api call in a friendly helper#

When you use the same raw call from more than one place, lift it into a function with sensible defaults. That's how the friendly methods on TelegramClient were built in the first place — and how you grow your own surface on top of teleproto without forking it.

get-last-messages.ts
import bigInt from "big-integer";
import { Api } from "teleproto";
import type { TelegramClient } from "teleproto";

// Your own friendly wrapper around messages.getHistory.
export async function getLastMessages(
  client: TelegramClient,
  username: string,
  n = 20,
) {
  const peer = await client.getInputEntity(username);
  const res = await client.invoke(
    new Api.messages.GetHistory({
      peer,
      limit: n,
      offsetId: 0,
      offsetDate: 0,
      addOffset: 0,
      maxId: 0,
      minId: 0,
      hash: bigInt.zero,
    }),
  );
  return "messages" in res ? res.messages : [];
}