Skip to content

Event payloads

Events lists which events exist and which permissions they need. This page lists what is inside each event. Every event is a CBOR map; in Python it is a dict. The shapes of ids, entities and messages are described in Data shapes.

Conventions used below:

  • int is a signed 64-bit integer, text is a UTF-8 string, bool a boolean, list an array.
  • “always” means the key is present; “optional” means the key is missing (not None) when there is no value, so read it with event.get("key").
  • Chat, user and message ids are plain integers; see id conventions.
  • Offsets and lengths in entities are counted in UTF-16 code units, not in Python characters or bytes.

The app builds the full event, then the host reduces it for your plugin:

Event kind Rule
Chat-bound subscription events (message.*, chat.*, call.*, story.new) Dropped when the chat is a secret chat, Saved Messages, the Telegram service chat (777000) or outside the chats the user granted. secret and self are removed from every one of them except message.* (there both are always false).
message.deleted without messages.read_deleted text and old_text (if the app sent it) are removed; entities stay. message.edited and the other message.* events are never reduced: they already need messages.read.
Targeted events (command, menu.click, badge.click) chat, topic, reply_to are removed when the chat is out of your scope. message, from, ids, text and old_text are also removed unless you have messages.read (and the chat is in scope). secret and self are always removed, so they never appear in the menu event tables either. Read these keys with event.get(...).
Hooks, timer, plugin.message, settings.*, dialog.*, window.*, toast.*, translate.request, ai.request, call.result Delivered as described, with no reduction.

The value passed to @freshgram.on_init (Wasm: the fg_init argument). It is not the freshgram.info() result.

Key Type Presence Meaning
account text always Id of the account this instance runs for (a decimal string such as "3"). A plugin runs once per account.
plugin text always The plugin id from the manifest.
version text always The installed plugin version.
api int always The host API level (currently 7).
@freshgram.on_init
def start(info):
freshgram.log("account %s, api %d" % (info["account"], info["api"]))

freshgram.info() (meta.info) answers {api, plugin, granted: [permission...], chats: "all" | [chat id...]}.

message.new, message.edited, message.deleted, message.reaction and message.pinned share one payload. Permission: messages.read. Service messages (a pin notice, a joined member), messages in Saved Messages and messages without a server id are never reported.

Key Type Presence Meaning
type text always The event name.
id int always Message id in the chat (server id).
chat int always Chat id.
chat_title text always Chat title or the user’s name.
chat_type text always user, bot, self, group, supergroup, forum, channel.
from int always Id of the sender peer. For channel posts it is the channel id; for an anonymous admin it is the chat id.
from_kind text always Kind of the sender, same values as chat_type.
sender_name text always Display name of the sender.
date int always Send time, unix seconds.
text text always Message text or media caption, empty if none. This is the real text, not what a message hook changed for display.
out bool always true for your own messages.
forwarded bool always true if the message is a forward. The origin of the forward is not reported.
secret bool always Always false here.
self bool always Always false here.
entities list optional Entities of the text; missing when there are none.
reply_to int optional Id of the message this one replies to. Missing when it is not a reply.
topic int optional Id of the topic root. 1 (the General topic) for every message outside a real topic, including ordinary chats.
grouped int optional Album id; messages of one album share it.
edit_date int optional Time of the last edit, unix seconds.
media text optional Media kind: photo, video, audio, voice, round, gif, sticker, file, poll, contact, location, webpage, other. Missing for a text-only message.
sticker map optional For a sticker: {id, emoji, set, animated, video}.
emoji_only bool optional true (only ever true) when the text consists of emoji only.
reactions list optional Current reactions: [{emoji | custom_emoji | paid, count, my}].
via_bot text optional Username of the inline bot the message was sent through.
local_id int optional Only on message.new for a message you sent from this app: the temporary id it had before the server confirmed it.
by_plugin bool optional Only true, only on message.new: the message was sent by a plugin (any plugin).
pinned bool optional Only on message.pinned: true after pinning, false after unpinning.

Differences per event:

Event When it arrives
message.new A message with a server id appeared: an incoming one, or your own one once the server confirmed it (then local_id is set and out is true).
message.edited The edit date or the text changed. An edit the app never saw the original of is reported only if it happened in the last 5 minutes.
message.deleted The message was deleted; reported once per message. Without messages.read_deleted there is no text.
message.reaction A new unread reaction appeared on a message (someone reacted to yours). reactions holds the full current summary; the reacting user is not in the event, ask messages.reactions.
message.pinned A pin or unpin that the app heard about while running; pinned tells which.
import freshgram
@freshgram.on("message.new")
def on_message(event):
if event["out"] or event["chat_type"] not in ("user", "group", "supergroup"):
return
if event.get("media") == "photo":
freshgram.log("photo from %s in %s" % (event["sender_name"], event["chat_title"]))
for entity in event.get("entities", []):
if entity["type"] == "url":
# offsets are UTF-16 units
raw = event["text"].encode("utf-16-le")
url = raw[entity["offset"] * 2:(entity["offset"] + entity["length"]) * 2].decode("utf-16-le")
freshgram.log("link: " + url)
freshgram.run()

Declared in contributes.outgoing / contributes.incoming; see Message hooks. They are delivered to the same @freshgram.on(...) handlers. Inside the handler only the hook calls (set_outgoing, drop_outgoing, set_incoming, hide_incoming), info, log and time work.

Permission: messages.transform_outgoing. The text is what is about to be sent (after earlier plugins in the chain changed it).

Key Type Presence Meaning
chat int always Target chat id.
chat_type text always user, bot, group, supergroup, forum, channel (never self: those sends are not offered to hooks).
text text always The text, or the caption of a file send.
entities list always Entities of the text; an empty list when there are none.
silent bool always Send without sound.
topic int optional Topic root id, missing when the send is not into a topic.
reply_to int optional Id of the replied message (a reply to the topic root itself is not reported).
scheduled int optional Scheduled send time, unix seconds.
media list optional Only for a file send and only if the hook declares media: [{kind, name, size}], kind is photo, video, audio or file, size in bytes.
@freshgram.on("message.outgoing")
def outgoing(event):
if "password" in event["text"].lower():
freshgram.messages.drop_outgoing("Looks like a password")
elif event["chat_type"] == "channel":
freshgram.messages.set_outgoing(silent=True)

Permission: messages.transform_incoming. The map is the message object (full form: id, chat, from, from_kind, date, out, reply_to, topic, media, sticker, emoji_only, reactions, buttons, …) plus:

Key Type Presence Meaning
chat_type text always Kind of the chat.
edited bool always true if the message was edited and is shown again.
service bool optional Only true: a service message (hooks with service: true only); text is then the service text, entities empty, and the message can only be hidden.
text text always Text as the previous hooks left it.
entities list always Entities, an empty list when none.

The event has no secret, chat_title, sender_name, forwarded or edit_date.

@freshgram.on("message.incoming")
def incoming(event):
if event["from_kind"] == "bot" and "casino" in event["text"].lower():
freshgram.messages.hide_incoming("spam")

All of these are subscription events and need the listed permission.

Permission chats.active. The user switched the active chat.

Key Type Presence Meaning
chat int always Chat id.
chat_title text always Chat title.
topic int optional Topic root id when a topic of a forum was opened.

Permission chats.activity. Repeats of the same action are merged: at most one event per 3 seconds for the same chat, user and action. Chats of blocked users are skipped.

Key Type Presence Meaning
chat int always Chat id.
user int always Who is doing it.
action text always typing, record_voice, upload_voice, record_video, upload_video, record_round, upload_photo, upload_file, choose_sticker, other.
cancelled bool always true when the user stopped (action is typing then).
topic int optional Topic root id.

Permission chats.activity. Only fires when the read marker moves forward. Inbox reads (outbox: false) are not reported while ghost mode is active.

Key Type Presence Meaning
chat int always Chat id.
read_till int always Id of the last message read.
outbox bool always true: the other side read your messages; false: you read theirs.

Permission chats.activity.

Key Type Presence Meaning
chat int always Group or channel id.
user int always Who joined or left.
joined bool always true joined, false left or was removed.

Permission chats.activity. Not sent for users.

Key Type Presence Meaning
chat int always Chat id.
title text always The new title.
old_title text optional Missing when the app had not seen the earlier title.

Permission users.presence. Not chat-bound. Your own status, bots, blocked users and hidden statuses are not reported; repeats of the same status are merged.

Key Type Presence Meaning
user int always User id.
status text always online, recently, week, month, long_ago, offline.
last_seen int optional Unix time the user was last online (or is online till); missing when unknown.
@freshgram.on_user_status
def status(event):
if event["status"] == "online":
freshgram.log("user %d is online" % event["user"])

Permission stories.read. The story itself is not passed.

Key Type Presence Meaning
chat int always The poster (a user or a channel).
story_id int always Story id.
date int always Time the app noticed it, unix seconds (not the post time).

Permission calls.activity. Only one-to-one calls with a user.

Key Type Presence Meaning
chat int always The other user’s id.
user int always The same id as chat.
incoming bool always true for an incoming call.
reason text call.ended only ended, busy, failed, other_device.
duration int call.ended only Seconds since the call was established, 0 if it never was.

call.incoming arrives only for incoming calls. call.ended arrives for both directions, once per call.

Permission app.activity. Only {type}. Not sent while ghost mode is active.

These need no subscription but need the matching UI permission.

Permission ui.command; the command must be declared in contributes.commands. Sent when the user types /name args in a chat and presses send (the text is not sent to the chat).

Key Type Presence Meaning
name text always The command name without the slash.
args text always The rest of the line, trimmed; empty if none.
chat int usually Chat id (removed when out of your scope).
topic int optional Topic root id when typed inside a topic.
reply_to int optional Id of the message the composer was replying to.

Permission ui.menu (ui.composer_button for composer); the item must be declared in contributes.menus. surface is one of message, chat, profile, composer, main_menu, tray, selection, shared_media. Composer buttons are menu items with surface: "composer"; there are no other composer events.

Key Type Presence Meaning
surface text always Where the item was clicked.
id text always The id from the manifest.
button text optional "right" for a right click on a composer button.
chat int message, chat, profile, composer, selection, shared_media Chat id.
topic int optional Topic root id (for message it is the message’s topic, 1 outside topics).
reply_to int composer Id of the message being replied to, if any.
message int message Id of the clicked message (needs messages.read).
from int message Sender id of that message (needs messages.read).
text text message Text of that message (needs messages.read).
ids list of int selection, shared_media Ids of the selected messages, at most 100 (needs messages.read).

main_menu and tray clicks carry only surface and id.

@freshgram.on_menu("copy-link", surface="message")
def copy_link(event):
if "message" in event:
freshgram.ui.toast("message %d: %d chars" % (event["message"], len(event["text"])))

Permission ui.badges. The user clicked a badge set with badges.set.

Key Type Presence Meaning
id text always Badge id.
user int always The peer the badge belongs to.
surface text always message, chat (chat header), profile, chats (chat list).
chat int message only Chat of the message.
message int message only Id of the message (needs messages.read).

Set by timers.set. Only {id: text}. Fires even if the plugin was unloaded.

Permission plugins.message.

Key Type Presence Meaning
from text always Id of the sending plugin.
data any always Whatever the sender passed to plugins.send.

settings.* needs ui.settings_page, dialog.* and window.* need ui.dialog.

Event Payload Meaning
settings.open {} The user opened the plugin’s settings. Send the page here with ui.settings_page.
settings.change / dialog.change / window.change id, value, window* An input item changed.
settings.click / dialog.click / window.click id, window* A button or list entry was pressed.
settings.long_click / dialog.long_click / window.long_click id, window* Long press or right click on an item that has long_click: true.
dialog.close {} The dialog was closed.
window.close window The window was closed.

* window (text, the window id) is present on all window.* events and absent on settings.* and dialog.*.

id is the item id from your page (text). The type of value depends on the item:

Item value
toggle bool
slider int
input text
select text, the chosen option value
tabs text, the id of the opened tab
chat int, the chosen chat id
@freshgram.on_setting("enabled")
def toggled(event):
freshgram.storage.set("enabled", event["value"])

Permission ui.toast, for a toast shown with action.

Event Keys Meaning
toast.click id (text) The button was pressed.
toast.close id (text), reason (text) The toast went away; reason is "action" after a press, "timeout" otherwise.

Permission translate.provide; the plugin must declare contributes.translator. Answer with translate.reply (the @on_translate decorator does it).

Key Type Presence Meaning
ticket int always Pass it back in translate.reply.
to text always Two-letter target language code.
format text always Always "plain".
items list of text always Up to 20 texts; answer with the same number of items.

Permission ai.provide; the plugin must declare contributes.aiProvider. Answer with ai.reply (@on_ai).

Key Type Presence Meaning
ticket int always Pass it back in ai.reply.
text text always The text to process, formatting removed.
format text always Always "plain".
proofread bool always Fix errors.
emojify bool always Add emoji.
translate_to text optional Two-letter language code to translate into.
tone text optional Requested tone, such as formal.
prompt text optional A free instruction from the user.

@on_ai hands your function the same dict without type, ticket and format.

The result of a call made with "async": true (all of freshgram.aio). The immediate reply to such a call is {ok: true, v: {ticket: N}}; later the event arrives. The Python SDK resolves the future for you; with the raw protocol match on ticket.

Key Type Presence Meaning
type text always "call.result".
ticket int always The ticket from the immediate reply.
ok bool always true on success.
v any when ok The same value the synchronous call would return.
e text when not ok Error code: denied, invalid, not_found, quota, limit, unavailable, failed.
m text when not ok A short explanation.

So the shape of v for each operation is exactly the result described in Data shapes; only the delivery differs. Operations whose v is empty (time.sleep, messages.edit, messages.delete, ui.toast, …) answer v: None. If the plugin was stopped before the answer, ok is false with e: "unavailable". A result larger than the message limit becomes e: "limit".

from freshgram import aio
async def last_text(chat):
history = await aio.messages.history(chat, limit=1) # call.result with v = list of messages
return history[0]["text"] if history else ""