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.
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:
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:
- Open core.telegram.org/methods and find your method.
- Look at the params and what they expect (a
Peer? anInputUser? anint? aVector<int>?). - Build the inputs with
client.getInputEntity(...),client.getEntity(...), or by instantiating anApi.Input*constructor directly. - Call
await client.invoke(new Api.<namespace>.<Method>(params)). - Handle the result. It's a TL union — narrow it with
instanceofbefore 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:
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.UpdateEditMessageRule 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.
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.
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 : [];
}