Skip to main content
One Telegram bot token does four jobs in Omniflow. It runs the customer bot, which is the primary sales and self-service surface. It settles Telegram Stars payments, which arrive on the bot’s own authenticated update stream rather than on any public route. It verifies the Login Widget and Mini App payloads that sign a customer into the web panel. And it delivers operator notifications into a forum group you control. Those four uses are spread across three processes, all reading the same APP_TELEGRAM_TOKEN from the same .env: the bot process runs the chat, the API verifies sign-ins and creates Stars invoices, and the worker charges renewals through the Stars adapter. Giving them separate tokens would let them disagree about which bot the customer is talking to. This page is the setup. What the bot actually does for a customer — every command, screen, and flow — is in the Telegram bot.

Creating the bot

1

Talk to BotFather

Open @BotFather, send /newbot, and answer with a display name and a username ending in bot. BotFather replies with the token.
2

Put the token in .env

An empty token is not an error: the bot process logs telegram bot disabled because APP_TELEGRAM_TOKEN is empty and idles. That is deliberate — an installation selling only through the web panel should not need a bot to start.
3

Start the stack and check the log

The bot logs telegram bot started with the update mode it chose. It also calls getMe on start to learn its own username, which is what makes referral links and the web Login Widget possible. If that call fails — Telegram briefly unreachable while the container starts — the log says so, and the bot asks again the next time a referral screen needs the username: after 30 seconds at the earliest, then with a doubling backoff up to once an hour, so no restart is needed.
4

Bind the web domain, if you run the customer panel

Send /setdomain to BotFather and give it the host your customer panel is served from. Telegram renders the Login Widget only on a domain bound to the bot. Without this the sign-in screen reports telegram: false and shows no Telegram button — honest, but not what you wanted.
You do not need to configure the command menu. The bot publishes its own list on every start through setMyCommands, in English and Russian: /start, /menu, /plans, /orders, /wallet, /settings, /support, /login, and /cancel. Editing the list in BotFather would be overwritten on the next restart. /ops is deliberately absent from the published list — see Operator tooling.

Long polling or a webhook

Long polling is the default and stays supported as an explicit development and fallback mode. The bot chooses by whether a webhook URL is set; there is no third setting to keep in step.

Turning the webhook on

Both values are validated at startup and a bad one is fatal rather than latent:
  • APP_TELEGRAM_WEBHOOK_URL must parse and use https, or the bot exits with APP_TELEGRAM_WEBHOOK_URL must be an HTTPS URL.
  • APP_TELEGRAM_WEBHOOK_SECRET must match ^[A-Za-z0-9_-]{32,256}$ — the character set Telegram itself accepts — or the bot exits with a message naming the rule.
On start the bot calls setWebhook with the URL, the secret token, and an explicit allowed_updates list of message, edited_message, callback_query, pre_checkout_query, my_chat_member, and chat_member. A registration failure is fatal: a bot that silently kept polling while an operator believed it was on a webhook would be worse than one that refused to start. Naming the update types rather than accepting everything means Telegram does not deliver update kinds nothing handles. The secret token is the whole authentication story for that route. Telegram sends it in a header on every delivery and the library verifies it before an update is parsed, so a request that does not carry it never reaches any handler. The endpoint also caps the body at 1 MiB. Point your reverse proxy at APP_BOT_HTTP_ADDR and terminate TLS there; see reverse proxy.
The webhook host is reachable from the internet, so the bot refuses to publish its own diagnostics there. APP_BOT_METRICS_ADDR serves /livez, /readyz, /metrics, and /healthz, and it is ignored when it equals APP_BOT_HTTP_ADDR. Give the two different addresses, and expose only the webhook one. Leaving APP_BOT_METRICS_ADDR empty keeps the bot off HTTP entirely in long polling mode.

Switching back

Clear APP_TELEGRAM_WEBHOOK_URL and restart. Telegram keeps the previously registered webhook until something deletes it, so also delete it through the Bot API (deleteWebhook) or the bot will still be receiving nothing while polling.

The Mini App and the Login Widget

The customer web panel signs a customer in from Telegram two ways, and both verify a payload rather than trusting one. The Login Widget is the button on /account/sign-in. Telegram signs its fields with HMAC-SHA256 under SHA-256 of the bot token, so a payload that verifies at all is proof Telegram produced it for this bot. The widget is loaded by the bot’s username, which Omniflow resolves from getMe on first use and caches — one fewer environment variable to keep in step with the token. If the username cannot be resolved, the sign-in screen reports telegram: false and omits the button rather than rendering one that never appears. The Mini App signs in with no button at all. Opened inside Telegram, the surrounding client hands the page a signed initData string, which the page posts to POST /v1/account/auth/telegram/miniapp. The verification differs in exactly one place that matters: the HMAC key is HMAC-SHA256("WebAppData", botToken) rather than a plain SHA-256 of the token. The two paths stay separate in code rather than sharing a helper that guesses which is which, because using the widget’s key here would verify nothing. Both checks bound auth_date to one minute (with 30 seconds of tolerance for a fast client clock). The hash never expires on its own, so without that bound a payload captured once — from a browser history entry, a referrer, a screenshot of a URL — would authenticate forever.
1

Set the widget domain

/setdomain in BotFather, with the host serving /account. This is what makes the Login Widget render at all.
2

Register the Mini App, if you want one

BotFather’s /newapp binds a web app URL to the bot. Omniflow’s customer panel needs nothing extra: opened inside Telegram it detects initData and signs in automatically, and opened in a normal browser it falls back to the widget.
3

Verify

GET /v1/account/auth/methods reports what this installation actually offers: {telegram, telegramBot, magicLink, oidc[]}. The sign-in screen renders from exactly that, so what you see there is what the API believes is configured.
A third, operator-enabled route exists for installations where the widget is not usable: the bot delivers a one-time sign-in link in response to /login. It stays off by default — APP_CUSTOMER_MAGIC_LINK_ENABLED=false — because it is a second bearer credential travelling through a chat. Enabling it requires APP_PUBLIC_URL and, on the bot, a 32-byte APP_DATA_ENCRYPTION_KEY. The web panel covers all three routes from the customer’s side.

The operator group

Operator notifications go to one Telegram group with Topics enabled. The bot creates and binds one forum topic per event kind, and recreates one that was deleted; you supply only the group.
1

Create the group and enable Topics

A group, not a channel. Turn on Topics in the group settings — the bot cannot create a forum topic in a group that does not have them.
2

Add the bot and grant Manage topics

Promote the bot to administrator with at least the Manage topics permission. Without it, topic creation fails and is recorded as missing_manage_topics_permission.
3

Set the chat ID

Restart the bot. With the value left at 0 the notifier logs operator notifications are disabled because no group is configured and enqueueing becomes a no-op — notices are not even stored, so turning the group on later does not replay history.
4

Confirm

Watch for operator topic could not be created in the bot log, or read operator_topics.status and last_error_code. The classified codes are missing_manage_topics_permission, chat_not_found, forum_topics_disabled, bot_not_in_group, and telegram_error — the raw Telegram message is dropped because it can echo request content.
Everything the group receives is deliberately content-free. A notice names the event, an amount class, and identifiers an operator can look up; it never carries customer text, a subscription link, a token, or a payment payload. Rendering uses an explicit field allowlist, so a field somebody adds to a payload later is ignored rather than published by accident. Volume is bounded: past APP_OPERATOR_NOTIFICATION_CAP messages of one kind per APP_OPERATOR_NOTIFICATION_WINDOW_MINUTES (defaults 30 and 5), further notices complete as suppressed with volume_cap and a warning is logged. A dedupe key collapses repeated triggers for one event — a retried webhook, a second reconciliation pass — into a single message. Operator notifications covers the queue itself.

Operator tooling in the chat

Backup and restore are the only administrative actions Telegram carries, and only for explicitly named accounts. APP_OPERATOR_TELEGRAM_IDS is a comma-separated list of Telegram user IDs allowed to run /ops; leaving it empty disables the command entirely, and a malformed entry is a fatal configuration error rather than a silently dropped operator. The command is not in the published command list, so it does not appear in the menu for anyone. See backup and restore.

Required channels

If you gate access on membership of a channel, the bot is what answers the question, through getChatMember. Add the bot to each required channel as an administrator so it can read membership. A muted (restricted) member still counts as a member while Telegram reports is_member — a restricted account that has left the channel does not — and an error is treated as “could not answer” rather than “no”: the worker records unknown and waits, and the purchase gate lets the sale through. Neither guesses. Required channels has the policy.

Configuration reference

Nothing re-reads the environment: changing any of these means restarting the process.
/admin/settings has a Telegram section with a bot username, webhook URL, a webhook-enabled switch, and write-only token and webhook-secret fields, and an Operator group section with a chat ID, a topics switch, and the notification cap and window. Both store what you type, sealed with the installation key — and no running process reads either back. The bot is configured from the environment at start. See installation settings.

Reliability worth knowing about

  • Consequential callbacks — confirm, cancel, payment-method choice, invoice, device removal, link rotation — are claimed exactly once per callback query in Valkey, and the tap itself (chat, message, button) is claimed for ten seconds, so a redelivered update or a double tap cannot repeat the action. This is why the bot refuses to start without APP_VALKEY_URL.
  • Delivery failures are classified: a flood wait, a blocked bot, and a deactivated account are different problems. A blocked or deleted account stops consuming retries and is excluded from further delivery until it recovers.
  • Every update carries a correlation ID that is logged with the order, payment, job, and Remnawave request it causes, so one string ties a customer’s report to everything it touched.
  • APP_METRICS_ENABLED does not apply to the bot: it builds its metrics registry unconditionally. What decides whether they are reachable is APP_BOT_METRICS_ADDR.