Skip to main content
The Telegram bot is Omniflow’s oldest and most complete customer surface. It runs as its own process, it talks to PostgreSQL and Remnawave directly rather than through the REST API, and it reads and writes the same customer records the customer web panel does. A cart started in a chat is the cart the browser shows; a support reply read on the web stops being unread in Telegram. The bot has no API of its own. Its only HTTP surfaces are the webhook endpoint, when an operator enables webhook mode, and the health and metrics listener — both described in Telegram integration. Everything a customer does happens inside one chat.

How the bot behaves

One message, edited in place

A command sends one message. Every subsequent tap edits that same message instead of sending a new one, so there is exactly one live screen and it is always current. A chat that accumulated one message per tap would be unusable within a session, and an old screen still showing last week’s price is a support ticket waiting to happen. While a screen loads, the message is replaced with ⏳ Refreshing your data… and then with the result. Re-rendering a screen that has not changed is not treated as an error — Telegram’s “message is not modified” answer is swallowed, because nothing went wrong from the customer’s point of view. Five things send a new message rather than editing: /start and /menu, each command that opens a screen, the Telegram Stars invoice, the confirmation of any free-text flow, and every push notification.

Private chats only

/start, every command, every button, /ops, and /login all return silently when the chat is not a private one. Account screens carry subscription state, expiry dates, and payment amounts, and a group chat is an audience.

Nothing can be forwarded

Every message the bot sends is marked protect-content and has link previews disabled. This applies to all messages, not only account ones, so a screenshot is the only way to move a screen out of the chat.

Russian and English

The interface exists in two languages. The bot follows the Telegram client’s locale by default: any locale code beginning with ru gets Russian, everything else gets English. Open /settings and tap ● Auto, RU, or EN to override it — the current choice is marked ● and the others ○. Dates always render in UTC in both languages, in the form 02 Jan 2026, 15:04 UTC. A missing date renders as —. Telegram Stars render as ⭐ 250; every other currency uses its own minor-unit exponent and drops the fraction when it is zero. Russian durations use proper plural forms.

Commands

Nine commands are published to Telegram’s command menu; a tenth is deliberately hidden. Any other message beginning with / opens the menu rather than producing an error. /cancel exists because every free-text step — a promo code, a support message, a top-up amount, a subscription name, a gift note, a gift code, a shop recipient or promo code — is a state a customer has to be able to escape. Tapping any button while such a step is open escapes it too: the prompt’s own Cancel, a menu entry, a push notification’s button. A customer who walks away from a prompt and later types “500” gets the menu, not a top-up method screen. The state would otherwise have lived for thirty minutes.
The Russian command list published to Telegram omits /login, so a Russian-locale client sees eight entries where an English one sees nine. The command itself works in both languages.
A t.me/<bot>?start=<payload> link opens the chat and sends /start <payload>. Three payloads are understood; anything else opens the menu. The order identifier has to be a well-formed UUID, and an order that is not yours renders the home screen — the bot does not reveal whether such an order exists.

The first /start

The first /start resolves — or creates — the canonical Omniflow customer behind the Telegram account. Three identities have to coexist: the Telegram account, the Omniflow customer that owns billing, wallet, support, and consent, and the Remnawave user that owns VPN access. The Omniflow customer has to exist before the first purchase, because plans, wallet, support, and news all belong to it, while the Remnawave user does not exist until fulfillment has run. Resolution takes an advisory lock on the Telegram ID, so two updates arriving at once converge on one customer instead of racing to create two. It then looks for a linked Telegram identity; failing that it adopts a pre-existing Remnawave mapping for the same ID, so an upgraded installation does not gain a second customer; failing that it creates one. The Remnawave link is made lazily and only by exact Telegram ID. When the customer has no Remnawave user on record, the bot asks Remnawave for one carrying that Telegram ID and, finding one, links it to the customer and gives the customer’s first subscription slot that user — so an upgraded or imported installation whose VPN users already know their Telegram accounts shows each customer their live access on the first /start, rather than “no subscription” and a purchase button. The lookup is repeated on the next screen if Remnawave could not be asked. If no Remnawave user carries that ID, what happens next depends on the installation. With purchases configured, the customer simply has no subscription yet and sees the purchase surface. Without them, the bot says so and stops:
Your Telegram account is not linked to a subscription yet. Contact support and an operator will link it securely.
with a 💬 Contact support button pointing at the operator’s configured support URL. The bot never asks for a username, an email address, or a subscription link, and never guesses — every alternative to an exact ID match is a way to claim somebody else’s subscription. Linking an existing Remnawave account is an operator action.

The main menu

The home screen shows the account status, a traffic bar, the expiry date, and — when the subscription is not simply healthy — a lifecycle notice in place of “Choose an action”. Status is one of 🟢 Active, ⚪️ Disabled, 🟠 Limited, 🔴 Expired, or ⚪️ Unknown. Traffic renders as a ten-block bar with a percentage, or as 📡 Traffic: <used> · unlimited when the plan has no limit. The keyboard is built once, from one round of queries, and a row appears only when the screen behind it can actually be used. A button leading to an empty or unconfigured screen teaches customers the bot is unreliable.
A customer whose Remnawave user has not been provisioned yet cannot reach the subscription or devices screens: those routes fall back to the home screen with a lifecycle notice explaining the state. 🚀 Connect explains itself directly — it says the subscription link appears once access is active — and 🎁 Invite works regardless, because the referral code belongs to the customer, not to the VPN user.

Subscription and traffic

📊 Subscription answers the question customers open the bot with. It shows the status label, ⏳ Remaining: N days, 📅 Until: <date>, and 📡 Used: <used> / <limit>. All four figures come from Remnawave, which stays authoritative for traffic, expiry, and the link itself. The buttons are 🔗 Open subscription, 📋 Copy link, 🔐 Rotate private link, 🔄, and ‹ Back. The first two appear only when the subscription URL parses, has a host, and uses http or https — a malformed link is omitted rather than rendered as a broken button. A subscription link is a bearer credential. A customer who pasted theirs somewhere needs to invalidate it without waiting for support, and needs to be told what it costs before they confirm. 🔐 Rotate private link shows:
The current link will stop working immediately. You will need to re-add the subscription on your devices.
Confirming revokes the link in Remnawave and re-renders the subscription screen with the new one. From the per-subscription screen, rotation returns the connect screen instead, so the new link can be added straight away. Rotation is refused while access is still being set up: “Access is still being set up. Refresh in a minute.”

Connecting a device

🚀 Connect opens a platform picker — iPhone/iPad, Android, Windows, macOS, Linux — and each platform leads to a three-step guide: install the recommended app, tap the add-profile button, confirm, connect. Every guide ends with “Never share your subscription link.” Each documented client is offered as a copy-text button carrying its import deep link, not as a URL button. Telegram inline buttons accept only http, https, and tg URLs, and a desktop browser may have no handler registered for happ:// at all — so a deep link that is copied works everywhere a button would not. Below them sit 📋 Copy link with the raw subscription URL and 🔗 Open subscription as a real URL button. The client list is shared with the web panel, so the two surfaces cannot recommend different apps. When there is no subscription link yet, the screen says the link appears once access is active and offers no copy buttons at all. See Connecting a device for the full per-platform detail.

Devices

📱 Devices shows In use: <total> / <limit or ∞>, then one row per device as • <model or platform> · <last seen date>, then:
For your privacy, the bot never displays HWIDs or IP addresses.
That is a hard boundary, not a display preference: a hardware ID identifies a person’s machine, so it never appears in a screen, in callback data, or in a log, and a test guards against one leaking into a button. Removing a device is two taps — 🗑 <device name>, then Remove on a confirmation screen. With more than one device present, ⚠️ Remove all devices appears and is likewise confirmed. Both call Remnawave and then re-render the list; a failure shows “Action was not completed — the data may have changed.”
At most five devices are listed, in both the text and the keyboard, and there is no pagination. A customer whose plan allows ten devices cannot see or remove devices six through ten from the chat — the web panel lists all of them.
The bot addresses a device by its position in the list it just rendered. If the list changed in between, the removal is refused with “device list changed; refresh and try again” rather than acting on whatever now sits at that position.

Multiple subscriptions

When an operator enables concurrent subscriptions, the first menu row becomes 🗂 My subscriptions. The list shows each subscription’s label, phase badge, and validity date, plus ➕ Add a subscription while the limit allows one. Opening one gives plan name, phase, valid-until, and — once provisioned — 🚀 Connect, 📱 Devices, 🔄 Rotate link, alongside ♻️ Renew, 🧩 Add-ons where the plan offers any, ♻️ Auto-renew, and ✏️ Rename. A subscription’s 📱 Devices screen works like the one above, and every one of its buttons — each removal, its confirmation, refresh, and back — names that subscription, so a removal acts on that subscription’s own VPN user. They used to reuse the single-subscription buttons, which resolve the customer’s first VPN user, and a customer with two subscriptions removed a device from the wrong one. Phase badges are No subscription, ⚙️ Activating, 🟢 Active, 🟡 Expiring soon, 🟠 Grace period, 🟠 Limit reached, ⚪️ Disabled, 🔴 Expired, and ⚠️ Activation unfinished. ✏️ Rename opens a free-text step: send a new name of 1 to 40 ordinary characters. Anything longer, or containing control characters, is refused with “The name must be 1 to 40 ordinary characters.” A new subscription’s default label is Subscription <slot>. ➕ Add a subscription re-checks the policy before showing the catalogue, so a refusal is named rather than generic: concurrency turned off by the operator, the account limit reached, or that plan’s own limit reached. With a single subscription and concurrency off, the switcher route opens the one detail screen directly — the mode is invisible to installations that do not use it. See Subscriptions. ♻️ Renew and 🧩 Add-ons act on that subscription’s own plan — a customer holding two subscriptions on different plans renews the second on its plan, not on the first one’s. A lapsed subscription is revived, not duplicated. Where concurrency is enabled, 🛒 Buy on a plan the customer already holds on an expired subscription opens a renewal of that subscription rather than a new slot beside it — otherwise every expiry would mint a new subscription, a new Remnawave user, and a new link, until the account hit its limit and could buy nothing. Only ➕ Add a subscription opens a new slot: the catalogue it shows carries that intent through to the checkout, and its plan pages offer 🛒 Buy alone.

Buying a plan

1

Open the catalogue

/plans or 🛒 Plans. Each plan renders as a block with the four facts that decide a purchase — period, traffic allowance, device limit, price — and a button labelled <name> · <price>. A trial plan carries a 🎁 Free trial badge. The comparison happens before a plan is opened, so nobody has to open five screens to compare four numbers.A plan with no price in the settlement currency, or no text for the chosen language, is omitted entirely rather than shown as unavailable. When nothing remains, the empty state names the currency: “No plans are available in USD right now.”
2

Read the plan

The detail screen adds the description, the upgrade, downgrade, and cancellation policies in words, the grace period when there is one, and the sentence “By continuing you accept the terms of service.” A 📄 Terms of service button appears when the operator configured a terms URL.One action is offered, chosen by the same rule the web panel applies, so the two surfaces never disagree about what a plan is to you:A forbidden operation is removed rather than left to fail after the tap; the backend re-checks the policy when the order is created. A trial the account is not eligible for shows the reason — already used, a subscription is active, the account is too new, and so on — and no button.
3

Choose locations, if the plan offers them

A plan whose version exposes selectable squads asks before it asks for money; a required selection collected after payment is a refund. Each squad is a toggle row, and the instruction line states the rule: pick freely, pick up to N, pick between N and M, or pick at least N. Continue re-validates the selection and re-renders the configurator rather than moving on if it does not resolve.
4

Choose a payment method

Only configured adapters that can actually settle this plan’s currency are listed, each labelled with the price it would charge: ⭐ Telegram Stars, 🪙 CryptoBot, 🏦 YooKassa, 🧾 Manual payment. Offering one that cannot settle would move the failure past the point of commitment. If none can, the screen says so and points at support.Choosing the method is also what fixes the order currency, which is why it comes before the summary. See Payments.
5

Read the summary and pay

The summary is the exact breakdown the order is created from — plan, period, method, subtotal, discount, add-ons, wallet applied, total due — so a preview and the order cannot disagree. When the wallet covers everything it reads Total due: 0 — the wallet covers this order in full.Controls: 💳 Pay, the promo row, 👛 Use wallet when there is any balance, 🧩 Add-ons when the plan has any, 🛍 Save and top up when an external amount remains, 💱 Change method, and Cancel.💱 Change method reopens the method list for this checkout and its ‹ Back returns to the summary. The promo code, add-ons, wallet toggle, location selection, and the subscription being renewed are all kept — only the method, and with it the currency, changes.

Promo codes

🏷 Enter promo code opens a free-text step: send the code in one message. The code is normalised and then validated against this exact order by running a real preview, so a code that applies to a different plan is refused here rather than at payment. On success the screen confirms ✅ Promo code X applied. and re-sends the summary with the discount line. On refusal the stored code is cleared and the reason is named: the code does not exist or is no longer active, it does not apply to this plan or account, it has reached its redemption limit, or its format is not valid. A code that stops being valid between screens never breaks the checkout — the order is re-priced without it and the reason is reported on the summary. Promo entry is limited to five attempts a minute.

Add-ons

Bought on the checkout summary, an add-on is charged with the plan in one payment and lasts the whole period. Bought mid-period from a subscription’s 🧩 Add-ons screen, the price depends on how much of the period is left, and the rule is printed under each add-on rather than discovered on the receipt: charged in full, charged in proportion to the time left, or charged at a daily rate for the days left. Tapping an add-on opens a confirmation, not a charge. It names the subscription, the add-on, the proration rule, the exact amount charged now, and where it comes from: the wallet when the balance covers it — in which case ✅ Confirm applies the add-on at once — or the wallet for what it can cover and a payment method you choose next for the rest. That method screen is the same one every order with an amount left to pay gets, listing every configured method that can settle the currency, priced at what is owed. See Add-ons and the configurator.

The channel gate

If the operator requires membership of one or more Telegram channels before a purchase, the check runs at the moment money moves — when 💳 Pay is tapped on a checkout, 🛒 Buy now on a saved cart, ✅ Confirm on an add-on, a gift’s plan or amount is chosen and again when its order is created, and Confirm and pay on a shop item — not when the plan is chosen. A customer can leave a channel between browsing and paying, and this is the last moment before money moves. The purchase is left intact, so joining and pressing the button again resumes exactly where they were:
📣 Please subscribe first — To continue with your purchase, subscribe to:
with an Open <channel> button per channel and I have subscribed, which returns to the purchase that was interrupted rather than to the catalogue. I have subscribed asks Telegram again. A recent answer that the customer is a member is trusted for five minutes so a flow does not cost one Telegram call per screen, but an answer that they are not is never trusted — the customer on this screen has most likely just joined — and the live answer is recorded so the background worker and the next purchase see it too. The gate fails open. A channel Telegram could not be asked about, or one with no invite link the customer could use, does not block the sale — losing a real sale to a brief Telegram outage costs more than the marketing requirement is worth, and a background worker re-checks membership anyway. A muted member still counts as a member, but one who has left the channel does not, even though Telegram keeps the restriction on their account. See Required channels.

Paying

Tapping 💳 Pay creates the order and immediately starts the payment. An order the wallet covers in full never touches a payment provider and goes straight to the order screen. Otherwise the screen offers 💳 Open payment page for a hosted provider, or ⭐ Pay in Telegram for Stars. Telegram Stars settle in the chat: ⭐ Pay in Telegram sends a real invoice, and Telegram then asks the bot a pre-checkout question. The bot approves only when the order belongs to this customer, the currency matches, the amount matches to the minor unit, and the order is still open — otherwise it answers “This order is no longer active. Please start again.” Refusals at confirmation each get their own screen: maintenance, a rejected additional subscription with its reason, an unresolvable location selection, a trial that is no longer eligible, an operation the plan forbids, or a generic “The order was not created. Try again or choose another payment method.” A payment that could not be started keeps the order — and its wallet reservation — and offers 🔄 Try again, which starts the payment again with the method the checkout recorded, and 💱 Pay another way, which opens the method picker for that order. The picker lists every configured method that can settle the order’s currency, priced at what the order still owes, minus any method whose payment already failed for it: the payment service resumes an existing payment per order and method, so a failed one would come back unchanged. 💳 Pay, 🔄 Try again, and the order’s method picker are replay-protected and share one rate limit — six per minute — so a double tap or a redelivered Telegram update cannot become a second order or a second payment.
The bot creates orders and payment intents. It never enqueues fulfillment: the worker owns provisioning, which is why an order moves to provisioning and then to completed without the chat doing anything. See Fulfillment.

Order status and history

/orders or 🧾 My orders lists the ten most recent orders as • <date> · <plan> · <amount> · <state>, one button each. Order identifiers are shown as the first block of the UUID — enough to quote to support. There is no pagination. Each phase has its own screen: The paid states above are a subscription order’s. An order that bought something else says what the money did, because only the worker ever moves a subscription order to completed — a top-up, a gift, or a shop purchase stays at paid for good and would read as “activating” forever: A pending gift order adds that the gift becomes redeemable as soon as the order is paid. Every screen ends with 🔄 Refresh, 🧾 My orders, and ‹ Menu. Refresh does more than re-read Omniflow’s own record: for a pending order with a payment intent it re-polls the provider, so a customer who comes back before the webhook arrives still sees the settled state. A manual payment adds a line saying an operator will confirm it by hand. See Orders and payments. Cancelling records the reason “cancelled by the customer in Telegram”. A cancellation the backend refuses re-renders the order screen unchanged rather than showing an error.

Wallet

/wallet or 👛 Wallet shows the spendable balance, the sentence “Wallet credit is applied to an order automatically unless you turn it off”, and the last ten ledger entries in the settlement currency, each as <date> · <type> · <signed amount>. Entry types read as credit, debit, order payment, refund, referral reward, correction, or expiry. The balance is the ledger total minus whatever is already reserved by a draft or pending order, floored at zero, so money committed to an unfinished purchase cannot be spent twice. When anything is reserved the screen says so — “Reserved by unpaid orders: X” — and that it comes back once the order is paid or cancelled under 🧾 My orders, because a balance that shrank without a word reads as money gone missing. See Wallet and ledger.

Topping up

➕ Top up wallet shows the current balance, Minimum: X · Maximum: Y, and — when the operator configured a rolling window — the remaining allowance for it. Presets appear two per row, and ✍️ Custom amount opens a free-text step. The amount is validated before the payment-method screen, so a rejection is explained early rather than after a payment attempt: below the minimum, above the maximum, over the window allowance, or simply unreadable (“That amount could not be read. Send a number, for example 500.”). Only presets the installation would actually accept are rendered. Free entry accepts , or . as the decimal separator, strips spaces, and refuses negatives. Then a payment method is chosen, the order screen appears, and the credit lands when the payment settles. 📜 Top-up history lists the last ten with their states and reopens a pending one. Over- and underpayment are resolved in the ledger: Omniflow credits exactly what the provider settled. See Wallet top-up and cart.

The saved cart

🛍 Save and top up on the checkout summary keeps the purchase instead of abandoning it, which is what makes an insufficient balance a pause rather than a dead end. The cart screen shows the plan, the total, the wallet balance, the add-on total, and one of four state lines: covered with automatic purchase on or off, or short by an amount with automatic purchase on or off. It also states when the cart is kept until — thirty days by default. The saved price is never reused. A cart is always quoted against the catalogue as it is right now, so a plan that changed price is re-priced and a plan that disappeared shows “This cart is out of date — clear the cart and build it again.” Buttons are 🛒 Buy now when the balance covers it, ➕ Top up wallet when it does not, 🤖 Automatic purchase on/off, 🗑 Clear, and ‹ Menu. Buy now shares the automatic sweep’s idempotency key, so tapping it while the sweep is running cannot charge twice. It works with automatic purchase off: that switch decides whether the cart may be bought while you are away, and a customer tapping the button is not away. Every other check — the cart has not expired, purchases are not paused, the balance covers the total — applies to both. There is one open cart per customer, shared with the shop: saving a shop item replaces a saved plan. A saved shop selection is never bought automatically, because a shop price is a provider quote that expires and charging one unattended would mean charging a number the customer last saw days ago.

The digital-goods shop

🛍 Shop appears when an operator has published at least one product. It sells Telegram Premium and Telegram Stars, separated into two lists because one is answered with a duration and the other with a quantity. These are not VPN entitlements: they never create or modify a subscription, and every screen speaks about the goods rather than about the payment — a customer asking where their Stars are is not asking whether their card was charged.
1

Open a product

The price is quoted here, not at confirmation, and the screen states how long it holds. If the provider price could not be taken, the screen says so and offers no buy button at all, so nobody is offered a purchase that would fail at the last step.
2

Name the recipient

Buy for someone else asks for an @username; Buy for me skips the prompt when Telegram published a handle for the account. Pasted t.me/… links and bare handles are both accepted and normalised, then checked against Telegram’s own rule of 5 to 32 characters starting with a letter.
3

Review and pay

The review restates the item, the amount, the price, any discount, the total, and the recipient on its own line immediately above the button that spends the money, followed by “Once sent, the delivery cannot be cancelled or taken back.” Buy for me still shows this step, because the account the bot knows about and the account the customer wants the Stars on are not always the same one. An order the wallet covers in full never reaches a payment provider.
A quote that expired between review and confirmation is re-taken rather than honoured. 🧾 My purchases lists the last ten with each order’s delivery state: awaiting payment, paid, sending, delivered, not delivered, under review, or refunded. A delivery under review adds:
We are checking this delivery by hand: the provider did not confirm the outcome. Your payment is safe — we will either complete the delivery or return the money to your balance.
The recipient’s handle is stored on the shop order, where delivery and support genuinely need it, and never on the customer record. Before the order exists it is held for a day on the review screen’s own record: each review is saved once, and its buttons carry only that record’s identifier, because Telegram allows a button 64 bytes and a product identifier with a username does not fit. An older review message therefore still buys exactly what it showed, even after a review for somebody else was opened. See Gifts and digital goods. 🏷 Promo code on the review screen opens a free-text step: send the code in one message and the review is re-rendered with the discount, or with the reason the code was refused. It is the same step the web panel offers for digital goods.

Gifts

🎁 Gifts lists the gifts you have sent, each as the last four characters of its code with the kind and status beside them, capped at ten. It offers 🎁 Gift a subscription, 👛 Gift wallet credit, and 🔑 Redeem a gift. The recipient does not need an Omniflow account beforehand. Gifting a subscription uses the ordinary plan catalogue rather than a separate gift catalogue, because two price lists can disagree. Gifting credit uses the operator’s configured top-up amounts; with none configured the screen says so and points at support. Either way the bot then asks for a short note of up to 200 characters. The note is optional: 🎁 Give without a note under the prompt buys the gift straight away, and sending a lone - does the same. A longer note is refused with “The note is longer than 200 characters”, and the prompt stays open under the same gift, so the shortened note completes it. The claim code is then shown once:
The code is shown once and not stored — save it and pass it on yourself.
Only the SHA-256 of the code is kept, so a database read never yields a redeemable code — and a sender who loses it cannot have it recovered, which is the deliberate trade. A gift the wallet did not cover adds “The gift becomes redeemable as soon as the order is paid”, with 💳 Pay — which opens the order’s method picker, so the sender chooses how to pay rather than being handed the first configured method — and a link to the order. Gifts are valid for thirty days, so the sender’s money is not held against an outcome indefinitely. Each gift flow is one gift. The flow is given its own idempotency key when the note is asked for, so a note Telegram delivers twice lands on the same order — and a second gift of the same plan to somebody else is a second order, not a replay of the first. Should a replay ever reach the screen, it says “This gift already exists”, names the code’s last characters, and links to the order; it never renders an empty code. 🔑 Redeem a gift takes the code and answers either “<amount> was added to your wallet” or “Your subscription is being activated — this takes under a minute.” Every refusal renders the same message: “This code cannot be redeemed. Check it, or ask the sender to issue a new one.” A recipient learning the difference between “no such code”, “already claimed”, and “expired” would also be an attacker learning which codes exist. Claiming is limited to five attempts a minute per Telegram account, which together with the code’s 80 bits of entropy is what bounds guessing. Claiming your own gift is refused.

Personal offers

✨ Your offer appears on the menu only when at least one offer is active. Up to five are listed, each with a countdown in whole days — “Expired”, “Last day”, or “N days left” — because an offer measured in hours reads as pressure rather than as information. Opening one shows its terms and its promo code, plus “The offer can be used once. Enter the promo code at checkout.” The code is shown rather than applied silently, so the customer can see what is being taken off and the checkout stays the single place a discount is computed. Opening an offer does not consume it: a customer who opens the catalogue and changes their mind has not used anything. Not interested dismisses an offer permanently.

Support

/support or 💬 Support lists up to ten conversations as • <subject> · <status> · <date> · N new, with ✍️ New request, an optional direct-contact button when the operator configured a support URL, 🔄 Refresh, and ‹ Back. The subject is the first line of the first message, truncated. Composing shows the rules before the message is written:
Send your text in one message. You may attach a screenshot or a document up to 10 MB. Never send passwords, tokens, or your subscription link. To cancel: /cancel.
A message is 1 to 4000 characters. Only photos and documents are accepted, at most five per message and each at most 10 MB; a video, voice note, animation, or sticker is refused outright rather than silently dropped. A message with an attachment and no text is stored with the body [attachment]; one with neither is refused.
Omniflow never downloads or stores the file. Only Telegram’s file reference and safe metadata — kind, filename, MIME type, size — are kept, and those references are purged after 90 days. A file uploaded through the web panel is held by the installation instead, which is why that surface needs a writable directory and the bot does not.
A thread shows the subject, status, last update, and up to twenty messages oldest-first, each prefixed 👤 You, 🛟 Support, or ⚙️ System, with attachments listed as 📎 <name> (<size>). A message is cut at 600 characters, and the screen keeps the newest messages that fit Telegram’s 4096-character limit, saying how many earlier ones it left out; the latest message is always shown. Before, a long conversation came to three times the limit and the request would not open at all. Every other screen is held to the same limit as a last resort: a text that would still be too long is shortened with … between tags rather than refused. The status reads open, waiting for you (the desk’s pending), resolved, closed, or merged, in the customer’s language. The buttons follow the same rules the web panel enforces: Reopening counts towards the ticket’s reopen tally and raises the operator’s unread badge, exactly as it does from the web panel, so a reopen from the chat appears as new work in the queue. A storage failure — the installation has no default queue, the database is down — is reported as “Support is unavailable right now” and the message is not saved; it is never blamed on the message’s length. When the request the customer was writing to turns out to be closed, merged, or gone, the compose session ends, so the next line typed goes to the menu rather than back into the same refusal. ✍️ New request continues the most recent open conversation rather than opening a second one — that is what a customer typing into the chat means. The ticket list and the web panel are where a second thread is chosen deliberately. The same reading applies to a message typed with no flow in progress. When a customer receives 🛟 Support reply and simply types an answer — text, or a photo or document with a caption — the bot appends it to their most recently active request that can still take a message (open, waiting for the customer, or resolved), provided that request was active within the last 72 hours, and confirms with ✅ Added to your request naming the subject, with an 💬 Open request button. Writing into a resolved request reopens it, and into a waiting one puts it back in front of the operator. A closed or merged request is never continued this way, and a customer with no recent conversation gets the main menu, as before. The confirmation names the request precisely because the customer did not choose it: if the bot guessed wrong, the button is one tap from the right thread. An operator reply arrives as a push message titled 🛟 Support reply with the subject and body and an 💬 Open request button. The unread counter is raised only after Telegram accepted the message, so a retried delivery cannot double-count. See Support desk.

News

📰 News is a read-tracked inbox of operator posts, categorised as 📰 News, 📢 Announcement, 🚨 Incident, or 🛠 Maintenance. An announcement pushed into the chat and then lost in scrollback cannot be re-read; the inbox makes it durable, and its read state drives the menu badge. Up to ten posts are listed newest first, each as 🔵 <title> with its category and date. Opening one marks it read. A newly published post also arrives as a push message with a 📰 Read button, at most once per customer. A post that was withdrawn, expired, or has no text in the customer’s language is invisible, and opening one from an old button answers “That post is no longer available.”

Settings and notifications

/settings holds every communication control a customer owns: the language switch, five per-kind notification toggles, marketing consent, quiet hours, and a shortcut to auto-renew. ✅ means on, ☑️ means off. The switches are per kind so that turning off traffic warnings does not also turn off renewal reminders. Marketing is off until the customer turns it on, and each change appends an immutable consent record naming the purpose, the policy version, and the fact that it came from the bot.
Service news is classified as marketing. A customer with Service news on and Marketing messages off receives no news posts. Announcement, incident, and maintenance posts are transactional and arrive regardless.
Quiet hours offers three choices only — 22:00–08:00, 23:00–09:00, and off — because an explicit list keeps callback data closed and avoids a free-text time parser. The window is evaluated in the customer’s own timezone and may wrap past midnight. The screen states the exception plainly: “Non-urgent messages wait during this window. Payment, activation, and support replies always arrive.” The zone is named beside the window — 22:00–08:00 · Moscow · UTC+03:00 — and 🌍 Time zone opens a list of about thirty common zones, each with its offset today. It writes the same users.timezone the web panel’s preferences screen does, and a zone set on the web that is not in the list is still shown by name. A customer still on the UTC default reads “No time zone is set, so UTC is used”, and when their Telegram language suggests a zone the picker asks about it first (“Your Telegram language suggests Moscow · UTC+03:00. Is that right?”) rather than assuming it. Everything that reaches the chat unprompted goes through one delivery policy — consent, per-kind preference, quiet hours, marketing cap, and Telegram reachability — evaluated in one place so two senders cannot disagree about whether a customer may be messaged. Notifications covers the full set of messages and the reasons one may be held back.

Auto-renew and saved payment methods

⚙️ Settings → ♻️ Renew states what will be charged, from where, and when, because those are the three things a person needs before agreeing to a charge they will not be present for. Nothing here is on by default. When no configured provider supports recurring charges for the settlement currency, the screen says so and offers nothing else: “No configured payment method supports recurring charges. We will remind you before expiry instead.” Today YooKassa is the only adapter that advertises recurring support, so auto-renew requires YooKassa to be configured. Auto-renew is a setting of one subscription, not of the account. On a single-subscription installation the screen is that subscription’s and the distinction is invisible. With concurrent subscriptions enabled, ♻️ Renew first asks which subscription to configure — “Choose the subscription to configure” — and the chosen one’s name heads the screen that follows; the same screen opens from ♻️ Auto-renew on a subscription’s own detail page. Every button on it names the subscription it changes, so arming one subscription never arms another. Off, the screen reads “♻️ Auto-renew is off — We will remind you 7, 3, and 1 day before expiry”, with Turn auto-renew on — offered only when the subscription holds a plan to renew — and 💳 Payment methods when any method is saved. On, it shows the plan, the method, Charged to: <wallet or card label>, the lead time, and lead-time buttons for 1, 3, and 7 days, plus a dunning notice when the last charge failed: “The last charge did not go through. We will try again automatically”, or, after several failures, “Automatic charging stopped after several failed attempts. Renew by hand or change your payment method.” The provider’s own failure code is deliberately not shown — “insufficient funds” and “declined” are operator vocabulary. The funding row offers Wallet always and Saved card only when a method is actually saved. With none, the screen says so instead — “No card is saved — the charge can only come from your wallet balance” — rather than rendering a button whose only outcome is a refusal. Today no code path saves a payment method, so on every installation the wallet is the only source a customer can pick; the saved-card controls exist for the day one is. 💳 Payment methods lists each saved method with its provider label, which one is the default, and its status, under the notice:
We store only a reference issued by the payment provider — no card number and no expiry date are kept.
Removing the last method promotes nothing, falls the funding source back to the wallet, and suspends auto-renew while keeping the consent record. Turning auto-renew off clears the plan, provider, currency, and consent but deliberately leaves saved methods alone — withdrawing agreement to automatic charging is not the same as asking for a card to be forgotten. See Recurring payments.

Referrals

🎁 Invite shows your referral code, the number of people invited, and the sentence “Rewards are configured by the service administrator.” 📨 Share invite opens Telegram’s share sheet with a t.me/<bot>?start=ref_<code> link, and the second button copies the full link rather than the bare code. Attribution is recorded the moment the invitee sends /start ref_<code>, before anything is rendered, and it is first-write-wins and permanent: the invited customer can only ever have one inviter. Self-referral is impossible by construction rather than by a check that could be bypassed. Two conditions apply: the referral programme has to be switched on, and the invitee has to be new — a customer with a paid order on record is not attributed, because a referral rewards bringing a customer in and a paying customer was not brought in by the link they tapped today. A code is minted once and stays stable. If Omniflow could not resolve the bot’s own username at startup, the share and copy buttons are omitted rather than pointing at a link that cannot work.
The bot’s referral screen shows the code and the invited count only. Qualified counts, earned rewards, remaining reward slots, and the programme’s terms are on the web panel. Loyalty tiers are not in the bot at all — there is no loyalty screen, badge, or command. See Referrals and loyalty.

/login — signing in on the web

/login mints a single-use link into the customer web panel, valid for ten minutes, and sends it as plain text:
🔐 Sign in to your account — This link works once and expires in 10 minutes. Do not forward it — whoever opens it signs in as you.
The flow starts in the chat rather than on a web form because a form would have to take some identifier and answer whether Omniflow knows it, and every version of that answer tells a stranger whether somebody is a customer here. The link is sent as text and not as a button because a Telegram inline button opens the in-app browser, which on several platforms keeps a separate cookie jar from the customer’s real browser — they would sign in somewhere they are not going to come back to. Three refusals are kept apart: link sign-in is not available right now (the operator has not enabled it, or the encryption key is not configured), too many sign-in links requested, and the link could not be created. Two independent limits apply — the bot’s own per-account budget and the identity service’s five links per hour per customer.

When the account is suspended or deleted

A customer an operator has suspended, or whose account has been deleted, sees one screen in place of every other:
⛔ Account suspended — Purchases and your subscription are on hold by the service administrator. Contact support and we will sort it out.
or, for a deleted account, “This account has been deleted. If that is a mistake, contact support.” It carries 💬 Support and, when the operator configured a support URL, 💬 Contact support. Every command, button, and free-text step answers with it — the menu, plans, wallet, orders, devices, link rotation, /login — except the support desk: the ticket list, opening a ticket, and writing into one keep working, because an operator suspends a customer to talk to them and the customer wants to talk back. This is the bot’s side of the rule the web panel applies with its 403 account_unavailable.

When purchases are not configured

The commerce surface is optional. On an installation where it could not be built, the bot keeps its original self-service behaviour — subscription, connect, devices, support, invite, settings — and every commerce route answers “Purchases are not configured yet. Please contact support.” Support then stores a single plain message with no attachments and no thread, and only the expiry and traffic notification toggles exist.

During maintenance

While an installation is paused, opening /plans or attempting any purchase, top-up, add-on, gift, or shop purchase shows:
🛠 Maintenance — Purchases and activation are paused. Orders you already paid for and active subscriptions are untouched, and your access keeps working.
with the expected return time when the operator supplied one, 🔄 Try again, and 💬 Support. The notice appears at the entry point rather than at the payment step, so a customer is told before they choose anything. Browsing is never blocked: order history, support, news, wallet, and existing access all keep working, and a maintenance lookup that itself fails never blocks a purchase — maintenance is an operator signal, not a fail-closed control. See Maintenance mode.

Rate limits and double taps

Every interaction is budgeted per Telegram account over a one-minute window. Exceeding a budget answers ”⏳ Too many requests. Please wait a moment and try again.” A rate-limiter outage fails closed for money-moving actions and open for navigation, so an infrastructure incident degrades browsing instead of allowing unbounded checkout attempts. Separately, every consequential button — paying, cancelling an order, choosing a provider, toggling the wallet, sending a Stars invoice, deleting devices, rotating a link, closing or reopening a ticket, enabling auto-renew, starting a top-up, buying or clearing a cart, buying an add-on, buying a gift without a note, confirming a shop purchase, changing an auto-renew setting — is claimed exactly once. A redelivered Telegram update or a second tap resolves to “This action has already been completed” rather than to a second order, payment, or deletion. Actions not covered by that claim carry their own idempotency keys instead. Two claims are taken, because Telegram delivers every tap as a new callback with an identifier of its own and the bot handles updates concurrently. The callback identifier is claimed for a day, which stops a redelivered update; the tap itself — chat, message, and button — is claimed for ten seconds, which stops a double tap. A claim on the identifier alone let two quick taps on 💳 Pay through together. An order a tap opens — a top-up, an add-on, a gift without a note — is also keyed on the tap, so the same button on the same message within ten minutes reaches the same order even if a duplicate got past the claim.

What the bot deliberately will not do

/ops — the one operator command

Telegram carries operator notifications and backup and restore actions only; every other administrative control lives in the web panel. /ops is here because backup and restore are what an operator needs when the web panel is the thing that is broken. Sent from an account named in the operator list, /ops shows the last five backups with their status, size, and timestamp — and nothing else: no connection details, no file paths, and no encryption material. 💾 Back up now takes a manual backup; ♻️ Restore latest… requires a second confirmation naming the backup, and warns that every change made since it was taken is lost. Each action writes an audit event whose actor is telegram:<id>. Sent from any other account, /ops produces no reply at all. Its buttons additionally require the commerce surface to be enabled; on an installation without it, they answer “Action was not completed”. See Backup and restore.

Health and metrics

The bot process exposes /healthz, /livez, /readyz, and /metrics on a separate listener, which stays off unless an operator sets its address — and refuses to share the webhook address, so internals are never published on an internet-facing host. Each update is counted by kind, and the kinds form a closed set so the metric label cannot grow without limit. No Telegram account, message body, or callback data ever becomes a metric label. Update spans are created but no tracer provider is installed anywhere in Omniflow, so nothing is exported — see Observability.