Skip to content

Operations — chats, users and the rest

This page continues Operations — messages. The rules it defines (order of checks, error codes, the chat argument, scope and hidden chats, rate buckets, the user action window, async) apply here unchanged; they are not repeated. Summary: Call protocol (ops). Python wrappers: Python — messages and chats.

Reminders that matter below:

  • A request is {"op": ..., "a": {...}}, optionally with "async": true; the answer is {"ok": true, "v": ...} or {"ok": false, "e": code, "m": message}.
  • Limits are charged before the permission check, so a refused call still spends its token.
  • Optional integer arguments of the wrong type are treated as absent; optional booleans of the wrong type give invalid; out-of-range limit values are clamped.
  • Secret chats and Saved Messages look like nonexistent chats (not_found); the service chat 777000 and chats outside the granted scope give denied (“this chat”).
  • No operation on this page ever returns quota.
Key Type Notes
id int peer id
title string
type string user, bot, group, forum, supergroup, channel (self and secret chats are never shown)
unread int unread count
pinned bool pinned in the main chat list
archived bool in the archive

Permission: chats.list. No bucket. Wait: 1.5 s.

freshgram.chats.list(limit=100)
{"op": "chats.list", "a": {"limit": 100}}
Argument Type Required Default Validation
limit int no 100 clamped to 1..500

Result: array of chat objects, main list first, then the archive. At most 5000 chats are scanned; chats outside the granted scope, secret chats, Saved Messages and 777000 are left out, so fewer than limit can come back. Errors: denied, unavailable.

Permission: chat.info. No bucket.

freshgram.chats.info(chat)
{"op": "chat.info", "a": {"chat": 123}}
Argument Type Required
chat int yes

Result: one chat object. Errors: denied (permission; scope; 777000), invalid (“chat”), not_found (unknown, secret, Saved Messages), unavailable.

Mark a chat as read. Permission: messages.send. Throttle: action bucket.

freshgram.chats.mark_read(chat)
{"op": "chats.mark_read", "a": {"chat": 123}}

Result: null. Errors: denied, invalid, not_found, limit (action bucket), unavailable. If the app itself refuses, the call answers with the code the app reports (usually failed) and the fixed message “the chat was not marked as read”.

Show “typing…” and similar actions. Permission: messages.send. Per-chat limit: one call per 3 s per chat (cancel exempt). No action bucket.

freshgram.chats.action(chat, kind="typing", topic=0)
freshgram.chats.action(chat, kind="cancel")
{"op": "chats.action", "a": {"chat": 123, "kind": "typing", "topic": 0}}
Argument Type Required Default Validation
chat int yes chat argument rules
kind string no typing typing, upload_photo, upload_video, upload_file, record_voice, upload_voice, record_video, record_round, choose_sticker, cancel. A kind that is not text, or longer than 32 bytes, silently becomes typing; any other text gives invalid
topic int no 0 >= 0 (invalid “topic”)

Result: null. cancel stops the typing action. The 3 s timestamp is stored before the kind is validated, so an invalid kind also uses up the 3 s window. Errors: denied, invalid, not_found, limit (“chat actions are sent too often”), unavailable. The broker does not check which kind names exist: an unknown kind is rejected by the app, which answers with its own code and the fixed message “the action was not sent” (so kind problems can show up as invalid with that message).

Permissions: chat.info and users.info (both). Throttle: members bucket (1 per second, shared by both). Wait: 10 s.

freshgram.chats.members(chat, limit=50, offset=0, query=None)
freshgram.chats.admins(chat, limit=50, offset=0)
{"op": "chats.members", "a": {"chat": 123, "limit": 50, "offset": 0, "query": "ann"}}
{"op": "chats.admins", "a": {"chat": 123}}
Argument Type Required Default Validation
chat int yes chat argument rules
limit int no 50 clamped to 1..200
offset int no 0 0 to 1000000 (invalid “offset”)
query string members only none 1 to 256 bytes; searches name and username; ignored by admins

Result: array of {"id", "name", "username", "role", "bot"}. role is member, admin or creator; in channel searches banned and left can also appear. Entries that are not loaded users, or that are hidden (secret), are skipped, so the list can be shorter than limit. Channels and supergroups need the right to view members; basic groups are read from the full chat and filtered in the app; a private chat is not_found.

Code When
denied missing chat.info or users.info; scope; the account cannot see the member list of that channel
invalid query empty/too long/not text; offset out of range
not_found chat unknown/secret/Saved; not a group or channel
limit members bucket
failed server error
unavailable no answer

Permission: chats.moderate. Throttle: moderation bucket (1 per second, shared). Requires a user action within the last 10 s.

freshgram.chats.ban(chat, user)
freshgram.chats.unban(chat, user)
{"op": "chats.ban", "a": {"chat": 123, "user": 777}}
Argument Type Required Validation
chat int yes chat argument rules
user int yes > 0; not 777000; not yourself (denied “this user”)

Result: null. The request to Telegram is sent without waiting; a server refusal is not reported. If the app itself refuses, the call answers with the code the app reports (usually failed) and the fixed message “the member was not changed”.

Code When
denied no chats.moderate; scope; the user is you or 777000; no user action in the last 10 s (“only right after a user action”); you are not allowed to ban members in that chat
invalid user missing or <= 0; unban in a basic group (only channels and supergroups can be unbanned)
not_found chat or user not known to the app, hidden, or the chat is neither a group nor a channel
limit moderation bucket

Permission: chats.manage. No bucket.

freshgram.chats.mute(chat, seconds=3600)
freshgram.chats.mute(chat, forever=True)
freshgram.chats.mute(chat) # unmute
{"op": "chats.mute", "a": {"chat": 123, "seconds": 3600}}
Argument Type Required Default Validation
chat int yes chat argument rules
seconds int no 0 0 to 31,622,400 (366 days); 0 with forever false unmutes
forever bool no false not together with a non-zero seconds

Result: null. Errors: denied, invalid (“seconds or forever”), not_found, unavailable. If the app itself refuses, the call answers with the code the app reports (usually failed) and the fixed message “the chat was not changed”.

Permission: chats.manage. No bucket.

freshgram.chats.archive(chat, archived=True)
freshgram.chats.pin(chat, pinned=True)
{"op": "chats.archive", "a": {"chat": 123, "archived": true}}
{"op": "chats.pin", "a": {"chat": 123, "pinned": true}}
Argument Type Required Default
chat int yes
archived (archive) / pinned (pin) bool no true

Result: null. chats.pin works on the main list; pinning an already pinned chat succeeds; when the pinned list is full it answers limit. chats.archive does not wait for the server. Errors: denied, invalid (“archived or pinned”), not_found, limit (pin list full), unavailable. If the app itself refuses, the call answers with the code the app reports (usually failed) and the fixed message “the chat was not changed”.

(messages.pin and messages.unpin, which pin a single message, are in Operations — messages.)


Key Type Notes
id, name, username int, string, string
bot, premium, contact, self bool
status string bot, online, recently, week, month, long_ago, offline, hidden
last_seen int unix time of the last online moment, 0 if unknown
has_photo bool

Users operations take the user id, not a chat; they are not filtered by the chat scope. Secret peers and the service user 777000 are never returned.

Permission: users.info. No bucket.

freshgram.users.info(user)
{"op": "users.info", "a": {"id": 777}}
Argument Type Required Validation
id int yes > 0 (invalid); 777000 gives denied (“this user”)

Result: user object. Only users the app already knows are found: not_found otherwise (also for a secret peer or an id that is a chat). Errors: denied, invalid, not_found, unavailable.

Find a user by username. Permission: users.info. Throttle: resolve bucket (1 per second, shared with channels.resolve). Wait: 10 s.

freshgram.users.resolve("@durov")
{"op": "users.resolve", "a": {"username": "durov"}}
Argument Type Required Validation
username string yes up to 64 bytes; after removing one leading @, 4 to 32 characters of A-Z a-z 0-9 _ (invalid “username”)

Result: user object. Errors: denied (permission; the result is 777000), invalid, not_found (no such username, or it is not a user), limit (resolve bucket), unavailable.

Current profile photo (full size). Permission: users.info. Throttle: action bucket. Wait: up to 40 s.

freshgram.users.photo(user)
{"op": "users.photo", "a": {"id": 777}}

Result: {"mime": "image/jpeg" | "video/mp4", "data": bytes}. A video avatar comes as MP4. Errors: denied (permission; 777000), invalid (id <= 0), not_found (not a known user, secret, or no photo), limit (over 100 MiB, or the action bucket), failed (not loaded within 30 s), unavailable.

Permission: contacts.manage. No bucket.

freshgram.users.block(user)
freshgram.users.unblock(user)
{"op": "users.block", "a": {"id": 777}}

Argument id (int, > 0, not 777000). Result: null. Errors: denied (permission; 777000), invalid, not_found (user unknown to the app, secret, or yourself), unavailable. The request is sent without waiting. If the app itself refuses, the call answers with the code the app reports (usually failed) and the fixed message “the user was not changed”.


{"id", "title", "username", "type", "about", "verified", "scam", "fake", "has_photo"} plus "members" (omitted if the count is unknown). type is channel, supergroup or forum. All channels.* operations accept supergroups and forums too.

Permission: chat.info. No bucket. Wait: 10 s (waits up to 3 s for the full channel info, then returns what is known: about can be empty and members missing).

freshgram.channels.info(channel)
{"op": "channels.info", "a": {"id": 1001}}
Argument Type Required Validation
id int yes > 0; outside the scope or 777000 gives denied (“this channel”)

Errors: denied, invalid, not_found (not a channel or supergroup, unknown, or secret), unavailable.

Permission: chat.info. Throttle: resolve bucket (shared with users.resolve).

freshgram.channels.resolve("@telegram")
{"op": "channels.resolve", "a": {"username": "telegram"}}

username as for users.resolve. Result: channel object. Errors: denied (permission; the channel is out of scope or 777000), invalid, not_found, limit, unavailable.

Permission: chat.info. Throttle: action bucket. Same result as users.photo ({"mime", "data"}), and the same errors. The channel must be in scope (denied “this channel”).

freshgram.channels.photo(channel)
{"op": "channels.photo", "a": {"id": 1001}}

Permission: contacts.list. No bucket.

freshgram.contacts.list(limit=200)
{"op": "contacts.list", "a": {"limit": 200}}
Argument Type Default Validation
limit int 200 clamped to 1..1000

Result: array of {"id", "name", "username"}; yourself, 777000 and users outside the chat scope are left out (after the limit is applied, so the list can be shorter). Errors: denied, unavailable.

Add a user the app already knows to your contacts. Permission: contacts.manage. No bucket. Wait: 100 s.

freshgram.contacts.add(user, "Bob", last_name="")
{"op": "contacts.add", "a": {"id": 777, "first_name": "Bob", "last_name": ""}}
Argument Type Required Default Validation
id int yes > 0; not 777000 (denied)
first_name string yes 1 to 64 bytes
last_name string no “” up to 64 bytes

Result: null. Errors: denied, invalid, not_found (user unknown, hidden, or a bot), failed (server error), unavailable.

Permission: folders.list. No arguments, no bucket.

freshgram.folders.list()
{"op": "folders.list", "a": {}}

Result: array (at most 100) of {"id": int, "title": string, "chats": [int]}. Each folder lists up to 500 loaded chats, without secret chats, Saved Messages, 777000 and chats outside the scope. The default “All chats” folder is not included. Errors: denied, unavailable.


All three need messages.send (searching is treated as part of sending).

Send an installed or recent sticker by id. Throttle: send bucket.

sent = freshgram.stickers.send(chat, sticker_id, reply_to=0, topic=0, silent=False)
{"op": "stickers.send", "a": {"chat": 123, "id": 5368324170671202286, "silent": false}}
Argument Type Required Default Validation
chat int yes invalid, then denied, then not_found
id int yes > 0; must be in the recent stickers or an installed set
reply_to, topic int no 0 >= 0
silent bool no false

Result: {"local_id": N} (never an id: the call does not wait for the server, even with messages.ids). Errors: denied (permission; scope; stickers restricted in that thread; the chat charges Stars per message), invalid, not_found (chat; the sticker is not known; reply not cached), limit (send bucket; slow mode), unavailable. If the app itself refuses, the call answers with the code the app reports (usually failed) and the fixed message “the sticker was not sent”.

Installed stickers for an emoji. No bucket.

freshgram.stickers.search("😀", limit=20)
{"op": "stickers.search", "a": {"emoji": "😀", "limit": 20}}
Argument Type Required Default Validation
emoji string yes 1 to 64 bytes (invalid “emoji”); a value that is not an emoji gives an empty list
limit int no 20 clamped to 1..100

Result: array of {"id", "emoji", "set", "animated", "video"}.

Recently used stickers. Argument limit (default 20, clamped 1..100); same result as stickers.search.

freshgram.stickers.recent(20)
{"op": "stickers.recent", "a": {"limit": 20}}

Ask an inline bot; nothing is sent to any chat. Permission: bots.inline. Throttle: inline bucket (1 per second, shared with bots.press_button). Wait: 10 s.

results = freshgram.bots.inline_query("@gif", "cats", chat=None)
{"op": "bots.inline_query", "a": {"bot": "gif", "query": "cats", "chat": 123}}
Argument Type Required Validation
bot string or int yes a username (optional @, 4 to 32 characters of A-Z a-z 0-9 _) or a positive user id; 777000 gives denied (“this bot”)
query string yes up to 256 bytes (may be empty)
chat int no context chat, with the usual chat argument rules (scope, hidden chats)

Result: array (at most 50) of {"query_id": int, "id": string, "title", "description", "text"}.

Code When
denied no bots.inline; the id form is 777000; chat out of scope
invalid bot missing or malformed, query missing or too long (“bot or query”); chat not an integer
not_found the username does not resolve; the user is not a bot, is hidden or is blocked; the id is not a known user; chat unknown/secret
limit inline bucket
failed the bot has no inline mode, or the request failed
unavailable no answer in 10 s

Send a result of bots.inline_query to a chat. Permissions: bots.inline and messages.send. No send bucket and no inline bucket (only the global call limit applies). Fire-and-forget.

freshgram.bots.send_inline_result(chat, results[0]["query_id"], results[0]["id"], reply_to=0, topic=0, silent=False)
{"op": "bots.send_inline_result", "a": {"chat": 123, "query_id": 99, "id": "r1"}}
Argument Type Required Default Validation
chat int yes chat argument rules
query_id int yes integer
id string yes 1 to 128 bytes
reply_to, topic int no 0 >= 0
silent bool no false

Result: {"local_id": null} (the Python SDK returns nothing). Errors: denied (either permission; scope; you cannot write there), invalid, not_found (chat; reply not cached), limit (slow mode), unavailable. A refusal by Telegram (expired query_id, …) is not reported. If the app itself refuses, the call answers with the code the app reports (usually failed) and the fixed message “the result was not sent”.

Press a button of a bot’s inline keyboard. Permission: bots.inline. Throttle: inline bucket. Wait: up to 100 s.

answer = freshgram.bots.press_button(chat, message, row=0, column=1)
{"op": "bots.press_button", "a": {"chat": 123, "id": 4567, "row": 0, "column": 1}}
Argument Type Required Validation
chat int yes chat argument rules
id int yes > 0 server id
row, column int yes >= 0

Result: {"text": string, "alert": bool, "url": string}. A callback button sends the callback and returns the bot’s answer; a URL button returns its url without any request. Buttons are listed in buttons of the message object.

Code When
denied no bots.inline; scope
invalid the ids/coordinates malformed; the button is neither a callback nor a URL button (for example “switch inline”, payments); chat not an integer
not_found chat unknown/secret/Saved; message not cached; no inline keyboard; row or column out of range
limit inline bucket
failed the bot or the server did not accept the callback (including buttons that need a password)

All three need composer.draft. They differ from other chat operations in one detail: a chat that is missing, not an integer, out of scope or 777000 gives denied (“this chat”), never invalid. A chat that is unknown, secret or Saved Messages gives not_found. No bucket; wait 1.5 s.

freshgram.composer.draft(chat)
{"op": "composer.draft", "a": {"chat": 123}}

Result: the plain text of the chat’s current draft ("" if none). Entities and the reply are not included.

freshgram.composer.set_draft(chat, "text")
{"op": "composer.set_draft", "a": {"chat": 123, "text": "text", "replace": false}}
Argument Type Required Default Validation
chat int yes see above
text string yes 0 to 4096 bytes; plain text only
replace bool no false must be a bool; a non-empty existing draft is replaced only with replace: true

Result: null. The cursor goes to the end; an existing reply of the draft is kept. Errors: denied, invalid (“text or replace”; “the draft is not empty, pass replace”), not_found, failed (“the draft was not set”), unavailable.

Insert text into the draft at the saved cursor, or at the end.

freshgram.composer.insert(chat, "text")
{"op": "composer.insert", "a": {"chat": 123, "text": "text"}}

Argument text: 1 to 4096 bytes (invalid “text”). Result: null; the cursor moves after the inserted text. Errors: denied, invalid, not_found, unavailable. If the app itself refuses, the call answers with the code the app reports (usually failed) and the fixed message “the text was not inserted”.


These have no arguments and no bucket.

Operation Permission Python Result
account.info account.info freshgram.account.info() {"id": int, "name": string, "username": string, "premium": bool} (the phone number is not exposed)
stars.balance stars.balance freshgram.stars.balance() returns the number {"balance": int} (whole Stars; waits up to 10 s for the balance to load)
settings.read settings.read freshgram.settings.read() {"language": string, "theme": "dark" | "light", "streamerMode": bool} (note the camelCase key)
{"op": "account.info", "a": {}}
{"op": "stars.balance", "a": {}}
{"op": "settings.read", "a": {}}

Errors for all: denied (permission), unavailable. stars.balance answers {"balance": 0} when the account is not available.

Permission: ghost.control (classified as a high-risk permission).

freshgram.ghost.get()
{"op": "ghost.get", "a": {}}

Result: {"active": bool, "send_read_messages": bool, "send_read_stories": bool, "send_online": bool, "send_typing": bool}. Errors: denied, not_found (no such account), unavailable.

freshgram.ghost.set(True)
freshgram.ghost.set(False) # only right after a user action
{"op": "ghost.set", "a": {"enabled": true}}
Argument Type Required Validation
enabled bool yes a bool, else invalid. Turning it off needs a user action within the last 10 s (denied “only right after a user action”); turning it on does not

Result: null. Errors: denied, invalid (“enabled”), not_found, unavailable. If the app itself refuses, the call answers with the code the app reports (usually failed) and the fixed message “ghost mode was not changed”.


Show a system notification. Permission: notifications (there is no permission called notify.show). Throttle: one per 5 seconds. No user action needed.

freshgram.notify.show("Backup done", title="My plugin")
{"op": "notify.show", "a": {"title": "My plugin", "text": "Backup done"}}
Argument Type Required Default Validation
text string yes 1 to 256 bytes
title string no “” up to 64 bytes; empty means the app name

Result: null. With native desktop notifications enabled it is a system notification; otherwise it is shown as an in-app toast “title: text”. Errors: denied, invalid (“title or text”), limit (“notifications are shown too often”), not_found, unavailable. If the app itself refuses, the call answers with the code the app reports (usually failed) and the fixed message “the notification was not shown”.


These are the replies of a plugin that declared contributes.translator / contributes.aiProvider. The request arrives as an event; the plugin answers with the matching operation using the event’s ticket. They are not tied to a chat and have no bucket.

Answer a translate.request event. Permission: translate.provide.

The request event: {"type": "translate.request", "ticket": N, "to": "<two-letter code>", "format": "plain", "items": [string, ...]} (1 to 20 items, together at most 24 KiB; at most 8 requests of a plugin wait at once; the ticket expires after 20 s).

@freshgram.on_translate
def translate(items, to):
return [text.upper() for text in items] # the SDK calls translate.reply for you
{"op": "translate.reply", "a": {"ticket": 5, "items": ["HELLO"], "format": "plain"}}
{"op": "translate.reply", "a": {"ticket": 5, "error": "language not supported"}}
Argument Type Required Default Validation
ticket int yes
items array of strings unless error each up to 32 KiB; up to 20 items; at most 96 KiB together
format string no plain plain, markdown, html; applied to every item (markup becomes entities)
error string no “” up to 200 bytes; a non-empty error fails the translation and items is ignored

Either a non-empty error or items must be given. When error is empty the number of items must equal the number of requested items.

Code When
denied no translate.provide
invalid missing ticket, items and error; an item not text or over 32 KiB; bad format; markup error or an item empty once its markup is stripped; the item count differs from the request (the ticket stays valid, you may answer again)
limit more than 20 items or over 96 KiB in total (“the answer is too large”)
not_found no such ticket: unknown, expired, already answered, or issued to another plugin (“no such translation”)
unavailable translation is not available in this host

An item answered as the empty string is counted as a failed item by the app.

Answer an ai.request event. Permission: ai.provide (high risk: it receives the text the user wants rewritten).

The request event: {"type": "ai.request", "ticket": N, "text": string, "format": "plain", "proofread": bool, "emojify": bool, "translate_to"?, "tone"?, "prompt"?}. At most 4 requests of a plugin wait at once; the ticket expires after 60 s.

@freshgram.on_ai
def rewrite(request):
return request["text"].strip() # the SDK calls ai.reply for you
{"op": "ai.reply", "a": {"ticket": 7, "text": "Result", "format": "plain"}}
{"op": "ai.reply", "a": {"ticket": 7, "error": "busy"}}
Argument Type Required Default Validation
ticket int yes
text string unless error “” up to 64 KiB
format string no plain plain, markdown, html
error string no “” up to 200 bytes; non-empty error fails the request and text is ignored

Errors: denied (no ai.provide), invalid (no ticket; error and text both empty; too long; bad format; markup error), not_found (“no such request”: unknown, expired, answered, or another plugin’s ticket), unavailable (no AI provider in this host).


Both need the permission plugins.message (on both plugins). No bucket.

Send data to another plugin.

freshgram.plugins.send("other.plugin", {"hello": 1})
{"op": "plugins.send", "a": {"to": "other.plugin", "data": {"hello": 1}}}
Argument Type Required Validation
to string yes 1 to 64 bytes; not this plugin’s own id (invalid “to”)
data any CBOR/JSON value no (default null) nesting depth at most 12, encoded size at most 64 KiB (limit “the message is too large”); an unencodable or too deep value gives invalid (“data”)

Result: null; delivery is asynchronous. The target receives {"type": "plugin.message", "from": "<sender id>", "data": ...}. Up to 1024 undelivered messages are queued in the host.

Code When
denied no plugins.message; the sender is not a registered plugin; the target does not accept messages from this plugin (contributes.messages.from). The message of the error is the target id
not_found no plugin with that id
invalid bad to or data
limit message over 64 KiB, or the host’s mail queue is full
unavailable plugin messages not available in this host
freshgram.plugins.peers()
{"op": "plugins.peers", "a": {}}

Result: array of plugin ids this plugin may send to (those that accept messages from it or from "*"). Errors: denied, unavailable.