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
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.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
APP_TELEGRAM_WEBHOOK_URLmust parse and use https, or the bot exits withAPP_TELEGRAM_WEBHOOK_URL must be an HTTPS URL.APP_TELEGRAM_WEBHOOK_SECRETmust match^[A-Za-z0-9_-]{32,256}$— the character set Telegram itself accepts — or the bot exits with a message naming the rule.
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.
Switching back
ClearAPP_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./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
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.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, throughgetChatMember. 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.
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_ENABLEDdoes not apply to the bot: it builds its metrics registry unconditionally. What decides whether they are reachable isAPP_BOT_METRICS_ADDR.