# Admin panel: all routes and API contracts Documentation of every route in the built-in `telesrv` admin panel — the SPA page addresses and the API routes behind them. The panel server lives in `cmd/telesrv-admin`; routes are declared in `(*server).routes()` (`server.go:50`). The panel is a plain HTTP server: it serves the built frontend (`/`) and a JSON API under the `/api/` prefix. Every API path requires a session (cookie), and every mutating request additionally requires a CSRF token. Many routes also check an operator permission (`permission`). ## How to use this document - For each GET endpoint, the **request parameters** (query) and a short **response** shape are listed. - For each POST endpoint under `/api/actions/*` and the verification decisions, the **request body** (JSON) is given. They all share common fields (see below) plus their own specific fields. - `int64` in JSON may be sent either as a number or as a string (e.g. `"user_id": "123"` or `"user_id": 123`) — the server accepts both (`flexInt64`). `flexUnix` accepts Unix seconds or a date in `2006-01-02` / RFC3339 form. - All mutating requests go to the Admin API. A command's response is either `{"status":..., "message":..., "command_id":...}` on success (HTTP 200) or `{"error":..., "code":...}` on error. An optimistic-locking conflict is `409`. ## Authentication and session | Method | Path | Description | | --- | --- | --- | | POST | `/api/login` | Log in. Body: `{"secret": "..."}`. Validates the config secret, issues a signed session cookie and CSRF cookie. The only mutating route without a CSRF token (no session yet); only Origin is checked. | | POST | `/api/logout` | Log out. Destroys the session and CSRF cookie. | | GET | `/api/session` | Returns `{"actor": "...", "permissions": [...], "csrf_token": "..."}`. Called by the panel on startup. | The session lives in a signed cookie (default TTL 12 hours); there is no server-side session store. Every request except `GET/HEAD/OPTIONS` and `/api/login` must present: - the `telesrv_admin_csrf` cookie; - the `X-CSRF-Token` header with the same value; - an Origin matching the host (when Origin is present). Violating any of the three — `403 Forbidden`. ## Permissions and their checks The panel distinguishes plain authorization (`requireAuthAPI`) from a permission check (`requirePermission`). Permission names match the strings in `TELESRV_ADMIN_UI_PERMISSIONS`: | Permission | What it grants | | --- | --- | | `*` | All permissions (wildcard). | | `premium.manage` | Managing the Premium plan catalog, granting Premium. | | `bots.token.read` | Exporting a bot token (`/api/actions/export-bot-token`). | | `verification.review` | Reading and deciding the official verification queue. | | `verification.revoke` | Stripping the official badge (in addition to `verification.review`). | | `botverification.review` | Reading and deciding the third-party (bot) verification queue. | | `botverification.manage` | Appointing verifiers, editing the icon catalog, stripping a third-party mark. | Official and third-party verification rights are intentionally independent. The session's permission list is returned from `/api/session` so the UI can hide unavailable sections. ## Interface pages (SPA) SPA routes are declarative: any section name is served as `/` (index.html), and the frontend decides what to render (`web/src/pages/Routes.tsx`). | Path | Page | | --- | --- | | `/` | Dashboard (counters, storage stats, section links). | | `/accounts` | Account list (with search). | | `/accounts/{id}` | Account card: profile, actions. | | `/channels` | Supergroup and channel list. | | `/channels/{id}` | Channel card. | | `/bots` | Bot list. | | `/bots/{id}` | Bot card. | | `/broadcasts` | Broadcasts. | | `/monetization`, `/premium` | Stars and Premium: plans, grants. Requires `premium.manage`. | | `/moderation` | Complaints and moderation: case list. | | `/moderation/{id}` | Moderation case details. | | `/emoji` | Emoji set catalog. | | `/stickers` | Sticker pack catalog. | | `/gif-catalog` | GIF catalog. | | `/messages`, `/messages/private` | Private message audit. | | `/messages/detail`, `/messages/private/detail` | Private message detail (`?owner_user_id=&msg_id=`). | | `/messages/groups` | Group/channel message audit. | | `/messages/groups/detail` | Group message detail (`?channel_id=&msg_id=`). | | `/gifts` | Star gifts: catalog, collectibles, auctions. | | `/give-gifts` | Gift granting. | | `/collectible-usernames` | Collectible usernames. | | `/collectible-usernames/{id}` | Collectible username card. | | `/collectible-phones` | Anonymous numbers. | | `/account-ratings` | Account ratings. | | `/account-ratings/{user_id}` | Account rating card. | | `/storage` | Object storage: stats. | | `/verification` | Official verification: application queue. Requires `verification.review`. | | `/verification/{id}` | Official verification application details. Requires `verification.review`. | | `/bot-verification` | Third-party verification: verifiers, icons, marks, queue. Requires `botverification.review`. | | `/bot-verification/{id}` | Third-party verification request details. Requires `botverification.review`. | An unknown API path returns `404 {"error":"api route not found"}`; any unknown frontend path is served as `/`. ## Response conventions - Successful reads — `200` + JSON; files (avatars, animations, previews) — the file itself. - Errors — JSON of the form `{"error": "...", "code": "..."}` with the corresponding HTTP status. - `401` — missing/expired session; `403` — CSRF/Origin violation or missing permission (in `requirePermission` the body gets an added `permission` field); `409` — optimistic-locking conflict (moderation case, verification); `502` — Admin API unreachable. - Errors from commands sent to the Admin API are returned as `{"status": ..., "message": ..., "error": ...}`. ## Common command fields (POST `/api/actions/*`) Every mutating request carries in its body: | Field | Type | Description | | --- | --- | --- | | `command_id` | string | Idempotency key for the command. Repeating the same `command_id` does not execute the action twice. If empty — generated by the server. | | `reason` | string | Mandatory operation reason (audit). | | `confirm` | bool | Operator confirmation (`true`). | --- ## API: dashboard and storage | Method | Path | Description | | --- | --- | --- | | GET | `/api/dashboard` | Summary: `counts`, `storage`, and optionally `host`. | | GET | `/api/storage/stats` | Object storage statistics. | ## API: accounts | Method | Path | Parameters / response | | --- | --- | --- | | GET | `/api/accounts` | Params: `q` (search), `before_id` (int64), `before_active_us` (int64, microseconds), `limit` (int). Response: `query`, `limit`, `rows`, `has_more`, `next_before_id`, `next_before_active_us`, `listing`. | | GET | `/api/accounts/{id}` | Account card: profile, flags, statistics. | | GET | `/api/accounts/{id}/avatar` | Account avatar (file). | | GET | `/api/account-ratings` | Params: `q`, `min_level` (int), `user_id` (int64), `before_id` (int64), `limit` (int). Response: `rows`, `has_more`, `next_before_id`. | | GET | `/api/account-ratings/{user_id}` | Response: `rating`, `events`. | ## API: channels and supergroups | Method | Path | Parameters / response | | --- | --- | --- | | GET | `/api/channels` | Params: `q`, `before_id` (int64), `before_updated_us` (int64, microseconds), `limit` (int). Response: `query`, `limit`, `rows`, `has_more`, `next_before_id`, `next_before_updated_us`, `listing`. | | GET | `/api/channels/{id}` | Channel card. | | GET | `/api/channels/{id}/avatar` | Channel avatar (file). | ## API: bots and broadcasts | Method | Path | Parameters / response | | --- | --- | --- | | GET | `/api/bots` | Params: `q`, `before_id` (int64), `limit` (int). Response: `query`, `limit`, `rows`, `has_more`, `next_before_id`, `listing`. | | GET | `/api/bots/{id}` | Bot card. | | GET | `/api/broadcasts` | Params: `before_id` (int64), `limit` (int). Response: `limit`, `rows`, `has_more`, `next_before_id`. | ## API: media catalogs | Method | Path | Parameters / response | | --- | --- | --- | | GET | `/api/emoji` | Params: `q`, `before_id` (int64), `limit` (int). Response: `query`, `rows`, `has_more`, `next_before_id`, `listing`. | | GET | `/api/emoji/{id}/animation` | Emoji Lottie animation (file). | | GET | `/api/stickers` | Param: `kind` (string, type filter). Response: `rows`, `max_items`. | | GET | `/api/stickers/{id}/documents` | Response: `document_ids`. | | GET | `/api/stickers/documents/{id}/animation` | Sticker animation (file). | | GET | `/api/gif-catalog` | Response: proxied from Admin API (`/v1/gif-catalog`). | | GET | `/api/gif-catalog/documents/{id}/preview` | GIF preview (file). | ## API: message audit | Method | Path | Parameters / response | | --- | --- | --- | | GET | `/api/messages` | Params: `owner_user_id` (int64, required with `peer_id`), `peer_id` (int64), `before_date` (int64), `before_id` (int), `limit` (int). Response: `owner_user_id`, `peer_id`, `before_date`, `before_id`, `limit`, `rows`. | | GET | `/api/messages/detail` | Params: `owner_user_id` (int64, req.), `msg_id` (int, req.). Response: message card. | | GET | `/api/messages/groups` | Params: `channel_id` (int64, req.), `before_date` (int64), `before_id` (int), `limit` (int). Response: `channel_id`, `before_date`, `before_id`, `limit`, `rows`. | | GET | `/api/messages/groups/detail` | Params: `channel_id` (int64, req.), `msg_id` (int, req.). Response: message card. | ## API: star gifts and collectibles | Method | Path | Parameters / response | | --- | --- | --- | | GET | `/api/gifts` | Response: `Gifts` (gift list). | | GET | `/api/auctions` | Response: `Auctions` — live state of all operator-authored auctions and scheduled drops. | | GET | `/api/official-gifts` | Response: proxied from Admin API (`/v1/official-gifts`). | | GET | `/api/official-gifts/{id}/animation` | Official gift animation (file). | | GET | `/api/gifts/{id}/animation` | Gift animation (file). | | GET | `/api/gifts/{id}/collectibles` | Response: proxied from Admin API (`/v1/gifts/{id}/collectibles`). | | GET | `/api/gifts/{id}/collectibles/{kind}/{attribute_id}/animation` | Collectible attribute animation (file). `kind` ∈ {`model`, `pattern`}. | | GET | `/api/collectible-usernames` | Params: `status` (`` | `vault` | `owned` | `burned`), `owner_user_id` (int64), `before_id` (int64), `limit` (int), `q`. Response: `rows`, `has_more`, `next_before_id`. | | GET | `/api/collectible-usernames/{id}` | Response: `asset`, `transfers`. | | GET | `/api/collectible-phones` | Params are passed through to the Admin API as-is (`/v1/collectible-phones?...`). | | GET | `/api/collectible-phones/{id}` | Params are passed through to the Admin API (`/v1/collectible-phones/{id}?...`). | ## API: Premium All routes in this section require the `premium.manage` permission (checked both at the panel and at the Admin API). | Method | Path | Description | | --- | --- | --- | | GET | `/api/premium/plans` | Premium plan catalog (proxied to Admin API `/v1/premium/plans`). | ## API: moderation All reads pass query parameters through to the Admin API as-is (`/v1/moderation/...`). Decisions are `POST` (see below). | Method | Path | Description | | --- | --- | --- | | GET | `/api/moderation/cases` | Moderation case list. | | GET | `/api/moderation/cases/{id}` | Moderation case. | | GET | `/api/moderation/reports/{id}` | Report. | | POST | `/api/moderation/cases/{id}/claim` | Claim the case. Body: common fields + `version` (int64), `internal_note` (string). | | POST | `/api/moderation/cases/{id}/decide` | Decide the case. Body: common fields + `version` (int64), `internal_note` (string). | | POST | `/api/moderation/cases/{id}/appeals/{appeal_id}/review` | Review an appeal. Body: common fields + `version` (int64), `internal_note` (string). | ## API: official verification All routes require the `verification.review` permission. Reads go straight to PostgreSQL; decisions always go through the Admin API (command journal, state machine, optimistic locking). | Method | Path | Parameters / body | | --- | --- | --- | | GET | `/api/verification/applications` | Params: `status`, `target_type`, `reviewer`, `q`, `before_id`, `limit`. Response: `rows`, `has_more`, `next_before_id`. | | GET | `/api/verification/applications/{id}` | Response: application, events, `applicant_controls_target`, `target_verified`. | | GET | `/api/verification/counts` | Application counts by status. | | POST | `/api/verification/applications/{id}/claim` | Decision body: common fields + `version` (int64), `internal_note` (string). | | POST | `/api/verification/applications/{id}/approve` | Body: common fields + `version` (int64), `internal_note` (string). Grants badge. | | POST | `/api/verification/applications/{id}/reject` | Body: common fields + `version` (int64), `internal_note` (string). | | POST | `/api/actions/revoke-verification` | Strip a badge. Requires `verification.review` **and** `verification.revoke`. Body — see actions section below. | A "decided by another moderator" conflict is returned as `409 Conflict`. ## API: third-party (bot) verification A separate mechanism with separate tables, permissions, and routes. Queue reads and decisions require `botverification.review`; managing verifiers, the icon catalog, and stripping marks requires `botverification.manage`. | Method | Path | Parameters / body | | --- | --- | --- | | GET | `/api/botverification/verifiers` | Params: `enabled_only` (bool), `limit` (int). Response: `rows`. | | GET | `/api/botverification/icons` | Params: `active_only` (bool), `limit` (int). Response: `rows`. | | GET | `/api/botverification/marks` | Params: `peer_type`, `verifier_bot_id` (int64), `q`, `before_id` (int64), `limit` (int). Response: `rows`, `has_more`, `next_before_id`. | | GET | `/api/botverification/requests` | Params: `status`, `peer_type`, `verifier_bot_id` (int64), `q`, `before_id` (int64), `limit` (int). Response: `rows`, `has_more`, `next_before_id`. | | GET | `/api/botverification/requests/{id}` | Response: `request`, `verifier`, `mark_active`. | | GET | `/api/botverification/counts` | Request counts by status. Response: `counts`. | | POST | `/api/botverification/requests/{id}/approve` | Decision body: common fields + `version` (int64), `internal_note` (string). | | POST | `/api/botverification/requests/{id}/reject` | Body: common fields + `version` (int64), `internal_note` (string). | | POST | `/api/botverification/requests/{id}/revoke` | Body: common fields + `version` (int64), `internal_note` (string). | | POST | `/api/actions/grant-bot-verifier` | Appoint a bot as verifier. Requires `botverification.manage`. Body — see below. | | POST | `/api/actions/set-bot-verifier-enabled` | Enable/disable a verifier. Requires `botverification.manage`. Body — see below. | | POST | `/api/actions/revoke-bot-verifier` | Strip a bot's verifier status. Requires `botverification.manage`. Body — see below. | | POST | `/api/actions/upsert-verification-icon` | Add/edit a catalog icon. Requires `botverification.manage`. Body — see below. | | POST | `/api/actions/set-verification-icon-active` | Enable/disable an icon. Requires `botverification.manage`. Body — see below. | | POST | `/api/actions/revoke-custom-verification` | Strip a third-party mark. Requires `botverification.manage`. Body — see below. | --- ## API: account actions All routes are `POST /api/actions/...`, require a session and CSRF. The request body always contains the **common command fields** (`command_id`, `reason`, `confirm`) plus the fields from the table below. | Path | Specific body fields | Permission | | --- | --- | --- | | `set-frozen` | `user_id` (int64), `frozen` (bool), `freeze_until` (time, opt.), `freeze_appeal_url` (string, opt.) | — | | `grant-premium` | `user_id` (int64), `months` (int) | `premium.manage` | | `upsert-premium-plan` | `months` (int), `duration_days` (int), `amount_stars` (int64), `fiat_currency` (string), `fiat_amount` (int64), `store_product` (string), `store_quantity` (int), `enabled` (bool), `sort_order` (int), `label` (string), `expected_version` (int64) | `premium.manage` | | `grant-stars` | `user_id` (int64), `amount` (int64) | — | | `set-verified` | `user_id` (int64), `verified` (bool) | — | | `set-account-flags` | `user_id` (int64), `scam` (bool), `fake` (bool) | — | | `set-support` | `user_id` (int64), `support` (bool) | — | | `set-account-username` | `user_id` (int64), `username` (string) | — | | `set-account-profile` | `user_id` (int64), `first_name` (string), `last_name` (string) | — | | `set-account-phone` | `user_id` (int64), `phone` (string) | — | | `set-account-login-email` | `user_id` (int64), `email` (string) | — | | `set-account-avatar` | **multipart**: `metadata` field (JSON with common fields + `user_id` (int64)) and a file in `file` | — | | `set-account-color` | `user_id` (int64), `for_profile` (bool), `has_color` (bool), `color` (int), `background_emoji_id` (int64) | — | | `set-account-emoji-status` | `user_id` (int64), `document_id` (int64), `until` (int, seconds) | — | | `revoke-sessions` | `user_id` (int64), `hash` (int64, opt.), `keep_hash` (int64, opt.), `revoke_all` (bool) | — | Example (granting Premium, the case from above): ```http POST /api/actions/grant-premium Content-Type: application/json X-CSRF-Token: { "command_id": "grant-premium-001", "reason": "Incident compensation", "confirm": true, "user_id": 123456789, "months": 12 } ``` ## API: channel actions | Path | Specific body fields | Permission | | --- | --- | --- | | `set-channel-flags` | `channel_id` (int64), `scam` (bool), `fake` (bool) | — | | `set-channel-settings` | `channel_id` (int64), `gigagroup` (*bool), `antispam` (*bool), `participants_hidden` (*bool), `noforwards` (*bool), `join_to_send` (*bool), `join_request` (*bool), `slowmode_seconds` (*int) — all optional pointers | — | | `set-channel-username` | `channel_id` (int64), `username` (string) | — | | `set-channel-color` | `channel_id` (int64), `for_profile` (bool), `has_color` (bool), `color` (int), `background_emoji_id` (int64) | — | | `set-channel-emoji-status` | `channel_id` (int64), `document_id` (int64), `until` (int) | — | | `set-channel-avatar` | **multipart**: `metadata` (JSON with common fields + `channel_id` (int64)) and a file `file` | — | | `set-channel-verified` | `channel_id` (int64), `verified` (bool) | — | ## API: bot actions | Path | Specific body fields | Permission | | --- | --- | --- | | `create-bot` | `owner_user_id` (int64), `name` (string), `username` (string) | — | | `delete-bot` | `bot_user_id` (int64) | — | | `export-bot-token` | `bot_user_id` (int64) | `bots.token.read` | | `create-broadcast` | `message` (string), `target_mode` (string), `user_ids` ([]int64, opt.) | — | ## API: sticker and emoji actions | Path | Specific body fields | Permission | | --- | --- | --- | | `create-sticker-set` | **multipart**: `metadata` (JSON: common fields + `title`, `short_name`, `kind`, `emoji`, `keywords`) and a file `file` | — | | `rename-sticker-set` | `set_id` (int64), `title` (string) | — | | `add-sticker-to-set` | **multipart**: `metadata` (JSON: common fields + `set_id` (string), `emoji`, `keywords`) and a file `file` | — | | `remove-sticker-from-set` | `set_id` (int64), `document_id` (int64) | — | | `set-sticker-set-archived` | `set_id` (int64), `archived` (bool) | — | | `set-sticker-set-sort-order` | `set_id` (int64), `sort_order` (int) | — | | `delete-sticker-set` | `set_id` (int64) | — | ## API: GIF catalog actions | Path | Specific body fields | Permission | | --- | --- | --- | | `create-gif-catalog-entry` | **multipart**: `metadata` (JSON: common fields + `title`) and a file `file` | — | | `set-gif-catalog-enabled` | `id` (int64, sent as string), `enabled` (bool) | — | | `set-gif-catalog-sort-order` | `id` (int64, string), `sort_order` (int) | — | | `delete-gif-catalog-entry` | `id` (int64, string) | — | ## API: gift and collectible actions | Path | Specific body fields | Permission | | --- | --- | --- | | `import-gift` | **multipart**: `metadata` (JSON: common fields + `gift_id` (int64), `title`, `stars` (int64), `convert_stars` (int64), `enabled` (bool), `sort_order` (int), `auction` (bool), `auction_slug`, `gifts_per_round` (int), `auction_start_date` (int), `auction_round_duration` (int), `availability_total` (int), `locked_until_date` (int)) and a file `file` | — | | `import-official-gift` | common fields + `source_gift_id` (string), `gift_id` (int64), `title`, `stars` (int64), `convert_stars` (int64), `enabled` (bool), `sort_order` (int), `include_collectible` (bool), `upgrade_stars` (int64), `supply_total` (int), `slug_prefix` (string), `locked_until_date` (int) | — | | `publish-gift-collectibles` | **multipart**, `gift_id` sent in query (`?gift_id=`): `metadata` (JSON: common fields + `upgrade_stars` (int64), `supply_total` (int), `slug_prefix` (string), `models` ([]object), `patterns` ([]object), `backdrops` ([]object)); animations are files keyed by the `models`/`patterns` entries | — | | `set-gift-enabled` | `gift_id` (int64), `enabled` (bool) | — | | `set-gift-sort-order` | `gift_id` (int64), `sort_order` (int) | — | | `give-gift` | common fields + `sender_user_id` (int64), `user_id` (int64), `channel_id` (int64), `gift_id` (int64), `hide_name` (bool), `message` (string), `upgrade` (bool), `model_attribute_id` (int64), `pattern_attribute_id` (int64), `backdrop_attribute_id` (int64) | — | ## API: collectible username actions | Path | Specific body fields | Permission | | --- | --- | --- | | `mint-collectible-username` | `username` (string), `owner_user_id` (int64), `owner_channel_id` (int64), `currency` (string), `amount` (int64), `crypto_currency` (string), `crypto_amount` (int64), `url` (string), `purchase_date` (int/unix or date) | — | | `transfer-collectible-username` | `username` (string), `to_user_id` (int64), `to_channel_id` (int64) | — | | `revoke-collectible-username` | `username` (string), `burn` (bool) | — | | `delete-collectible-username` | `username` (string) | — | ## API: anonymous number actions | Path | Specific body fields | Permission | | --- | --- | --- | | `mint-collectible-phone` | `phone` (string), `tier` (string), `owner_user_id` (int64), `currency` (string), `amount` (int64), `crypto_currency` (string), `crypto_amount` (int64), `url` (string), `purchase_date` (int/unix or date) | — | | `update-collectible-phone-price` | `phone` (string), `currency` (string), `amount` (int64), `crypto_currency` (string), `crypto_amount` (int64) | — | | `transfer-collectible-phone` | `phone` (string), `to_user_id` (int64) | — | | `revoke-collectible-phone` | `phone` (string), `burn` (bool) | — | | `delete-collectible-phone` | `phone` (string) | — | ## API: account rating actions | Path | Specific body fields | Permission | | --- | --- | --- | | `recompute-account-rating` | `user_id` (int64) | — | | `adjust-account-rating` | `user_id` (int64), `amount` (int64) | — | ## API: message deletion | Path | Specific body fields | Permission | | --- | --- | --- | | `delete-messages` | `owner_user_id` (int64), `peer_id` (int64), `ids` ([]int), `revoke` (bool) | — | | `delete-history` | `owner_user_id` (int64), `peer_id` (int64), `max_id` (int), `min_date` (int), `max_date` (int), `max_batches` (int), `just_clear` (bool), `revoke` (bool) | — | ## API: verification decisions (request bodies) Official verification — `revoke-verification`: | Field | Type | Description | | --- | --- | --- | | `target_type` | string | Target type (`user` / `channel`, etc., validated by `domain.VerificationTargetType.Valid()`). | | `target_id` | int64 | Target identifier (not the application!). | | `internal_note` | string | Operator-only internal note (opt.). | | + common fields | | `command_id`, `reason`, `confirm`. | Third-party verification — verifier/mark actions (`botverification.manage`): | Path | Specific body fields | | --- | --- | | `grant-bot-verifier` | `bot_id` (int64), `icon_document_id` (int64), `company_name` (string, req.), `default_description` (string), `can_modify_custom_description` (bool), `version` (int64, 0 for new) | | `set-bot-verifier-enabled` | `bot_id` (int64), `enabled` (bool) | | `revoke-bot-verifier` | `bot_id` (int64) | | `upsert-verification-icon` | `document_id` (int64), `name` (string, req.), `owner_bot_id` (int64, opt., 0 = shared) | | `set-verification-icon-active` | `icon_id` (int64), `active` (bool) | | `revoke-custom-verification` | `verifier_bot_id` (int64), `peer_type` (string, validated), `peer_id` (int64) | All of these also carry the common fields (`command_id`, `reason`, `confirm`).