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
umbotis 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');
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.row()) are moved to the next
row.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
ipFilter with rejectWithoutIp: true rejects all updates. A polling bot
does not need ipFilter — the bot makes the requests to the platform itself.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_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.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.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:
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.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).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.
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');
}
}
}
Get a token from @BotFather
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'))"
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 thex-telegram-bot-api-secret-tokenheader and reject requests not from Telegram (401 before any logic runs). WithoutwebhookSecretthe adapter accepts any request with anupdate_idfield — 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.
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');
}
}
}
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
VkAdapterconstructor the keys are passed with thevk_prefix (vk_confirmation_token,vk_secret_key,vk_api_version), and inappConfig.tokens.vk— without the prefix (confirmation_token,secret_key,api_version). Both formats are valid and supported by the framework.Verifying requests. VK sends
secretin the body of every callback request when the "Secret key" is enabled in the group settings; the adapter compares it withsecret_keyin constant time. Withoutsecret_keythe adapter accepts any request with thetype+group_idfields — 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 withipFilter(the VK Callback API IP ranges).
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');
}
}
}
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-secretheader. Set it as the second constructor argument ({ secret: ... }) or inappConfig.tokens.max_app.webhookSecret— the adapter will start rejecting requests with a wrong header (401). Without a secret the adapter accepts any request with theupdate_type+timestampfields — 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.
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');
}
}
}
200 to the service webhook event that Viber sends when registering the webhook — without it the webhook will not be registeredbot.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-signatureheader — the adapter verifies it automatically. The API format is described in the Viber developer documentation.
Columns/Rows of each button set its size in
the grid, not the number of cardsclass 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');
}
}
}
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.
{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)ping with pongclass 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');
}
}
}
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.
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 correspondingcontrollerfields. 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→ thex-telegram-bot-api-secret-tokenheader); without a secret verification is disabled.- VK — verification is enabled only if
tokens.vk.secret_keyis set (compared with thesecretfield in the request body); without a secret it is skipped.- MAX — verification is enabled by setting
tokens.max_app.webhookSecret(thex-max-bot-api-secretheader).
ℹ️ About sounds:
- Voice platforms (Alice, Marusia) substitute sounds as
<speaker audio="...">in TTS.- Alice and Marusia can upload your audio files (the
getSoundInDBhelpers fromAlisa/SoundandMarusia/Sound— internally they useYandexSoundRequest/MarusiaRequest); Marusia's standard sounds are substituted from the fixedmarusia-sounds/*set.- Chat platforms (Telegram, VK, MAX) upload the audio file and send it as a voice/audio message; with
speech_kit_tokenset, the text part of the TTS is synthesized via Yandex SpeechKit.- Viber and SmartApp strip sound markers from TTS (in Viber
soundProcessingreturnsnull).
Preload for media.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(...).ping. The framework automatically answers pong.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.delete this.userData.foo does not work — the platform returns the old value. Use this.userData.foo = null.pong to the platform's service requests.image_id only (an integer); a gallery is sent as an ItemsList.text it is taken from tts without markup.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.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.parse_mode is passed only with an explicit telegram_parse_mode. With HTML/MarkdownV2 enabled, the developer is responsible for escaping dynamic data.bot.send(userId, text, T_TELEGRAM) works (unlike voice platforms).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).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).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.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}`;
});
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.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.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.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.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).'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.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.options.color: 'primary' | 'secondary' | 'positive' | 'negative'.min_api_version: 7 (VIBER_DEFAULT_API_VERSION). Version 7 is needed for
rich_media (cards); cards are not displayed on old clients.controller.tts with an empty text goes as regular
text (without sound markup), and with a filled text it is not used.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.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).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.appConfig.tokens.max_app.speech_kit_token
(or the SPEECH_KIT_TOKEN environment variable).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.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.chat_id, the reply goes to the chat, not to
the private dialog (the chat ID is in platformOptions.chatId).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.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.controller.emotion = 'radost' (22 options).controller.isSendRating = true starts a skill rating.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.
bot.addEvent)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.
controller.api)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');
});
send method)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 validreceiver(the user's user_id), MAX needs an initiated dialog (user_idorchat_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);
Full reference — API v-3.1 · all versions.