Operations — messages
This page describes every messages.* operation exactly as the host implements it (broker.cpp and the app side in ayu/features/plugins/plugins_data.cpp). The chat, user, channel, bot and account operations are on Operations — chats, users and the rest. The short summary is in Call protocol (ops); the Python wrappers are in Python — messages and chats.
If two sources disagree (for example the Python docstring and the host), this page says what the code does.
Request and response
Section titled “Request and response”A request is {"op": "<name>", "a": {<arguments>}}; add "async": true for an awaited call (see Async). A response is one of:
{"ok": true, "v": <result>}{"ok": false, "e": "<code>", "m": "<message>"}Operations that return “nothing” answer {"ok": true, "v": null}. The Python SDK raises freshgram.Denied, Invalid, NotFound, QuotaExceeded, LimitExceeded, Unavailable or Failed for the codes (see Errors and limits). The Python examples below use the synchronous freshgram.*; freshgram.aio.* has the same methods.
Rules shared by all operations
Section titled “Rules shared by all operations”Order of checks
Section titled “Order of checks”Every call passes the same gates, in this order. The first failing gate answers:
- Request shape: no
opgivesinvalid(“no operation”);athat is not a map givesinvalid(“arguments must be a map”). - Global rate: at most
callsPerSecondcalls in a rolling second (default 200, counts every call, even failing ones) otherwiselimit(“too many calls”). - Async: with
async: truethe call is queued (see below); otherwise - Hook gate: while a message hook handler runs, every operation except
meta.info,log,time.nowand the four hook operations answersunavailable(“not while changing an outgoing message”). - Throttle of the operation (token buckets below). This happens before the permission and argument checks, so a call that is refused later still spends its token.
- Permission: a missing permission gives
deniedwith the permission id as the message. - App available: without the app side (for example in some test hosts)
unavailable(“data”). - Arguments and chat, then the app.
If the app thread does not start the work within 1.5 s, or a server request does not finish within its wait (see each operation), the call answers unavailable (“the app did not answer in time”).
Error codes used by messaging operations
Section titled “Error codes used by messaging operations”| Code | Typical meaning here |
|---|---|
denied |
permission missing (message is the permission id); chat outside the granted scope or the service chat 777000 (“this chat”); the action is not allowed in this chat or for this message (you cannot send there, not your message, edit window over); needs a recent user action |
invalid |
an argument is missing, has a wrong type, is out of range or malformed; bad markup |
not_found |
the chat is not known to the app, is a secret chat or Saved Messages; the message is not in the app’s cache or not on the server; a reply target is not in the cache |
limit |
a throttle bucket is empty; slow mode of the chat is active; something is too large |
unavailable |
the app did not answer in time, no app side, a hook is running, the call cannot be queued |
failed |
the server refused or the send/upload failed |
quota |
never returned by messaging operations (there is no daily or per-day quota on them; quota belongs to storage and badges) |
The chat argument, scope and hidden chats
Section titled “The chat argument, scope and hidden chats”chat is the numeric peer id as shown by chats.list and in events (for a private chat this is the user id). A value that is not an integer gives invalid with the argument name as the message.
For most operations the checks run in this order:
- integer, else
invalid; - the service chat
777000, or a chat outside the granted chat scope (the user picked “selected chats” for the plugin), givesdenied(“this chat”); - the app does not know the chat (it is not loaded), or the chat is a secret chat or Saved Messages, gives
not_found(“chat”).
Secret chats and Saved Messages are therefore never readable or writable by plugins; they look exactly like a chat that does not exist. (messages.send checks its other arguments before steps 2 and 3, see below.)
The same scope and hidden-chat rules apply to from_chat and to_chat of messages.forward. Deleted messages are returned only with messages.read_deleted; without it they are silently removed from lists and messages.get answers not_found.
Message ids
Section titled “Message ids”- A server id is a positive integer.
- A local id is what
messages.sendandmessages.send_mediareturn inlocal_id: a large negative integer, valid until the app restarts. It is accepted bymessages.edit,messages.edit_media,messages.delete,messages.react,messages.reactions. The host then waits (up to 90 s) until the message gets its server id; a message that failed to send answersfailed; one that is still not sent after the wait answersunavailable. A negative id that is not a local id givesinvalid. messages.get,messages.reply_chain,messages.pin,messages.unpin,messages.forward,messages.scheduled_deleteandbots.press_buttontake server ids only (invalidotherwise).- Id lists (
ids) hold 1 to 100 non-zero integers; duplicates are removed and the list is sorted ascending.
Text, formats and entities
Section titled “Text, formats and entities”| Rule | Value |
|---|---|
| Message text | up to 4096 bytes of UTF-8 (not characters) |
| Caption | up to 4096 bytes |
format |
"plain" (default), "markdown", "html"; any other value gives invalid |
| Markup | parsed by the host into text plus entities; bad markup gives invalid with the parser’s message; text that is empty once the markup is stripped gives invalid |
entities |
up to 256 objects {type, offset, length, ...}; offset >= 0, length > 0, in UTF-16 units |
Entity type |
bold, italic, underline, strike, code, pre, spoiler, blockquote, text_url, mention_name, custom_emoji; also accepted but never sent: url, mention, hashtag, cashtag, bot_command, email, phone |
| Entity fields | url (for text_url, required; must start with http://, https:// or tg://, up to 2048 bytes), language (for pre, up to 64 bytes), id (positive; required for custom_emoji and mention_name), collapsed (bool, only for blockquote) |
entities with a non-plain format |
invalid (“entities cannot be combined with a format”) |
An entity that does not fit in the text, or a mention_name whose user the app does not know, is skipped silently when sending.
Optional integer arguments (limit, reply_to, topic, schedule, before, duration, and so on) that have the wrong type are treated as absent and take the default; negative values are rejected where the table says so. Optional boolean arguments with a wrong type give invalid. Out-of-range limit values are clamped, not rejected.
Rate limits
Section titled “Rate limits”All buckets are per plugin and account (one broker). They are checked before permissions (see above).
| Bucket | Operations | Rule | Error |
|---|---|---|---|
| send | messages.send, stickers.send, messages.forward, messages.send_media, messages.edit_media, messages.send_poll |
burst of 5, refilled at 1 token per second | limit (“messages are sent too often”) |
| action | messages.edit, messages.delete, messages.react, chats.mark_read, users.photo, channels.photo |
burst of 20, refilled at 1 token per 200 ms (5 per second) | limit (“this is done too often”) |
| members | chats.members, chats.admins |
1 per second (shared) | limit |
| moderation | chats.ban, chats.unban |
1 per second (shared) | limit |
| resolve | users.resolve, channels.resolve |
1 per second (shared) | limit |
| inline | bots.inline_query, bots.press_button |
1 per second (shared) | limit |
| chat action | chats.action |
1 per 3 seconds per chat (cancel is exempt) |
limit |
| notify | notify.show |
1 per 5 seconds | limit |
| global | every call | callsPerSecond (200) per rolling second |
limit |
| pending | awaited calls waiting at once | 32 | limit (“too many calls are pending”) |
Every other messaging operation (messages.history, search, get, reply_chain, pinned, scheduled, scheduled_delete, pin, unpin, reactions, bots.send_inline_result, composer calls, …) has no bucket of its own. Slow mode of a chat is a separate limit that comes from the app.
User action window
Section titled “User action window”A “user action” is a UI event the user caused and the host delivered to the plugin: menu.click, badge.click, command, settings.* events, toast.click, dialog.click|change|long_click, window.click|change|long_click. Closing a dialog, window or toast (including a timeout) does not count. The window is 10 seconds from the latest such event. Messaging operations that need it: chats.ban, chats.unban and ghost.set with enabled: false; they answer denied (“only right after a user action”) otherwise. These calls do not consume the window, so several can be made inside the same 10 s.
Message object
Section titled “Message object”history, search, get, reply_chain, pinned, scheduled and media.list return message objects:
| Key | Type | Notes |
|---|---|---|
id |
int | server id (for scheduled, the scheduled id) |
chat |
int | |
from |
int | sender peer id |
from_kind |
string | user, bot, self, group, forum, supergroup, channel (absent in server-loaded entries) |
date |
int | unix time |
text |
string | original text (not the plugin-modified display) |
out |
bool | sent by you |
entities |
array | formatting entities, only if any |
sticker |
object | {id, emoji, set, animated, video} for sticker messages |
emoji_only |
bool | only present when true |
reactions |
array | {emoji | custom_emoji | paid, count, my} |
topic |
int | topic/thread root id, if any |
reply_to |
int | replied message id, if any |
media |
string | photo, video, gif, round, voice, audio, file, sticker, poll, contact, location, webpage, other |
deleted |
bool | only with messages.read_deleted, only when true |
buttons |
array of rows | {text, type: callback | url | other, url?} of an inline keyboard |
Messages that the app had to load from the server (older history, search, pinned, scheduled) are reduced to id, chat, from, date, text, out, reply_to, topic, media; from falls back to the chat id when the server gives no sender.
Async calls and hook-only calls
Section titled “Async calls and hook-only calls”Any operation on this page can be awaited with "async": true, except the four hook operations (messages.set_outgoing, messages.drop_outgoing, messages.set_incoming, messages.hide_incoming), which answer invalid (“this call cannot be awaited”). The immediate answer is {"ok": true, "v": {"ticket": N}}; the real answer later arrives as a call.result event with the same ticket and the same ok/v/e/m keys. Limits and permissions are applied when the call runs, so a limit or denied arrives in call.result. An answer larger than the message limit is replaced with limit (“the answer is too large”). If the plugin stops meanwhile the result is unavailable (“the plugin stopped”). Inside a hook handler an awaited call answers unavailable. See Async.
Operations that wait for the server (messages.send with messages.ids, messages.send_media, messages.edit_media, messages.edit, messages.delete, messages.react, messages.pin, bots.press_button, …) block the handler for the whole wait when called synchronously; prefer freshgram.aio.
Sending
Section titled “Sending”messages.send
Section titled “messages.send”Send a text message. Permission: messages.send; optionally messages.ids (changes the result). Throttle: send bucket.
sent = freshgram.messages.send(chat, "**hi**", markdown=True, reply_to=0, topic=0, silent=False, schedule=0){"op": "messages.send", "a": {"chat": 123, "text": "<b>hi</b>", "format": "html"}}| Argument | Type | Required | Default | Validation |
|---|---|---|---|---|
chat |
int | yes | see chat argument | |
text |
string | yes | 1 to 4096 bytes | |
format |
string | no | plain |
plain, markdown, html |
entities |
array | no | none | see entities; not with a non-plain format |
custom_emoji |
array | no | none | up to 100 {offset, length, id}; offset >= 0, length > 0, id > 0, each at most 4096; not with a non-plain format (use  or <emoji id=N> there); must lie inside the text, else invalid; Premium is decided by the server |
reply_to |
int | no | 0 | >= 0; a message id that must be in the app’s cache |
topic |
int | no | 0 | >= 0; forum topic or thread root id |
silent |
bool | no | false | |
schedule |
int | no | 0 | 0 sends now; otherwise a unix time later than now+10 s and at most 365 days ahead |
Result without messages.ids: {"local_id": N}. With messages.ids: {"id": N_or_null, "local_id": N}; the call then waits for the server (up to 90 s; the whole call is cut at 100 s). id is null when the server did not answer in time. A scheduled message is not tracked: local_id is null and id is null. local_id is also null if the app showed no message at all.
| Code | When |
|---|---|
denied |
no messages.send; chat out of scope or 777000; you cannot write there (banned, read-only channel, restricted media/text rights in that thread, …) |
invalid |
chat or text missing/empty/too long (“chat or text”); malformed custom_emoji or entities; format unknown, silent not a bool, negative reply_to/topic/schedule; markup error; schedule outside the allowed window; a custom emoji outside the text |
not_found |
chat unknown to the app, secret or Saved Messages; reply_to message not in the cache |
limit |
send bucket empty; slow mode of the chat is running; global limits |
unavailable |
the app did not answer; hook running |
failed |
only with messages.ids: the message failed to send (“the message was not sent”) |
Notes: arguments are validated before the scope check here, so a wrong argument in a hidden chat is invalid, not denied. The message is sent as the account’s chosen “send as” peer, if the chat requires a choice. A text message sent by a plugin passes the message.outgoing hooks of the plugins (a send made by a plugin is not exempt), unlike plugin-sent media, which skips the hooks.
messages.send_media
Section titled “messages.send_media”Send one file, photo, video or voice message. Permission: messages.send (and optionally messages.ids). Throttle: send bucket.
sent = freshgram.messages.send_media(chat, data, "photo.png", caption="**look**", markdown=True, kind="auto", spoiler=False){"op": "messages.send_media", "a": {"chat": 123, "data": "<bytes>", "name": "photo.png", "kind": "auto", "caption": "look"}}| Argument | Type | Required | Default | Validation |
|---|---|---|---|---|
chat |
int | yes | chat argument rules (checked first) | |
data |
bytes | yes | non-empty (invalid “data”); over 100 MiB gives limit (“the file is too large”) |
|
name |
string | yes | 1 to 128 bytes; no / or \ |
|
kind |
string | no | auto |
auto (photos and videos as media, other files as documents), file (always a document), voice |
caption |
string | no | “” | up to 4096 bytes |
format, entities |
no | for the caption, as for messages.send |
||
silent |
bool | no | false | |
spoiler |
bool | no | false | ignored for voice |
reply_to, topic |
int | no | 0 | >= 0, as for messages.send |
schedule |
int | no | 0 | as for messages.send |
duration |
int | no | 0 | 0 to 3600 seconds; required (non-zero) for voice; ignored otherwise |
Result: same as messages.send ({"local_id"} or {"id", "local_id"} with messages.ids). The file is written to a temporary outbox folder (kept at most one hour). The voice file is not inspected: the host does not check that it is OGG/Opus, and a caption is not used for voice.
| Code | When |
|---|---|
denied |
no messages.send; scope; chat charges Stars per message; you may not send this kind there (photos, videos, music, files or voice messages restricted) |
invalid |
bad name, kind, caption, flags, negative numbers, duration out of 0..3600 or missing for voice; markup error; schedule outside the window; the app cannot read the file as media |
not_found |
chat unknown/secret/Saved; reply message not cached |
limit |
file over 100 MiB; send bucket; slow mode |
failed |
the temporary file could not be written; with messages.ids the upload/send failed |
unavailable |
the app did not answer |
Plugin-sent media skips the outgoing hooks (the app does not pass it through message.outgoing).
messages.send_poll
Section titled “messages.send_poll”Send a poll. Permission: messages.send. Throttle: send bucket.
freshgram.messages.send_poll(chat, "Lunch?", ["Pizza", "Sushi"], anonymous=True, multiple=False, quiz=False, correct=None, explanation="", close_period=0){"op": "messages.send_poll", "a": {"chat": 123, "question": "Lunch?", "options": ["Pizza", "Sushi"], "quiz": true, "correct": 0}}| Argument | Type | Required | Default | Validation |
|---|---|---|---|---|
chat |
int | yes | chat argument rules | |
question |
string | yes | host: 1 to 1024 bytes; the app additionally rejects more than 255 characters after trimming | |
options |
array of strings | yes | 2 to 10 entries, each non-empty, host: up to 400 bytes; the app additionally rejects more than 100 characters per option | |
anonymous |
bool | no | true | |
multiple |
bool | no | false | not allowed together with quiz |
quiz |
bool | no | false | |
correct |
int | quiz: yes | -1 | for a quiz: 0 <= correct < len(options); for a non-quiz it must be absent (-1) |
explanation |
string | no | “” | host: up to 800 bytes; the app rejects more than 200 characters; only for a quiz |
close_period |
int | no | 0 | 0 (never) or 5 to 600 seconds |
reply_to, topic |
int | no | 0 | >= 0 |
silent |
bool | no | false |
Result: {"local_id": null} (a poll is not tracked and cannot be awaited for an id). The Python SDK returns nothing. The send is fire-and-forget: a server refusal is not reported.
| Code | When |
|---|---|
denied |
no messages.send; scope; polls not allowed in that thread |
invalid |
any argument above out of range (“question, options or flags”, “options”, “correct or multiple”, “correct or explanation”); the app-side character limits |
not_found |
chat unknown/secret/Saved; reply not cached |
limit |
send bucket; slow mode |
Editing, deleting, reacting, forwarding
Section titled “Editing, deleting, reacting, forwarding”messages.edit
Section titled “messages.edit”Edit the text of a message you sent. Permission: messages.edit. Throttle: action bucket. Wait: up to 100 s (a local id may need to wait for the server id).
freshgram.messages.edit(chat, message, "new text", markdown=False, entities=None){"op": "messages.edit", "a": {"chat": 123, "id": 4567, "text": "new text"}}| Argument | Type | Required | Default | Validation |
|---|---|---|---|---|
chat |
int | yes | chat argument rules | |
id |
int | yes | non-zero server or local id | |
text |
string | yes | 1 to 4096 bytes (the text cannot be cleared) | |
format, entities |
no | plain |
as for messages.send |
Result: null.
| Code | When |
|---|---|
denied |
no messages.edit; scope; the message is not yours, or Telegram’s edit window has passed |
invalid |
id zero or a negative non-local id, empty/too long text, bad format/entities, markup error |
not_found |
chat unknown/secret/Saved; message not in the cache; a local id that is gone |
limit |
action bucket |
failed |
the server refused the edit; the local message failed to send |
unavailable |
local id still unsent after the wait; the app did not answer |
messages.edit_media
Section titled “messages.edit_media”Replace the file of a message you sent. Permission: messages.edit. Throttle: send bucket. Wait: up to 5 minutes (upload timeout 270 s, then failed).
freshgram.messages.edit_media(chat, message, data, "new.png", caption="", kind="auto", markdown=False, spoiler=False){"op": "messages.edit_media", "a": {"chat": 123, "id": 4567, "data": "<bytes>", "name": "new.png"}}| Argument | Type | Required | Default | Validation |
|---|---|---|---|---|
chat |
int | yes | chat argument rules | |
id |
int | yes | non-zero server or local id | |
data |
bytes | yes | non-empty; over 100 MiB gives limit |
|
name |
string | yes | 1 to 128 bytes, no / or \ |
|
kind |
string | no | auto |
auto or file (voice is rejected as invalid) |
caption, format, entities |
no | “” | the new caption replaces the old one; an empty caption clears it | |
spoiler |
bool | no | false |
Result: null, returned after Telegram accepted the new file. “Message not modified” counts as success.
| Code | When |
|---|---|
denied |
no messages.edit; scope; not your message, edit window over, the message cannot take new media, another media edit is running, the chat forbids that file kind, or Telegram answers one of its edit errors |
invalid |
bad arguments; the file is a sticker; the file type does not fit the album the message belongs to; the app cannot read it as media; Telegram answers MEDIA_NEW_INVALID |
not_found |
chat/message unknown; the message vanished during the upload |
limit |
file over 100 MiB; send bucket |
failed |
the upload failed or timed out; other server errors |
messages.delete
Section titled “messages.delete”Delete messages you sent. Permission: messages.delete. Throttle: action bucket. Wait: up to 100 s.
freshgram.messages.delete(chat, [sent, 4567], everyone=True){"op": "messages.delete", "a": {"chat": 123, "ids": [4567, -288230376151711743], "everyone": true}}| Argument | Type | Required | Default | Validation |
|---|---|---|---|---|
chat |
int | yes | chat argument rules | |
ids |
array of int | yes | 1 to 100 non-zero ids; server or local ids | |
everyone |
bool | no | true | wrong type gives invalid |
Result: null. everyone is a request: if any of the messages cannot be deleted for everyone, all are deleted only for you, without an error. Every message must be yours and deletable; one failure aborts the whole call before anything is deleted.
| Code | When |
|---|---|
denied |
no messages.delete; scope; one of the messages is not yours or cannot be deleted |
invalid |
ids empty, over 100 entries, with zeros or non-integers; negative ids that are not local |
not_found |
chat unknown/secret/Saved; any message not in the cache |
limit |
action bucket |
failed, unavailable |
a local id that failed or is still unsent; no answer |
messages.react
Section titled “messages.react”Set or remove your reaction. Permission: messages.react. Throttle: action bucket. Wait: up to 100 s.
freshgram.messages.react(chat, message, emoji="👍") # setfreshgram.messages.react(chat, message, custom_emoji=5368324170671202286)freshgram.messages.react(chat, message) # remove yours{"op": "messages.react", "a": {"chat": 123, "id": 4567, "emoji": "👍"}}| Argument | Type | Required | Default | Validation |
|---|---|---|---|---|
chat |
int | yes | chat argument rules | |
id |
int | yes | non-zero server or local id | |
emoji |
string | no | none | up to 64 bytes; empty string = remove |
custom_emoji |
int | no | none | > 0; not together with emoji |
With neither emoji nor custom_emoji (or emoji: "") all your non-paid reactions on the message are removed. Setting a reaction that is already yours does nothing and succeeds. Paid (Stars) reactions cannot be set. Result: null. The host does not check that the reaction is allowed in the chat; the app decides, and if it refuses, the call answers with the code the app reports (for example failed) and the fixed message “the reaction was not set”.
| Code | When |
|---|---|
denied |
no messages.react; scope; the message cannot be reacted to in this chat |
invalid |
id zero; both emoji and custom_emoji; custom_emoji <= 0; emoji too long or not text |
not_found |
chat/message unknown |
limit |
action bucket |
failed (or another code from the app) |
the app refused: message “the reaction was not set” |
messages.reactions
Section titled “messages.reactions”List who reacted. Permission: messages.read (not messages.history). No bucket. Wait: 10 s.
freshgram.messages.reactions(chat, message, emoji=None, custom_emoji=0, limit=50){"op": "messages.reactions", "a": {"chat": 123, "id": 4567, "emoji": "👍", "limit": 50}}| Argument | Type | Required | Default | Validation |
|---|---|---|---|---|
chat, id |
int | yes | as for messages.react |
|
emoji / custom_emoji |
no | all | filter; not together | |
limit |
int | no | 50 | clamped to 1..100 |
Result: array of {"user": int, "name": string, "date": int, "emoji" \| "custom_emoji" \| "paid"}. user is a peer id (it can also be a channel). Errors: denied (permission, scope), invalid (as for messages.react), not_found (chat, message, or a local/service message that has no reactions list), failed (server error), unavailable.
messages.forward
Section titled “messages.forward”Forward messages. Permissions: messages.send and messages.read. Throttle: send bucket.
freshgram.messages.forward(from_chat, [101, 102], to_chat, silent=False){"op": "messages.forward", "a": {"from_chat": 1, "ids": [101, 102], "to_chat": 2, "silent": false}}| Argument | Type | Required | Default | Validation |
|---|---|---|---|---|
from_chat, to_chat |
int | yes | chat argument rules for both (from_chat first) |
|
ids |
array of int | yes | 1 to 100 positive server ids (the Python SDK passes the list as it is: pass numbers, not Sent) |
|
silent |
bool | no | false |
Result: null. Source messages are looked up only in the app’s cache; the host does not load them from the server. Slow mode is not checked.
| Code | When |
|---|---|
denied |
missing either permission (the message names the first missing one); scope for either chat; you cannot send to the target; a message is deleted or forbids forwarding |
invalid |
ids malformed; silent not a bool; a non-integer chat |
not_found |
either chat unknown/secret/Saved; any message not in the cache |
limit |
send bucket |
failed (or another code from the app) |
the app refused: message “the messages were not forwarded” |
Reading
Section titled “Reading”All reading operations below need messages.history (except messages.reactions, see above), have no bucket, and follow the chat rules and deleted-message rules above.
messages.history
Section titled “messages.history”The latest messages of a chat, newest first.
freshgram.messages.history(chat, limit=20, before=0, topic=0, from_user=None){"op": "messages.history", "a": {"chat": 123, "limit": 20, "before": 0, "from_user": "me"}}| Argument | Type | Required | Default | Validation |
|---|---|---|---|---|
chat |
int | yes | invalid, then denied (scope or 777000), then not_found (chat unknown, secret or Saved Messages) |
|
limit |
int | no | 20 | clamped to 1..100 |
before |
int | no | 0 | only messages with an id smaller than this; 0 = from the newest |
topic |
int | no | 0 | only messages of this topic or thread root |
from_user |
int or "me" |
no | all | a positive user id (not 777000) or the string "me"; anything else gives invalid (“from_user”) |
Result: array of message objects. The app answers from its cache first and loads the rest from the server (service messages, empty and unsent messages are skipped). Entries from the cache are full objects, entries loaded from the server are reduced (see Message object). With from_user the server-side part needs the sender to be known to the app, otherwise only cached messages come back. Server failures are not errors: the list is just shorter. Wait: 1.5 s for the cache, then 10 s for the server.
Errors: denied, invalid, not_found, unavailable.
messages.search
Section titled “messages.search”Server-side text search in one chat.
freshgram.messages.search(chat, "report", limit=20){"op": "messages.search", "a": {"chat": 123, "query": "report", "limit": 20}}| Argument | Type | Required | Default | Validation |
|---|---|---|---|---|
chat |
int | yes | chat argument rules | |
query |
string | yes | 1 to 256 bytes | |
limit |
int | no | 20 | clamped to 1..100 |
Result: array of reduced message objects, newest first. No paging. A failed search returns an empty list. Errors: denied, invalid (“query”), not_found, unavailable (10 s wait).
messages.get
Section titled “messages.get”One message by server id.
freshgram.messages.get(chat, 4567){"op": "messages.get", "a": {"chat": 123, "id": 4567}}| Argument | Type | Required | Validation |
|---|---|---|---|
chat |
int | yes | chat argument rules |
id |
int | yes | > 0 (local ids give invalid) |
Result: one message object (the app loads the message from the server if it is not cached). not_found also for service messages, for a message that belongs to another chat, and for deleted messages without messages.read_deleted. Errors: denied, invalid, not_found, unavailable.
messages.reply_chain
Section titled “messages.reply_chain”The messages a message replies to, nearest first.
freshgram.messages.reply_chain(chat, 4567, limit=5){"op": "messages.reply_chain", "a": {"chat": 123, "id": 4567, "limit": 5}}| Argument | Type | Required | Default | Validation |
|---|---|---|---|---|
chat |
int | yes | chat argument rules | |
id |
int | yes | > 0 |
|
limit |
int | no | 5 | clamped to 1..20 |
Result: array of full message objects (the starting message is not included). The walk stops quietly at a message that is missing, in another chat, deleted (without messages.read_deleted), or already seen (cycle). If the starting message itself cannot be loaded, the error of that load is returned (not_found, unavailable). Each step may wait up to 10 s.
messages.pinned
Section titled “messages.pinned”Pinned messages, newest first.
freshgram.messages.pinned(chat, limit=20){"op": "messages.pinned", "a": {"chat": 123, "limit": 20}}Arguments: chat (rules above), limit (default 20, clamped 1..100). Result: array of reduced message objects. Errors: denied, not_found, failed (server error), unavailable.
messages.scheduled
Section titled “messages.scheduled”Your scheduled messages in a chat.
freshgram.messages.scheduled(chat){"op": "messages.scheduled", "a": {"chat": 123, "limit": 100}}Arguments: chat, limit (default 20, clamped 1..100). Result: array of reduced message objects whose id is the scheduled id (use it for messages.scheduled_delete). Errors as for messages.pinned.
messages.scheduled_delete
Section titled “messages.scheduled_delete”Delete scheduled messages. Permission: messages.delete. No bucket. Wait: up to 100 s.
freshgram.messages.scheduled_delete(chat, [12, 13]){"op": "messages.scheduled_delete", "a": {"chat": 123, "ids": [12, 13]}}| Argument | Type | Required | Validation |
|---|---|---|---|
chat |
int | yes | chat argument rules |
ids |
array of int | yes | 1 to 100 positive scheduled ids |
Result: null. Errors: denied, invalid (“ids”), not_found (chat unknown/secret/Saved), failed (server error), unavailable.
Pinning
Section titled “Pinning”messages.pin and messages.unpin
Section titled “messages.pin and messages.unpin”Pin or unpin a message in the chat. Permission: chats.manage (not a messages permission). No bucket. Wait: up to 100 s.
freshgram.messages.pin(chat, 4567, silent=False)freshgram.messages.unpin(chat, 4567){"op": "messages.pin", "a": {"chat": 123, "id": 4567, "silent": false}}{"op": "messages.unpin", "a": {"chat": 123, "id": 4567}}| Argument | Type | Required | Default | Validation |
|---|---|---|---|---|
chat |
int | yes | chat argument rules | |
id |
int | yes | > 0 (server id) |
|
silent |
bool | no | false | applies to pin only; unpin does not use it but rejects a non-boolean value with invalid “id or silent” |
Result: null. Errors: denied (no chats.manage; scope; you cannot pin in this chat), invalid (“id or silent”), not_found (chat or message), failed (server error), unavailable.
Hook-only operations
Section titled “Hook-only operations”These four operations work only while a message hook handler is running and change the message being sent or received. They are available only in the synchronous freshgram.messages (not in aio), cannot be awaited (invalid “this call cannot be awaited”) and are the only messaging operations allowed inside a hook; every other operation answers unavailable there. See Message hooks for the manifest.
Facts about hooks that decide how these calls behave:
- The handler has 1000 ms per plugin. A plugin that is not
blockis skipped when it is slow, fails or is not ready; ablockplugin that fails, is slow or is not ready makes the app refuse the message and show a toast. - Edits count only if the handler finished successfully (exit code 0). Several plugins run in order and each sees the result of the previous one. Several edits in the same handler merge, and the last value of a field wins.
- Hooks are never called for secret chats, Saved Messages, or the service chat 777000.
- Outgoing hooks see text messages, including those sent by plugins, and media sends that the user makes (the text is the caption); media sent by a plugin skips them.
- Calling a hook operation outside its hook gives
invalid; with the permission missing it givesdeniedfirst.
messages.set_outgoing
Section titled “messages.set_outgoing”Change the message being sent. Permission: messages.transform_outgoing. Only in message.outgoing.
freshgram.messages.set_outgoing("**new**", markdown=True, silent=True, schedule=0, reply_to=0, topic=0){"op": "messages.set_outgoing", "a": {"text": "new", "silent": true}}| Argument | Type | Required | Validation |
|---|---|---|---|
text |
string | no | up to 4096 bytes; non-empty (an empty text is allowed only in the hook for a media send, where it clears the caption) |
format |
string | no | plain (default), markdown, html |
entities |
array | no | as above; not with a non-plain format (invalid “entities”) |
silent |
bool | no | |
schedule |
int | no | >= 0; 0 sends now; a time that is not in the valid window (later than now+10 s, at most 365 days) is ignored by the app |
reply_to |
int | no | >= 0; 0 removes the reply; a message that is not in the cache is ignored |
topic |
int | no | >= 0; a topic the user may not write to is ignored |
At least one of text, entities, silent, schedule, reply_to, topic must be present (invalid “nothing to change”; format alone does not count). When the text changes and no entities are given, the old entities are dropped. Result: null.
| Code | When |
|---|---|
denied |
no messages.transform_outgoing |
invalid |
not inside message.outgoing (“only while handling message.outgoing”); a bad argument (“text, format, silent, schedule, reply_to or topic”); bad entities; markup error; nothing to change |
unavailable |
called with async (inside a hook) |
messages.drop_outgoing
Section titled “messages.drop_outgoing”Do not send the message. Permission: messages.transform_outgoing. Only in message.outgoing.
freshgram.messages.drop_outgoing("not sending secrets"){"op": "messages.drop_outgoing", "a": {"reason": "not sending secrets"}}| Argument | Type | Required | Default | Validation |
|---|---|---|---|---|
reason |
string | no | “” | up to 200 bytes; a non-empty reason is shown to the user as a toast |
Result: null. The input field stays empty. Errors: denied, invalid (outside message.outgoing; bad reason).
messages.set_incoming
Section titled “messages.set_incoming”Change how a received message is displayed; the real text, forwarding, editing and other plugins’ events keep the original. Permission: messages.transform_incoming. Only in message.incoming, and not for service messages (invalid “a service message can only be hidden”).
freshgram.messages.set_incoming(text="…", markdown=True)freshgram.messages.set_incoming(entities=[{"type": "bold", "offset": 0, "length": 3}]){"op": "messages.set_incoming", "a": {"text": "...", "format": "markdown"}}| Argument | Type | Required | Validation |
|---|---|---|---|
text |
string | no | 1 to 4096 bytes |
format |
string | no | plain, markdown, html |
entities |
array | no | only with plain format (otherwise invalid); a new text without entities is shown unformatted |
At least text or entities is required (invalid “nothing to change”). Result: null. Errors: denied, invalid (outside message.incoming; service message; empty text; bad format/entities; markup error), unavailable.
messages.hide_incoming
Section titled “messages.hide_incoming”Hide the received message. Permission: messages.transform_incoming. Only in message.incoming; allowed for service messages too (if the hook declares service).
freshgram.messages.hide_incoming("spam"){"op": "messages.hide_incoming", "a": {"reason": "spam"}}| Argument | Type | Required | Default | Validation |
|---|---|---|---|---|
reason |
string | no | “” | up to 200 bytes |
Result: null. Errors: denied, invalid (outside message.incoming; bad reason).
Availability summary
Section titled “Availability summary”| Operation | Needs | Allowed in a hook | async |
|---|---|---|---|
every messages.* except the four below |
no hook | no (unavailable) |
yes |
messages.set_outgoing, messages.drop_outgoing |
message.outgoing hook |
yes | never (invalid) |
messages.set_incoming, messages.hide_incoming |
message.incoming hook |
yes | never (invalid) |
