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.
General rules
Section titled “General rules”- 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 insidefiles, and they are not accepted as language keys inside the language map of a Localized value either (x-ruis not a valid language code, soname: has a bad entry 'x-ru'). - Validation collects errors (at most 50) and reports them as
where: what, for examplepermissions.required: unknown permission 'foo.bar'. A package with any error is rejected. manifest.jsonitself is not listed infiles. 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,iconandchangelogmust be left out rather than set to""(the error ishas a bad length, ormust be a valid https URLfor an empty URL). Empty texts inside a Localized map are rejected too.
Types used below
Section titled “Types used below”| 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.
Top-level keys
Section titled “Top-level keys”| 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.
version
Section titled “version”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
Section titled “compat”"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.
Version ranges
Section titled “Version ranges”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
Section titled “runtime”"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::calland a fixed list ofwasi_snapshot_preview1functions (anything else is rejected asimports '<module>::<name>', which is not allowed), and must exportmemoryplus eitherfg_alloc,fg_initandfg_on_event(reactor), or_start(command). See the Wasm ABI.
activation
Section titled “activation”"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
Section titled “permissions”"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 |
contributes
Section titled “contributes”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 |
events
Section titled “events”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.
commands
Section titled “commands”{ "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 and incoming hooks
Section titled “outgoing and incoming hooks”"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
Section titled “messages”"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
Section titled “translator”"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
Section titled “aiProvider”"aiProvider": { "name": "My Model", "maxChars": 4096 }name: required, up to 40. maxChars: 64–8192, default 4096. Needs ai.provide.
dependencies and conflicts
Section titled “dependencies and conflicts”"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
Section titled “storage”"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
Section titled “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
Section titled “signature”"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.
Checks made outside the manifest parser
Section titled “Checks made outside the manifest parser”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 |
Complete example
Section titled “Complete example”{ "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.)
