umbot
    Preparing search index...

    Platform integration

    This page is machine-translated from the Russian original. If something reads oddly, the Russian version is the source of truth — open an issue.

    The umbot framework provides a single API for building voice skills and chatbots on all the leading Russian and international platforms.

    Feature umbot Jovo SaluteJS Native SDK
    Alice + Marusia + Sber ✅ ❌ ⚠️ Sber Requires manual routing and duplicated logic
    A single business logic ✅ ✅ ❌ ❌
    Telegram / VK / Viber support ✅ ⚠️ Partial ❌ Requires manual routing and duplicated logic
    TypeScript out of the box ✅ ✅ ✅ ⚠️ Depends on the SDK

    The strength of umbot is the full Russian voice assistant stack (Alice, Sber SmartApp, Marusia) in one codebase. SaluteJS is the native SDK of the Sber (Salute) ecosystem, so Sber is its home platform, but multi-platform support (Alice, Marusia, chatbots) is not available in it. Jovo focuses on multi-platform chatbots (of the Telegram / VK / Viber trio it has Telegram and Viber, but not VK) and is not integrated with Russian voice platforms. Native SDKs (telegraf, alice-sdk, vk-io) target a single platform and require duplicating logic for multi-platform support. For the current lists of supported platforms, see their official documentation.

    Platform Identifier Status
    Yandex Alice alisa ✅ The full skills protocol
    Marusia marusia ⚠️ Existing skills only: VK stopped accepting new ones (20.12.2024)
    Sber SmartApp smart_app ✅ The SmartApp API protocol
    Telegram telegram ✅ The basic set (the rest of the API — via controller.api / TelegramRequest)
    VK vk ✅ The basic set (the rest of the API — via controller.api / VkRequest)
    MAX max_app ✅ The basic set (the rest of the API — via controller.api / MaxRequest)
    Viber viber ✅ The basic set. New Viber bots — on commercial terms only
    Any other platform ... ✅ Via adapters

    What the basic messenger set includes for each platform is in the sections below and in the "Platform contract check".

    The platform is selected automatically based on the request the application received — just do not forget to connect the platform adapters. You can also specify explicitly which platform is used:

    const bot = new Bot('max_app');
    
    • HTTPS with a valid SSL certificate
    • A stable response time (< 3 seconds recommended)
    • Webhook URL support
    • Node.js 20.19+ and TypeScript 5+
    import { Bot } from 'umbot';
    import { fullPlatforms } from 'umbot/plugins';

    const bot = new Bot();
    bot.use(fullPlatforms); // Connect all available platforms
    bot.setPlatformParams({
    // Platform parameters
    welcome_text: 'Hi!', // The greeting text
    help_text: 'I can...', // The help text
    intents: [],
    });
    bot.setAppConfig({
    // General parameters
    json: './data', // The directory for JSON data
    error_log: './logs', // The directory for logs
    isLocalStorage: true, // Use local storage
    });
    bot.start('localhost', 3000); // Start the application

    Bot determines by itself which platform the request came from — by the request body and headers. You do not need to configure anything: one webhook endpoint accepts requests from all platforms.

    If automatic detection fails (a rare case, usually when proxying through your own gateway), you can override it:

    bot.setPlatformResolver((query, headers, detect) => {
    // detect() runs the standard automatic detection
    if (headers?.['x-my-routing'] === 'alice') return 'alisa';
    return detect ? detect(query, headers) : null;
    });

    Each platform has its own limits on text length, the number of buttons, card size and state. Adapters bring the response to an acceptable form themselves, so the code stays the same for all platforms:

    • Buttons beyond the limit are dropped with a warning in the log. Adapter limits: Alice, Marusia and VK — 10 buttons, SmartApp — 8, Viber — 6, MAX — 30, Telegram — 40. Extra buttons in a row (buttons.row()) are moved to the next row.
    • Text longer than the limit is truncated: Alice and Marusia — 1024 characters, Telegram and VK — 4096, MAX — 4000, Viber — 7000, SmartApp — 250 in a "bubble".
    • A button payload larger than the limit — the button is skipped with a warning; the data is neither truncated nor rewritten.
    • State larger than the limit (Alice — 1 KB, Marusia — 3584 bytes) is not sent, and an error is logged.
    • A device without a screen (a smart speaker) — buttons and cards are not sent.

    The framework cannot trim your business logic: the response time to voice platforms is your responsibility. The framework logs a warning after 2 s of processing and an error after 2.9 s; upload media in advance with Preload.

    By default the platform itself sends a request to your HTTPS address — a webhook (bot.start(), webhookHandle, webhookEvent). Telegram, VK and MAX can also hand out updates on request: bot.startPolling() starts long polling (getUpdates in Telegram, Bots Long Poll in VK, GET /updates in MAX), and no public address is needed — handy for local development and servers without HTTPS.

    bot.use(new TelegramAdapter(process.env.TELEGRAM_TOKEN));
    await bot.startPolling(); // resolves after bot.stopPolling(), bot.close() or SIGINT/SIGTERM
    • An update goes through the same pipeline as a webhook (middleware, commands, the user queue), except signature verification: it was received from the API with the bot token. Updates of one batch run in parallel, no more than 32 at a time; updates of one user run one after another, in batch order.
    • Polling has no client IP: ipFilter with rejectWithoutIp: true rejects all updates. A polling bot does not need ipFilter — the bot makes the requests to the platform itself.
    • A network error or 5xx — a retry with a pause of 1 to 30 seconds. A wrong token or an active webhook in Telegram (a 409 response) stops polling of that platform with the reason in the log.
    • Telegram: polling does not work while the bot has a registered webhook (a 409 response). The webhook is not removed silently — the token may belong to a production bot. Use another token for development or remove the webhook explicitly with the new TelegramAdapter(token, { telegram_delete_webhook: true }) option: the adapter calls deleteWebhook on the first request and logs a warning. In polling mode the reply always goes via the API: the telegram_webhook_reply option has no effect.
    • VK: you need a community token and the Long Poll API enabled ("API usage" → "Long Poll API") with the event types you need. The Callback API secret (VK_SECRET_KEY) is not needed for polling. The names of the authors of a batch's messages are loaded with a single users.get request.
    • MAX recommends polling for development and testing, and a webhook (POST /subscriptions) in production. According to the MAX documentation, the first request without marker returns only the last accumulated event: messages that arrived before the bot started, except the last one, are not processed.
    • You can combine them: for example, bot.start() for Alice and bot.startPolling({ platforms: ['telegram'] }) for Telegram.

    Alice, Marusia, SmartApp and Viber work only via a webhook: to check them on your local machine you need a tunnel (ngrok and similar, see getting-started). Without a network you can check the logic in the console with BotTest (umbot/test).

    How the framework handles the stream of webhooks:

    • One user's requests run one after another (the key is the platform + userId). A double button press or several webhook connections no longer lead to two handlers reading the same userData and only the last one being saved. Requests from different users run in parallel. If the user's previous request takes longer than 10 seconds, the next one starts without waiting for it (for Alice, Marusia and SmartApp — no longer than half of the remaining response time). The queue lives in process memory: with several replicas, route one user's requests to one replica.
    • Redeliveries are not processed twice. Telegram, VK, MAX and Viber redeliver if they did not get a 2xx response in time (MAX — 30 seconds). The framework remembers accepted deliveries for an hour (up to 10 000 in process memory) and answers a redelivery with 200 ok without running the logic. The key includes a hash of the request body, so a forged request with a guessed update_id will not block the real update. A redelivery that arrives while the original request is being processed waits for its outcome (up to 30 seconds); if the original failed with a server error (500), the redelivery is processed again. Events whose response carries content are not deduplicated: Telegram in the telegram_webhook_reply mode, confirmation in VK, webhook and conversation_started in Viber. Deduplication works only for requests via webhookHandle / webhookEvent (bot.run() does not perform it).
    • A developer account in Yandex Dialogs
    • An HTTPS endpoint for the webhook
    • Response time < 3 seconds
    1. Create a skill in the Yandex Dialogs console
    2. Get an OAuth token in Yandex OAuth if needed. The token is needed for uploading audio or images.
    3. Configure the parameters in code:
    bot.setPlatformParams({
    isAuthUser: true, // For working with authorization
    intents: [],
    });
    bot.use(new AlisaAdapter('YOUR_OAUTH_TOKEN')); // Way 1: the token in the constructor (higher priority)
    // bot.setAppConfig({ // Way 2: the token in the config (an alternative if not passed in the constructor)
    // tokens: {
    // alisa: {
    // token: 'YOUR_OAUTH_TOKEN',
    // },
    // },
    // });

    You do not have to put the token in code: the ALISA_TOKEN environment variable is picked up automatically (without configuring env in the config). The old YANDEX_TOKEN name is kept for backward compatibility — if both variables are set, ALISA_TOKEN takes precedence.

    • User authorization support
    • Local data storage
    • Built-in speech synthesis
    • Cards and galleries support
    class AlisaController extends BotController {
    public action(intentName: string | null): void {
    if (intentName === WELCOME_INTENT_NAME) {
    // Authorization check
    if (!this.userToken) {
    this.isAuth = true;
    this.text = 'Authorization is required to continue';
    return;
    }

    // Working with an authorized user
    this.text = `Hi, ${this.nlu.getUserName()?.first_name || 'user'}!`;
    this.tts = 'Hi! Glad to see you again!';

    // Adding a card
    this.card.addImage('image_token', 'Welcome', 'Description', 'Button');

    // Adding buttons
    this.buttons.addBtn('Help').addBtn('Start game');
    }
    }
    }
    • A bot created with @BotFather
    • An HTTPS webhook URL
    • Telegram Bot API support
    1. Get a token from @BotFather

    2. The quick path for steps 2–3: npx umbot webhook telegram https://your-domain/webhook in the project folder. The command takes TELEGRAM_TOKEN from .env, generates a secret, registers the webhook with it and saves TELEGRAM_WEBHOOK_SECRET to .env — the framework picks it up itself. Manually: generate a webhook secret (the same string will be needed in two places):

      node -e "console.log(require('crypto').randomBytes(24).toString('base64url'))"
      
    3. Register the webhook, passing the secret in secret_token:

        curl "https://api.telegram.org/bot<TOKEN>/setWebhook" \
    -d "url=https://your-domain/webhook" \
    -d "secret_token=<SECRET>"
    ```

    4. Configure the parameters in code (the secret is the same as in `setWebhook`):

    ```ts
    bot.use(new TelegramAdapter('YOUR_BOT_TOKEN')); // Way 1: the token in the constructor (higher priority)
    // bot.setAppConfig({ // Way 2: the token in the config (an alternative)
    // tokens: {
    // telegram: {
    // token: 'YOUR_BOT_TOKEN',
    // webhookSecret: process.env.TELEGRAM_WEBHOOK_SECRET, // the same secret as in setWebhook
    // },
    // },
    // });

    Verifying requests. Set appConfig.tokens.telegram.webhookSecret — the adapter will check the x-telegram-bot-api-secret-token header and reject requests not from Telegram (401 before any logic runs). Without webhookSecret the adapter accepts any request with an update_id field — anyone who learns the webhook URL can send messages on behalf of any user; this is acceptable only for local debugging. More in configuration.md → Webhook signature verification.

    • A rich set of UI elements
    • File and media support
    • Inline buttons and a keyboard
    class TelegramController extends BotController {
    public action(intentName: string | null): void {
    if (intentName === WELCOME_INTENT_NAME) {
    this.text = 'Hi! I am a Telegram bot built with umbot';

    // Adding inline buttons
    this.buttons
    .addBtn('Website', 'http://localhost')
    .addBtn('Help', null, { command: 'help' });

    // Sending an image
    this.card.addImage('image_url', ' ', 'Image description');
    }
    }
    }
    • A VK group
    • Group administrator rights
    • Community messages enabled
    1. Create a VK group
    2. Get an access key in the group settings (community management → API usage; the developer portal is dev.vk.com)
    3. Set up the Callback API and enable the "Secret key" in its settings (without it there is nothing to verify the signature with — see the note below)
    4. Configure the parameters in code:
    bot.use(
    new VkAdapter('YOUR_BOT_TOKEN', {
    vk_confirmation_token: 'YOUR_CONFIRMATION_TOKEN',
    vk_secret_key: 'YOUR_SECRET_KEY', // the same "Secret key" that is enabled in the group settings
    vk_api_version: '5.199',
    }),
    ); // Way 1: the token and options in the constructor (higher priority)
    // bot.setAppConfig({ // Way 2: the token in the config (an alternative)
    // tokens: {
    // vk: {
    // token: 'YOUR_BOT_TOKEN',
    // confirmation_token: 'YOUR_CONFIRMATION_TOKEN',
    // secret_key: 'YOUR_SECRET_KEY',
    // api_version: '5.199',
    // },
    // },
    // });

    Note: in the VkAdapter constructor the keys are passed with the vk_ prefix (vk_confirmation_token, vk_secret_key, vk_api_version), and in appConfig.tokens.vk — without the prefix (confirmation_token, secret_key, api_version). Both formats are valid and supported by the framework.

    Verifying requests. VK sends secret in the body of every callback request when the "Secret key" is enabled in the group settings; the adapter compares it with secret_key in constant time. Without secret_key the adapter accepts any request with the type + group_id fields — anyone who learns the webhook URL can send messages on behalf of any user. If the secret cannot be enabled in the group, restrict access with ipFilter (the VK Callback API IP ranges).

    • Message carousel support
    • A message keyboard
    • Working with attachments
    • Integration with the VK API
    class VKController extends BotController {
    public action(intentName: string | null): void {
    if (intentName === WELCOME_INTENT_NAME) {
    this.text = 'Hi! I am a VK bot';

    // Adding a keyboard
    this.buttons.addBtn('Menu').addBtn('Help').addBtn('About us', 'https://vk.ru/group');

    // Sending a carousel
    this.card
    .addImage('photo_token_1', 'Product 1', '100 RUB')
    .addImage('photo_token_2', 'Product 2', '200 RUB');
    }
    }
    }
    • A verified organization/sole proprietor profile is required
    1. Go to your organization's profile on the platform
    2. In the Chatbots section, click Create
    3. Fill in the bot settings (its card) and click Create
    4. Register the webhook with a secret: npx umbot webhook max https://your-domain/webhook (MAX accepts only HTTPS on port 443). The command takes MAX_TOKEN from .env, calls POST /subscriptions with a generated secret and saves it to .env as MAX_WEBHOOK_SECRET — the framework picks it up itself.
    bot.use(new MaxAdapter('YOUR_BOT_TOKEN', { secret: 'YOUR_WEBHOOK_SECRET' })); // Way 1: the token + the webhook secret
    // bot.setAppConfig({ // Way 2: the token in the config (an alternative)
    // tokens: {
    // max_app: {
    // token: 'YOUR_BOT_TOKEN',
    // webhookSecret: process.env.MAX_WEBHOOK_SECRET, // the same secret as in the bot's subscription
    // },
    // },
    // });

    Verifying requests. MAX passes the secret in the x-max-bot-api-secret header. Set it as the second constructor argument ({ secret: ... }) or in appConfig.tokens.max_app.webhookSecret — the adapter will start rejecting requests with a wrong header (401). Without a secret the adapter accepts any request with the update_type + timestamp fields — anyone who learns the webhook URL can send messages on behalf of any user; acceptable only for local debugging. More in configuration.md → Webhook signature verification.

    • Message carousel support
    • A message keyboard
    • Working with attachments
    • Integration with the MAX API
    class MaxController extends BotController {
    public action(intentName: string | null): void {
    if (intentName === WELCOME_INTENT_NAME) {
    this.text = 'Hi! I am a MAX bot';

    // Adding a keyboard
    this.buttons
    .addBtn('Menu')
    .addBtn('Help')
    .addBtn('About us', 'https://dev.max.ru/docs/chatbots/bots-create');

    // Sending a carousel
    this.card
    .addImage('photo_token_1', 'Product 1', '100 RUB')
    .addImage('photo_token_2', 'Product 2', '200 RUB');
    }
    }
    }
    • A bot account created in the Viber Admin Panel
    • An HTTPS webhook URL
    • A sender name that matches the bot's name in Viber
    1. Create a bot in the Viber Admin Panel and copy the bot token
    2. Set the webhook URL on your server. The adapter itself answers 200 to the service webhook event that Viber sends when registering the webhook — without it the webhook will not be registered
    3. Configure the parameters in code:
    bot.use(
    new ViberAdapter('YOUR_BOT_TOKEN', {
    viber_sender: 'YOUR_BOT_NAME', // required: the bot's name in Viber
    }),
    ); // Way 1: the token and options in the constructor (higher priority)
    // bot.setAppConfig({ // Way 2: the token in the config (an alternative)
    // tokens: {
    // viber: {
    // token: 'YOUR_BOT_TOKEN',
    // sender: 'YOUR_BOT_NAME',
    // },
    // },
    // });

    Note: Viber confirms request authenticity with the x-viber-content-signature header — the adapter verifies it automatically. The API format is described in the Viber developer documentation.

    • Message text up to 7000 characters
    • Buttons are rich media (RichMedia): the adapter sends up to 6 buttons in the current implementation (the Viber grid allows up to 42: 6×7); the Columns/Rows of each button set its size in the grid, not the number of cards
    • Sounds and TTS are not supported by the platform
    class ViberController extends BotController {
    public action(intentName: string | null): void {
    if (intentName === WELCOME_INTENT_NAME) {
    this.text = 'Hi! I am a Viber bot';

    // Adding buttons
    this.buttons.addBtn('Help').addBtn('About us', 'https://example.com');
    }
    }
    }
    • A VK developer account
    • An HTTPS endpoint
    • Marusia protocol support
    1. Create a skill in the Marusia developer console (skill documentation — vk.com/dev/marusia_skill_docs)
    2. Get a token for uploading media
    3. Configure the parameters:
    bot.use(new MarusiaAdapter('YOUR_MEDIA_TOKEN')); // Way 1: the media upload token in the constructor (higher priority)
    bot.setAppConfig({
    isLocalStorage: true,
    // tokens: { // Way 2: the token in the config (an alternative)
    // marusia: {
    // token: 'YOUR_MEDIA_TOKEN',
    // },
    // },
    });

    The token is needed not only for images but also for uploading your own sounds. Since 3.1.0 MarusiaSound can upload audio files to Marusia (marusia.getAudioUploadLink → upload → marusia.createAudio), so custom sounds work on both voice platforms — Alice and Marusia. Preloading is done with Preload.loadSounds(paths, [T_ALISA, T_MARUSIA]): sound tokens are cached in the database (as for Alice), the route is the same as in the contract check (section 6, "Marusia's outgoing API requests"). In the handler it is enough to work with controller.sound — the adapter picks the token by the file path itself.

    • Voice input/output support
    • Local storage
    • Cards: BigImage ({type, image_id}) and ItemsList ({type, items: [{image_id}]}); image_id is an integer. Marusia cards have no titles, descriptions or buttons, and the protocol has no ImageGallery type — a gallery is sent as an ItemsList (up to 7 images; a list — up to 5)
    • ⚠ Since 20.12.2024 VK has stopped creating and supporting custom Marusia skills (the protocol documentation was removed from dev.vk.com; the format was checked against an archived copy).
    • Uploading your own sounds (with the media upload token, see "Setup" above)
    • Health check: the framework automatically answers the service ping with pong
    class MarusiaController extends BotController {
    public action(intentName: string | null): void {
    if (intentName === WELCOME_INTENT_NAME) {
    this.text = 'Hi! I am a Marusia skill';
    this.tts = 'Hi! I am ready to help you';

    // Adding a card
    this.card.addImage('image_token', 'Welcome', 'Choose an action');

    // Adding buttons
    this.buttons.addBtn('Start').addBtn('Help');
    }
    }
    }
    • A Sber developer account
    • An HTTPS endpoint
    • SmartApp protocol support
    1. Create an application in the Sber developer portal
    2. Configure the parameters:
    bot.use(new SmartAppAdapter()); // No token needed — authentication goes through the Sber ecosystem
    bot.setAppConfig({
    isLocalStorage: true,
    });

    Why is there no token? SmartApp uses the Sber platform's built-in authentication — the application is verified through the Sber ecosystem at registration, and no separate API token is required.

    • Canvas App support
    • Built-in scenarios
    • A rich UI
    • Integration with the Sber ecosystem
    class SmartAppController extends BotController {
    public action(intentName: string | null): void {
    if (intentName === WELCOME_INTENT_NAME) {
    this.text = 'Hi! I am a SmartApp built with umbot';

    // Adding a card
    this.card.addImage('image_token', 'Welcome', 'Choose an action');

    // Adding buttons
    this.buttons.addBtn('Start').addBtn('Help');
    }
    }
    }
    const bot = new Bot();
    bot.use(new MyAdapter()); // Set a custom adapter
    // MyAdapter.ts
    import { BasePlatformAdapter, TContent } from 'umbot/plugins';
    import { BotController } from 'umbot';
    import { Text } from 'umbot';

    class MyAdapter extends BasePlatformAdapter {
    /**
    * The unique platform name
    */
    platformName: string = 'my_platform';

    /**
    * Returns whether the request belongs to the current platform
    * @param query - The request body
    * @param headers - The HTTP headers
    */
    isPlatformOnQuery(query: unknown, headers?: Record<string, unknown>): boolean {
    const q = query as Record<string, unknown>;
    return !!(q.data && (q.data as Record<string, unknown>).messageCount !== undefined);
    }

    /**
    * Processing the received request. In this method you need to fill the botController with the required data
    * @param query - The request from the platform
    * @param controller - The application controller
    */
    setQueryData(query: unknown, controller: BotController): boolean | Promise<boolean> {
    if (!query) {
    // write adapter errors through the context logger, not to console directly
    controller.appContext.logError('MyAdapter.setQueryData(): an empty request was sent');
    return false;
    }
    let content: Record<string, unknown>;
    if (typeof query === 'string') {
    content = JSON.parse(query);
    } else {
    content = query as Record<string, unknown>;
    }

    const data = content.data as Record<string, unknown> | undefined;

    controller.requestObject = content;
    controller.userId = content.userId as string;
    controller.userCommand = ((data?.text as string) || '').toLowerCase();
    controller.originalUserCommand = (data?.text as string) || '';
    controller.messageId = data?.messageCount as number;

    if (content.store) {
    controller.state = content.store as Record<string, unknown>;
    }

    controller.isScreen = false;

    return true;
    }

    /**
    * Returns the result that will be sent to the platform.
    * @param controller
    */
    getContent(controller: BotController): TContent {
    return {
    text: controller.text,
    tts: controller.tts,
    };
    }

    /**
    * Returns a demo request that the platform will send
    * @param query The user's request
    * @param userId The user ID
    * @param count The request sequence number
    * @param state Data from the local storage
    */
    getQueryExample(
    query: string,
    userId: string,
    count: number,
    state: Record<string, unknown> | string,
    ): Record<string, unknown> {
    return {
    userId,
    data: {
    text: query.toLowerCase(),
    messageCount: count,
    },
    store: state,
    };
    }
    }
    Property Alice Marusia SmartApp Telegram VK Viber Max
    Voice (native TTS) ✅ ✅ ✅ ❌ ❌ ❌ ❌
    Local storage ✅ ✅ ✅ (external API) ❌ ❌ ❌ ❌
    Proactive sending (bot.send) ❌ ❌ ❌ ✅ ✅ ✅ ✅
    Image upload ✅ ✅ ❌ (URL) ✅ ✅ ❌ (URL) ✅
    Sound file upload ✅ ✅ ❌ ✅ ✅ ❌ ✅
    Standard sounds (S_AUDIO_*) ✅ ✅ ❌ ❌ ❌ ❌ ❌
    S_EFFECT_* effects ✅ ❌ ❌ ❌ ❌ ❌ ❌
    TTS via SpeechKit (speech_kit_token) ❌ ❌ ❌ ✅ ✅ ❌ ✅
    Webhook signature verification ❌ ❌ ❌ ✅* ✅* ✅ ✅*
    Emotions / appeal ❌ ❌ ✅ ❌ ❌ ❌ ❌

    Where it says ❌, the platform does not support the feature, and the framework simply ignores the corresponding controller fields. The code will not break.

    ⚠️ About "Webhook signature verification":

    • Alice, SmartApp, Marusia — requests have no signature at all: the entire payload (including user_id) is controlled by the sender. Do not interpolate this data into a URL or a query without escaping, and do not treat such a request as authenticated.
    • Viber — the signature is always verified automatically (x-viber-content-signature).
    • Telegram — verification is enabled by setting a secret (tokens.telegram.webhookSecret → the x-telegram-bot-api-secret-token header); without a secret verification is disabled.
    • VK — verification is enabled only if tokens.vk.secret_key is set (compared with the secret field in the request body); without a secret it is skipped.
    • MAX — verification is enabled by setting tokens.max_app.webhookSecret (the x-max-bot-api-secret header).

    ℹ️ About sounds:

    • Voice platforms (Alice, Marusia) substitute sounds as <speaker audio="..."> in TTS.
    • Alice and Marusia can upload your audio files (the getSoundInDB helpers from Alisa/Sound and Marusia/Sound — internally they use YandexSoundRequest / MarusiaRequest); Marusia's standard sounds are substituted from the fixed marusia-sounds/* set.
    • Chat platforms (Telegram, VK, MAX) upload the audio file and send it as a voice/audio message; with speech_kit_token set, the text part of the TTS is synthesized via Yandex SpeechKit.
    • Viber and SmartApp strip sound markers from TTS (in Viber soundProcessing returns null).
    • A strict response time. The framework tracks processing time itself: a response slower than 2000 ms logs a warning, slower than 2900 ms — an error. Check the platform's own timeouts in its current documentation. Use Preload for media.
    • Alice's state limit: 1 KB. If the data is larger or cannot be serialized, the state field is not sent and the previous state is not cleared. For large data use a database adapter.
    • An empty response is not filled in by the framework (voice platforms). An empty text is allowed by the documentation when tts is filled in. If the developer left both fields empty, umbot keeps them as is and logs a warning: empirically such a response may be accepted, but the Alice documentation does not guarantee this scenario. On chat platforms (Telegram, VK, Viber, MAX) there is a fallback: with an empty text and a filled tts, the framework uses tts (without sound SSML markup) as the response text.
    • isScreen = false on smart speakers. Buttons and cards are not displayed. Check this.isScreen before this.card.addImage(...).
    • Health check (ping). Yandex periodically sends ping. The framework automatically answers pong.
    • The authorization event. Account linking completion (account_linking_complete_event) arrives as the universal auth event: bot.addEvent('auth', ...) — the fact of linking is recorded in controller.userEvents.auth. User text utterances are the message event.
    • Deleting fields. delete this.userData.foo does not work — the platform returns the old value. Use this.userData.foo = null.
    • Limits. Text and TTS — up to 1024 characters, state — up to 3584 bytes, a button payload — up to 4096 bytes (if exceeded: the state is not sent, the button is skipped with a warning).
    • Health check (ping). The framework automatically answers pong to the platform's service requests.
    • Cards. BigImage and ItemsList, the elements are image_id only (an integer); a gallery is sent as an ItemsList.
    • The response text cannot be empty (unlike Alice): with an empty text it is taken from tts without markup.
    • No local storage. With isLocalStorage: true and no DB adapter, userData is kept in process memory (memorySession): steps work, but the data is lost on restart and is not shared between processes and replicas. For reliable storage, connect a database.
    • TTS via SpeechKit. Speech requires appConfig.tokens.telegram.speech_kit_token (or the SPEECH_KIT_TOKEN environment variable — it is applied to Telegram, VK and MAX at once). Without it controller.tts is ignored.
    • Markup is off by default. parse_mode is passed only with an explicit telegram_parse_mode. With HTML/MarkdownV2 enabled, the developer is responsible for escaping dynamic data.
    • Proactive sending. bot.send(userId, text, T_TELEGRAM) works (unlike voice platforms).
    • Group chats. userId is taken from from.id (the person), not from chat.id (the group) — one user gets one database record both in a group and in a private chat. The reply is delivered to the original chat (the chat ID is platformOptions.requestData.telegram.chatId).
    • Events. The adapter recognizes all update types (media, callback, inline, message_edited, channel_post, my_chat_member, etc.) — unknown service updates are acknowledged with HTTP 200 without a reply. Non-text updates are caught by event routing: bot.addEvent('photo' | 'voice' | 'callback' | 'inline' | 'message_edited' | 'channel_post', ...) (the full list of types is in api-reference.md, the "Event routing" section).
    • Inline buttons without a payload (options.inline). A button without payload and url goes as a regular reply keyboard by default. With the { inline: true } option it is shown as an inline button under the message, and a press arrives at the bot as the button text: this.buttons.addBtn('Catalog', '', '', { inline: true }). Text longer than the callback_data limit (64 bytes) is passed as a service token #t<n> and restored by the adapter from the message keyboard. The option has no effect on request_contact / request_location — Telegram accepts them only in a regular keyboard. Projects generated with npx umbot create from-flow set the option on all Telegram buttons.
    • Webhook reply (opt-in). new TelegramAdapter('TOKEN', { telegram_webhook_reply: true }): a simple text reply goes in the webhook response body ({method: 'sendMessage', ...}) — Telegram executes it itself, saving one outgoing POST per request. Following grammy: opt-in (off by default), not applied to callback/inline requests and to replies with cards/sounds — they go the regular way. Keep in mind: sending errors cannot be diagnosed in this case (Telegram acknowledges the webhook before actually executing the method).
    import { Bot } from 'umbot';
    import { TelegramAdapter, T_FORMAT_MARKDOWN, escapeMarkdownV2 } from 'umbot/plugins';

    // Option 1: plain text without parse_mode
    const botPlain = new Bot().use(new TelegramAdapter('TOKEN'));

    // Option 2: explicit MarkdownV2 (the framework does not escape — the developer is responsible for validity)
    const botMd = new Bot().use(
    new TelegramAdapter('TOKEN', {
    telegram_parse_mode: T_FORMAT_MARKDOWN,
    }),
    );

    // Option 3: a text reply in the webhook body without a separate POST
    const botWebhookReply = new Bot().use(
    new TelegramAdapter('TOKEN', {
    telegram_webhook_reply: true,
    }),
    );

    // Safely inserting user input into MarkdownV2
    botMd.addCommand('whoami', ['who am i'], (_, ctx) => {
    const userName = escapeMarkdownV2(ctx.originalUserCommand ?? '');
    ctx.text = `*You wrote:* ${userName}`;
    });
    • Two tokens. The bot token + vk_confirmation_token (for confirming the webhook during initial setup). If VK sent a confirmation request and confirmation_token is not set, the adapter answers ok without running the business logic and logs where to set the token.
    • The secret key. Optional: set vk_secret_key in the adapter constructor or VK_SECRET_KEY in .env to verify every request from the VK Callback API. If the secret key is enabled in the group settings, VK sends a secret field in the body of every event — the adapter compares it with the stored value.
    • No local storage. With isLocalStorage: true and no DB adapter, userData is kept in process memory (memorySession): steps work, but the data is lost on restart and is not shared between processes and replicas. For reliable storage, connect a database.
    • The user name comes from a cache. The users.get result (the name for nlu.getUserName()) is cached in process memory for 1 hour (up to 5000 entries; API errors are not cached). You can disable loading with the adapter option new VkAdapter(token, { vk_load_user_info: false }) — then getUserName() returns null, but a reply takes one request to VK instead of two. To reset the cache (tests, a name change) — clearVkUserCache() from umbot/plugins.
    • Callback buttons are acknowledged via messages.sendMessageEventAnswer. On a callback button press (message_event) the adapter acknowledges the event (sendMessageEvent without event_data — the loading indicator on the user's button disappears) and sends the handler's reply as a regular message (messages.send). If the business logic failed, a snackbar with the error text is shown instead of a message. To show your own snackbar, call controller.api.answerCallback(text). The event ID is stored in platformOptions.requestData.vk.eventId (falling back to platformOptions.eventId).
    • The callback button payload is normalized. The string 'buy' or the JSON {"command":"buy"} in the payload ends up in userCommand as buy and fires as a regular command — without parsing requestObject manually.
    • Button layout. buttons.row() ends a row (up to 5 buttons; location/vkpay/open_app take a whole row); buttons with the same options._group (a string or a number) also go into one row.
    • Button color. options.color: 'primary' | 'secondary' | 'positive' | 'negative'.
    • The sender name is required. It must match the bot's name in Viber.
    • The API version is 7 by default. If the user did not pass a version explicitly, the adapter sends min_api_version: 7 (VIBER_DEFAULT_API_VERSION). Version 7 is needed for rich_media (cards); cards are not displayed on old clients.
    • Sounds are not supported. Custom sounds are not sent; controller.tts with an empty text goes as regular text (without sound markup), and with a filled text it is not used.
    • No local storage. With isLocalStorage: true and no DB adapter, userData is kept in process memory (memorySession): steps work, but the data is lost on restart and is not shared between processes and replicas. For reliable storage, connect a database.
    • Service events. The adapter handles the subscribed/unsubscribed events (they are logged), delivered/seen/failed (acknowledged without an error), conversation_started and the webhook event when registering the webhook (see "Setup" above). Through event routing (bot.addEvent('start' | 'subscribed' | 'unsubscribed', ...)) you can attach your own logic to them; the user's media message types are available as photo/video/document/ contact/location/sticker (details are in controller.payload and requestObject).
    • No local storage. With isLocalStorage: true and no DB adapter, userData is kept in process memory (memorySession): steps work, but the data is lost on restart and is not shared between processes and replicas. For reliable storage, connect a database.
    • TTS via SpeechKit. Speech requires appConfig.tokens.max_app.speech_kit_token (or the SPEECH_KIT_TOKEN environment variable).
    • The sending queue. MAX limits sending to a single dialog — no more often than 1 message per 500 ms (and no more than 2 callback answers per second per dialog). MaxRequest queues outgoing messages per dialog with a 500 ms interval, so fast repeated replies do not get a 429 from the platform. The queue's internal timers do not block the process from exiting.
    • Attachments right after upload. MAX does not process an uploaded file instantly and may answer a message with it with the attachment.not.ready error. MaxRequest retries such a send up to 3 times with pauses of 0.5 / 1 / 2 s; if the file is still not ready, the error is logged. For frequently used images and sounds, upload the files in advance (Preload) — then the reply carries a ready token.
    • Message limits. Text up to 4000 characters, up to 12 attachments per message (the keyboard counts as an attachment), the keyboard — up to 30 rows of 7 buttons. Excess is trimmed by the framework with a warning in the log.
    • Group chats and channels. If the webhook has chat_id, the reply goes to the chat, not to the private dialog (the chat ID is in platformOptions.chatId).
    • Callback buttons. On a press the adapter acknowledges the callback (POST /answers with an empty body) and sends the handler's reply as a new message — as in Telegram and VK. If you want the reply to replace the message with the pressed button (this is how message in POST /answers works), enable the new MaxAdapter(token, { max_callback_edit_message: true }) option. If the handler itself called controller.api.answerCallback(text), there will be no second acknowledgement.
    • API. The base URL is platform-api2.max.ru; authorization with the Authorization: <token> header (the platform no longer supports query parameters). A detailed contract comparison is in platform-contract-comparison.md.
    • No token. Authentication goes through the Sber ecosystem.
    • Emotions. controller.emotion = 'radost' (22 options).
    • Rating flow. controller.isSendRating = true starts a skill rating.
    • Events. An application launch (RUN_APP) arrives as the start event, and a completed rating as rating (text utterances are message): bot.addEvent('start' | 'rating', ...).

    As you can see from the examples above, the controller code looks practically the same for all platforms.
    You write the logic once, using the universal this.text, this.buttons, this.card, etc.
    The framework determines which platform the request came from and automatically converts your response to the right format.

    You do not need to check this.appType manually and write different code for Alice, Telegram or VK —
    the platform adapters do it for you. The only exception is rare cases that require
    platform-dependent behavior (for example, generating UTM tags in links). For such situations you can always
    access this.appType explicitly and add extra logic.

    With this approach you can focus on your application's business logic rather than on the implementation details of each platform. One codebase — works everywhere.

    Two cross-platform features of 3.1.0 cover what used to require manually parsing requestObject for each platform. The full API reference (signatures, examples) is in api-reference.md; here is how they map to the platforms.

    Non-text updates (photos, voice messages, callback buttons, message edits, a start, subscriptions) arrive as universal events: the adapter writes the type to controller.eventType, and bot.addEvent(eventType, handler) handlers are called before steps and commands. Each adapter declares the list of supported events (supportedEvents) — bot.addEvent warns about a typo or an event that no connected platform supports:

    Platform Events (supportedEvents)
    Telegram message, photo, voice, video, document, location, contact, sticker, callback, inline, message_edited, channel_post
    VK message, callback
    MAX message, callback, start, message_edited
    Viber message, photo, video, document, contact, location, sticker, start, subscribed, unsubscribed
    Alice message, auth
    Marusia message, auth
    SmartApp message, start, rating

    A custom platform extending BasePlatform declares its own supportedEvents (the default is ['message']) and takes part in the validation automatically.

    Unified access to the active platform's capabilities: sendPhoto / sendDocument / sendAudio / sendVideo(file, { caption }), answerCallback(text, showAlert?) and can(method) to check support. The facade is lazy — it is created on the first access to ctx.api; on voice platforms (Alice, SmartApp, Marusia) it is null (their reply is built as the webhook body; media are sent via controller.card / controller.sound).

    Method Telegram VK MAX Viber
    sendPhoto full via the standard upload flow /uploads null + warn
    sendDocument full yes /uploads null + warn
    sendAudio full no (null) /uploads null + warn
    sendVideo full no (null) /uploads null + warn
    answerCallback yes (showAlert is supported only by Telegram) show_snackbar POST /answers null + warn

    Viber returns can() === false for all methods: its Bot API accepts media only by a public URL with a required size — use controller.card / ViberRequest directly.

    A custom platform and controller.api: the facade is connected by the adapter method createApi(controller) (the IPlatformAdapter contract). The BasePlatform base implementation returns null (the facade is unavailable), so a platform with outgoing API calls only needs to override one method — the core learns about it without any changes on its side:

    import { BasePlatformAdapter } from 'umbot/plugins';
    import type { BotController, IControllerApi } from 'umbot';

    class MyAdapter extends BasePlatformAdapter {
    // ...
    createApi(controller: BotController): IControllerApi | null {
    return makeMyApi(controller); // your own facade factory
    }
    }

    Besides connecting adapters one by one, there are sets from umbot/plugins: voicePlatforms (Alice, SmartApp, Marusia), botPlatforms (Telegram, VK, MAX, Viber) and fullPlatforms (all 7). The list of all adapters is adapters from umbot/plugins.

    If you use Express, Fastify or any other HTTP framework, you can integrate umbot with the webhookHandle method.

    import express from 'express';
    import { Bot } from 'umbot';
    import { fullPlatforms } from 'umbot/plugins';

    const app = express();
    app.use(express.json({ type: '*/*' })); // important for Alice/Sber

    // Initialize the application
    const bot = new Bot();
    bot.use(fullPlatforms);
    bot.setAppConfig({
    json: './data',
    error_log: './logs',
    isLocalStorage: true,
    env: 'local',
    });

    // Connecting the webhook handler
    app.post('/webhook', async (req, res) => {
    try {
    await bot.webhookHandle(req, res);
    } catch (err) {
    console.error('Webhook error:', err);
    res.status(500).send('Internal Server Error');
    }
    });

    app.listen(3000, () => {
    console.log('Server started at http://localhost:3000/webhook');
    });

    Starting with version 3.0.0, the framework supports proactive message sending — that is, a skill (if supported) or a bot can initiate a dialog with the user without an incoming request.

    ⚠️ Important: not all platforms support this feature. For example, Alice, SmartApp and Marusia do not allow sending messages without a request. Telegram, VK, Viber and MAX support sending via bot.send() — the implementation is inherited from the base adapter (without their own checks in the platform adapters): Viber needs a valid receiver (the user's user_id), MAX needs an initiated dialog (user_id or chat_id). Support depends on the platform used.

    import { T_TELEGRAM } from 'umbot/plugins';

    // Sending a message to a user in Telegram
    const result = await bot.send('123456789', 'Hi! This is a broadcast.', T_TELEGRAM);
    • Keep tokens in environment variables
    • Use HTTPS
    • Verify request signatures
    • Validate incoming data
    • Use TypeScript
    • Follow the SOLID principles
    • Write tests
    • Keep documentation