Skip to content

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.

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.

Every call passes the same gates, in this order. The first failing gate answers:

  1. Request shape: no op gives invalid (“no operation”); a that is not a map gives invalid (“arguments must be a map”).
  2. Global rate: at most callsPerSecond calls in a rolling second (default 200, counts every call, even failing ones) otherwise limit (“too many calls”).
  3. Async: with async: true the call is queued (see below); otherwise
  4. Hook gate: while a message hook handler runs, every operation except meta.info, log, time.now and the four hook operations answers unavailable (“not while changing an outgoing message”).
  5. 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.
  6. Permission: a missing permission gives denied with the permission id as the message.
  7. App available: without the app side (for example in some test hosts) unavailable (“data”).
  8. 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”).

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)

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:

  1. integer, else invalid;
  2. the service chat 777000, or a chat outside the granted chat scope (the user picked “selected chats” for the plugin), gives denied (“this chat”);
  3. 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.

  • A server id is a positive integer.
  • A local id is what messages.send and messages.send_media return in local_id: a large negative integer, valid until the app restarts. It is accepted by messages.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 answers failed; one that is still not sent after the wait answers unavailable. A negative id that is not a local id gives invalid.
  • messages.get, messages.reply_chain, messages.pin, messages.unpin, messages.forward, messages.scheduled_delete and bots.press_button take server ids only (invalid otherwise).
  • Id lists (ids) hold 1 to 100 non-zero integers; duplicates are removed and the list is sorted ascending.
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.

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.

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.

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.

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.


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 ![alt](tg://emoji?id=N) 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.

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).

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

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

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

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

Set or remove your reaction. Permission: messages.react. Throttle: action bucket. Wait: up to 100 s.

freshgram.messages.react(chat, message, emoji="👍") # set
freshgram.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”

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.

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”

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.

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.

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).

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.

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.

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.

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.

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.


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.


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 block is skipped when it is slow, fails or is not ready; a block plugin 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 gives denied first.

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)

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).

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.

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).

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)