Skip to content

manifest.json — full reference

This page lists every key the host accepts in manifest.json, with its type, limits and default, taken from the manifest parser. For a gentler introduction see Manifest; for building the package see Packaging and the dev runner.

  • The manifest is a JSON document: UTF-8, no byte order mark, at most 256 KiB, nesting depth at most 16, at most 20 000 items.
  • Every object is checked strictly: an unknown key is an error. The only exception is a key that starts with x- — it is ignored at any level (use it for your own tooling). x- keys are not allowed inside files, and they are not accepted as language keys inside the language map of a Localized value either (x-ru is not a valid language code, so name: has a bad entry 'x-ru').
  • Validation collects errors (at most 50) and reports them as where: what, for example permissions.required: unknown permission 'foo.bar'. A package with any error is rejected.
  • manifest.json itself is not listed in files. The manifest parser does not notice a listing, but opening the package then fails with 'manifest.json' is listed but missing (see Checks made outside the manifest parser).
  • Empty strings are rejected for every string key. In particular author.name, author.contact, author.url, license, homepage, repository, support, updates.feed, icon and changelog must be left out rather than set to "" (the error is has a bad length, or must be a valid https URL for an empty URL). Empty texts inside a Localized map are rejected too.
Type Rules
Localized A plain string, or an object language → string. The object must contain default; at most 32 entries; every text is non-empty and within the key’s length limit. Language keys are default or a code of 2–12 characters: the first two are lowercase letters, the rest may be letters, digits or - (ru, pt-BR, zh-Hans). A plain string is stored as default.
URL A string up to 500 characters that must parse as an https URL.
Path A safe relative path, up to 200 characters, /-separated. Each segment is 1–128 printable ASCII characters (0x20–0x7E), is not . or .., does not start or end with a space, does not end with ., and contains none of \ : * ? " < > |.
Token Lowercase letters, digits and -.
Version range See Version ranges.

Localized lookup at run time: exact language (pt-BR), then the part before the first dash (pt), then default.

Key Type Required Limits / default
schema integer yes must be 1
id string yes see id
name Localized yes each text up to 80 characters
version string yes semantic version, up to 64 characters
description Localized no each text up to 1000 characters; no control characters except newline, no bidirectional controls. Shown in the install dialog; supports bold, italic, underline, strikethrough, code, pre, quote, spoiler and https:// links
author object no name (up to 100), url (URL), contact (up to 200); no other keys; none of the strings may be empty
license string no 1 to 64 characters (an empty string is an error)
homepage, repository, support URL no not empty
changelog Path no must be a file of the package (only the path and the files entry are checked, not the content)
icon Path no must be a file of the package; PNG, up to 256 KiB. The manifest parser checks only the path and the files entry; the PNG signature and the size are checked when the package is opened (details)
categories array of Token no up to 8 entries, each up to 32 characters
keywords array of Token no up to 16 entries, each up to 32 characters
locales array of language codes no up to 64 entries; the languages the plugin is translated to
compat object yes see compat
runtime object yes see runtime
activation object no see activation
permissions object no see permissions; default: none
contributes object no see contributes
dependencies array no up to 16, see dependencies and conflicts
conflicts array no up to 16
storage object no see storage
updates object no see updates
files object yes see files
signature object no see signature

A reverse-DNS name, 5–64 characters: lowercase a-z, digits 0-9, . and -. It must contain at least one .; it must not start or end with . or -, and two separators (. or -) must not be adjacent. Valid: com.example.hello, io.github.me.my-plugin. Invalid: hello (no dot), Com.Example.X (uppercase), com..x, com.example-.

The id is the plugin’s identity: it names its storage directory, is used in /command@plugin.id, and is what dependencies, conflicts and contributes.messages.from refer to.

MAJOR.MINOR.PATCH with an optional -prerelease. Numbers have no leading zeros and at most 9 digits. The pre-release part is up to 32 characters of letters, digits, . and -, without empty segments. Build metadata (+build) is not accepted. Comparison follows semver: a pre-release is lower than its release (1.0.0-beta.2 < 1.0.0), numeric pre-release parts compare as numbers and are lower than alphanumeric ones.

"compat": {
"freshgram": ">=7.5.0 <8.0.0",
"testedWith": "7.5.10",
"api": 7,
"requires": ["menu.message", "format.html"],
"accounts": { "premium": true }
}
Key Type Required Meaning
freshgram Version range yes Client versions the plugin works with
testedWith semver (up to 64 characters) no The client version you last tested. If the running client has a higher major, or the same major and a higher minor, the install dialog warns “Made for freshGram X and may not work correctly” (the plugin is still installable)
api integer or { "min", "max" } no A single number n means “level n or higher” (min = n, max = 1000); it must be 1–1000, otherwise compat.api: must be from 1 to 1000. The object form sets both; min/max are 1–1000, default 1/1000, and max must not be below min. The host currently has API level 7
requires array of strings no Up to 32 host features (each up to 64 characters of a-z 0-9 . _ -). The host offers every permission id, menu.<surface> for each menu surface, and format.html
accounts object no Only key: premium (boolean). true makes the plugin installable only on a Premium account

Install is refused (error “incompatible with this app”) with these details: needs freshGram <range>, was made for freshGram <range>, needs API level a to b, needs <feature>, needs a Premium account.

A range is a list of comparators separated by single or multiple spaces; all must hold (logical AND). At most 8 comparators, up to 128 characters.

Comparator Meaning
* any version
1.2.3 or =1.2.3 exactly this version
>1.2.3, >=1.2.3, <1.2.3, <=1.2.3 comparison

Every version in a comparator must be a full X.Y.Z[-pre]. Not supported: ^, ~, x/1.2 shorthand, ||, commas, hyphen ranges. Operators must be glued to the version (>= 1.0.0 with a space is read as two tokens and fails). Pre-releases are compared exactly as versions, with no special exclusion. Example: >=7.0.0 <8.0.0.

"runtime": { "kind": "python", "entry": "main.py", "pythonVersion": "3.14" }
"runtime": {
"kind": "wasm", "module": "plugin.wasm", "language": "rust", "abi": 1,
"limits": { "memoryMb": 64, "eventTimeoutMs": 500, "storageMb": 10 }
}
Key Type Applies to Rules
kind string both required, up to 16 characters: python or wasm
entry Path python required for python, must end in .py, must be listed in files. Error is only for python plugins on wasm
module Path wasm required for wasm, must end in .wasm, must be listed in files. Error is only for wasm plugins on python
pythonVersion string python optional, up to 32 characters; informational only. It is read and stored for python plugins, never compared with the installed interpreter, and for wasm plugins it is silently ignored (not even length-checked)
language string both up to 32 characters; a hint (rust, c, …)
abi integer both 1–1000, default 1; only 1 is supported
limits.memoryMb integer both 1–1024, default 64
limits.eventTimeoutMs integer both 10–60000, default 500
limits.storageMb integer both 0–1024, default 10; 0 means no storage (shown as such in the dialog)

The limits are shown to the user in the install dialog (“Limits: memory … · event time … · storage …”). Extra checks done when the package is opened:

  • Python: the entry file must be valid UTF-8 text without NUL bytes.
  • Wasm: the module may import only freshgram::call and a fixed list of wasi_snapshot_preview1 functions (anything else is rejected as imports '<module>::<name>', which is not allowed), and must export memory plus either fg_alloc, fg_init and fg_on_event (reactor), or _start (command). See the Wasm ABI.
"activation": { "on": ["command", "event", "menu"], "background": false }
Key Type Meaning
on array, up to 8 of: startup, command, event, menu, settings, message, timer, translate, ai, toast What may wake the plugin up. Default when on is absent: all of them except startup. If on is present, only the listed triggers are used
background boolean Default false. true requires the background permission

A plugin is started at app start (the startup trigger) if startup is in on, or background is true and the background permission is granted, or it declares an outgoing/incoming hook and the matching messages.transform_* permission is granted. Otherwise it is started lazily by the other triggers.

"permissions": {
"required": [
{ "id": "messages.read", "scope": "selected", "reason": { "default": "Counts messages.", "ru": "Считает сообщения." } }
],
"optional": [
{ "id": "net.local", "ports": [8080, "9000-9010"], "reason": "Talks to a local service." },
{ "id": "fs.folder", "mode": "read", "reason": "Reads your notes folder." }
]
}

Keys: required and optional, each an array of up to 64 objects. A permission id may appear once across both lists.

Key Type Rules
id string required, up to 64 characters, must be in the permission list
reason Localized required, each text up to 500 characters. Shown to the user
scope string only for messages.read: selected or all (1 to 16 characters; any other permission is an error). If it is omitted the permission is treated as all for the risk level; see Permissions
mode string required for fs.folder: read or write (up to 8 characters); an error for any other permission
ports array required for net.local (1–16 entries), an error for any other permission. Each entry is an integer port or a string range "low-high"; ports 1–65535, low <= high, at most 5 digits per number

required permissions are shown as always on in the install dialog; optional ones can be switched off by the user. Risk level per permission and the “user action” window are described in Permissions.

A permission with scope: "selected" is only granted together with a chat selection. If the host is asked to grant it with no chat selection at all, it is silently left out of the grants (no error), so messages.read then behaves as denied. The install dialog prevents this by refusing “selected chats” with no chats picked.

Cross-checks between permissions and the rest of the manifest

Section titled “Cross-checks between permissions and the rest of the manifest”

Each of these is a validation error if the matching permission is not declared (in required or optional):

If the manifest has It needs
an event in contributes.events the event’s permission (table below)
contributes.menus (not empty) ui.menu
a menu on the composer surface ui.composer_button (in addition to ui.menu)
contributes.commands (not empty) ui.command
contributes.settingsPage: true ui.settings_page
activation.background: true background
contributes.messages.from (not empty) plugins.message
contributes.translator translate.provide
contributes.aiProvider ai.provide
contributes.outgoing messages.transform_outgoing
contributes.incoming messages.transform_incoming

All keys are optional. Unknown keys are errors.

Key Type Limits
events array of strings up to 32
menus array of objects up to 32
commands array of objects up to 32
settingsPage boolean default false
outgoing object see below
incoming object see below
messages object { "from": [...] }
icons object up to 32 icons
translator object
aiProvider object

Each name must be a known event, and the manifest must declare its permission.

Event Needs permission
message.new, message.edited, message.deleted, message.reaction, message.pinned messages.read
chat.opened chats.active
chat.typing, chat.read, chat.member, chat.title_changed chats.activity
user.status users.presence
story.new stories.read
call.incoming, call.ended calls.activity
app.focus, app.blur app.activity

Chat events (message.*, chat.*, call.*, story.new) follow the plugin’s chat scope and never come from secret chats. Payloads: Events.

{ "surface": "message", "id": "count-words", "title": { "default": "Count words", "ru": "Посчитать слова" },
"icon": "stats", "when": { "chat_type": ["group", "supergroup"], "has_text": true } }
Key Type Rules
surface string required, up to 32 characters: message, chat, profile, composer, main_menu, tray, selection, shared_media
id Token required, up to 64 characters, lowercase letters, digits, - (no underscore)
title Localized required, each text up to 80
icon string optional; up to 32 characters of a-z 0-9 _ -; must be a built-in icon or a name from contributes.icons
when object optional, see below

Built-in icons: add archive block bot cancel chat chats clear collapse commands copy customize delete devices download edit emoji expand export file folder font forward gif hide hourglass info like link lock mark_read mute network night notifications palette permissions phone photo pin premium profile qr reactions remove reply report restore schedule search select send settings share show silent sound_off sound_on spoiler star stats stickers storage timer translate unmute unpin users zoom_in zoom_out.

when keys (all optional; unknown keys are errors; all given conditions must hold):

Key Type Surfaces Meaning
chat_type array of up to 16 of user, bot, self, group, supergroup, forum, channel message, selection, shared_media, chat, profile, composer Chat kind
out boolean message, selection, shared_media Outgoing (true) or incoming (false) message
has_media boolean same The message has media (a link preview does not count)
has_text boolean same The message has text
forwarded boolean same The message is forwarded
media array of up to 16 of photo, video, gif, round, voice, audio, file, sticker, poll, contact, location, webpage, other same Media kind

Using a key on a surface it does not apply to (for example has_text on tray, or any key on main_menu/tray) is an error does not apply to the <surface> surface.

{ "name": "hello", "description": { "default": "Says hello", "ru": "Здоровается" } }

name: required, 1–32 characters of a-z 0-9 _. description: Localized, optional, each text up to 200. Call a command of a specific plugin as /name@plugin.id if two plugins use the same name (the install dialog warns about collisions).

"outgoing": { "onFailure": "pass", "media": true, "filter": { "chat_types": ["user"], "chats": [123456] } },
"incoming": { "onFailure": "block", "mode": "hold", "service": false }
Key Hook Type Default Meaning
onFailure both pass or block pass What happens to the message if the plugin fails or times out
mode incoming swap or hold swap swap replaces the text the user sees; hold waits for the plugin’s answer before the message is shown
media outgoing boolean false Also run for file sends (the text is the caption)
service incoming boolean false Also see service messages (pins, joins); the plugin can only hide them
filter.chats both array of up to 256 non-zero integers all Only these chat ids
filter.chat_types both array of up to 32 chat types (as in when.chat_type) all Only these chat kinds

See Message hooks. Needs messages.transform_outgoing / messages.transform_incoming.

onFailure (both hooks) and mode (incoming) are read as strings of at most 8 characters; a longer or empty value is has a bad length, any other word than the ones above is onFailure must be 'pass' or 'block' / mode must be 'swap' or 'hold'.

"messages": { "from": ["com.example.other", "*"] }

from: up to 64 entries, each a plugin id or "*" (any plugin). Lists whose plugin.message the plugin accepts; needs plugins.message. The install dialog tells the user which plugins can send to it.

"icons": { "my-star": "assets/star.svg", "logo": "assets/logo.png" }

Map name → Path; names are 1–32 characters of a-z 0-9 _ -; the file must end with .png or .svg, must be listed in files; at most 32 icons. Keys starting x- are skipped. The names can then be used as icon of menu items.

The parser checks only the extension and the files entry. At run time the app silently ignores a custom icon file larger than 64 KiB (no error is reported and the icon is simply not available), so keep these files small; the 256 KiB limit applies to the package icon only.

"translator": { "name": "My Translate", "languages": ["en", "ru", "de"], "maxChars": 4096 }

name: required, up to 40. languages: up to 200 language codes. maxChars: 64–8192, default 4096. Needs translate.provide.

"aiProvider": { "name": "My Model", "maxChars": 4096 }

name: required, up to 40. maxChars: 64–8192, default 4096. Needs ai.provide.

"dependencies": [ { "id": "com.example.core", "version": ">=1.2.0 <2.0.0" } ],
"conflicts": [ { "id": "com.example.old" }, { "id": "com.example.other", "version": "<2.0.0" } ]

Up to 16 entries each; entries have only id and version. id must be a valid plugin id and not the plugin’s own. version is a version range, up to 128 characters; it is required in dependencies and optional in conflicts (without it, any version conflicts). How they are resolved is described in Publishing and updates.

"storage": {
"schema": 2,
"export": { "prefixes": ["settings.", "theme"], "exclude": ["settings.token"] }
}
Key Type Rules
schema integer 1–100000, default 1. The version of your stored data layout
export.prefixes array of strings required and non-empty if export is present; up to 32; each up to 128 characters, no control characters
export.exclude array of strings up to 32, same rules

A storage key is exported/imported when it starts with one of prefixes and with none of exclude. Declaring export enables the “Export settings” / “Import settings” buttons in the plugin’s details. See Publishing and updates.

"updates": { "feed": "https://example.com/my-plugin/updates.json" }

feed: URL. The value is validated and stored with the manifest. Note: the current host and app do not fetch it — there is no automatic update check; updates are installed by opening a newer .fgpl file (see Publishing and updates). The top-level key is updates, with the nested feed; there is no updatesFeed key.

"files": {
"main.py": "sha256-<base64 of the 32-byte digest>",
"assets/icon.png": "sha256-<base64>"
}

A map Path → hash of every file in the package except manifest.json: up to 4095 entries. The hash is sha256- followed by the standard Base64 (with padding) of the 32-byte SHA-256 of the file. The host rejects a package where a file is missing, unlisted, or does not match its hash. The runtime file (entry/module), icon, changelog and every contributes.icons file must be listed. You normally do not write this by hand: fgpl_tool pack computes it (Packaging).

"signature": { "alg": "ed25519", "publicKey": "<base64, 32 bytes>", "value": "<base64, 64 bytes>" }

All three keys are required if signature is present; alg must be ed25519; publicKey decodes to 32 bytes and value to 64 bytes (standard Base64). The signature is checked when the package is opened; an invalid one rejects the package. Details: Signing.

Length limits of the strings (checked before decoding): alg up to 16 characters, publicKey up to 100, value up to 200; an empty string is an error.

The parser (manifest.cpp) checks the shape of the manifest only. For icon and changelog it checks that the value is a safe relative path that appears in files; for contributes.icons it checks the .png/.svg extension and the files entry. It never looks at file contents or sizes and does not reject a files map that names manifest.json. The rest happens when the package is opened (package.cpp) or in the app:

Check Where Result
icon starts with the PNG signature and is at most 256 KiB opening the package the package is rejected: the icon must be a PNG of at most 256 KiB
files names manifest.json opening the package rejected: 'manifest.json' is listed but missing (the manifest is never a member of files)
a file of the archive is missing from files, its hash differs, or a listed file is not in the archive opening the package rejected
a contributes.icons file is larger than 64 KiB the app, when it loads the manifest silently not used, no error
changelog nowhere only the path and the files entry
{
"schema": 1,
"id": "com.example.hello",
"name": { "default": "Hello", "ru": "Привет" },
"version": "1.2.0",
"description": { "default": "Greets and counts messages.", "ru": "Здоровается и считает сообщения." },
"author": { "name": "Example Dev", "url": "https://example.com", "contact": "dev@example.com" },
"license": "MIT",
"homepage": "https://example.com/hello",
"repository": "https://github.com/example/hello",
"icon": "assets/icon.png",
"categories": ["utilities"],
"keywords": ["hello", "counter"],
"locales": ["en", "ru"],
"compat": { "freshgram": ">=7.5.0 <8.0.0", "testedWith": "7.5.10", "api": 7 },
"runtime": { "kind": "python", "entry": "main.py", "limits": { "memoryMb": 64, "storageMb": 10 } },
"activation": { "on": ["command", "event", "menu"] },
"permissions": {
"required": [
{ "id": "messages.read", "scope": "selected", "reason": "Counts the messages it sees." },
{ "id": "storage.private", "reason": "Remembers the count." },
{ "id": "ui.command", "reason": "Adds /hello." },
{ "id": "ui.menu", "reason": "Adds a message menu item." }
]
},
"contributes": {
"events": ["message.new"],
"commands": [{ "name": "hello", "description": "Says hello" }],
"menus": [{ "surface": "message", "id": "count", "title": "Count words", "icon": "stats", "when": { "has_text": true } }]
},
"storage": { "schema": 1, "export": { "prefixes": ["settings."] } }
}

(files and signature are added by fgpl_tool pack.)