Skip to main content

The channel test-account model: per-channel shared ladders

Some interactive messaging channels on Orbit ship a shared test account — a channel asset Devotel owns that you bind to one verified recipient so you can send real traffic before you own a production asset. Critically, the model is per-channel, not universal: Telegram ships it, RCS uses it during approval, and WhatsApp ran it until 2026-10-08, when the shared demo WABA was retired. This page defines the model and marks where each channel stands today. The ladder is distinct from dv_test_sk_* test mode: test mode decides whether a send is simulated (persisted, never dispatched, no provider reached), while a shared test account is a real delivery path — messages actually reach the provider under Devotel’s channel asset. Both exist so you can evaluate in the way that fits: simulation for integration shape, real delivery for end-to-end proof — on the channels that still ship a shared asset.

Which channels still run a shared test account

  • Telegram — ships it. The production asset is an unregulated BotFather bot, but the graduation step is still a paste-a-token action, so the shared demo bot remains useful as a real-delivery warm-up. Telegram keeps the full ladder below.
  • RCS — uses it during approval. The production asset is a Google-approved agent, and approval takes days. While your agent application is pending, the platform’s approved demo agent carries real sends for verification, exactly the warm-up role the WhatsApp demo used to play. Once Google approves your agent, the shared lane ends.
  • WhatsApp — retired 2026-10-08. Meta’s approval friction no longer justifies a shared WABA: every organization now connects its own WABA through Embedded Signup, and the demo endpoints answer 410 GONE instead of OTP or send traffic. See the 2026-10-08 changelog entry and the WhatsApp channel page for the current error surface.
Graduation, on every channel that ever runs the ladder, terminates in the same place: your own asset connected, the same API surface, your own branding and full capability.

The three-mode state machine, per channel

On a channel that ships a shared test account (Telegram today; RCS during approval; WhatsApp before retirement), your organization sits in exactly one mode at a time. Telegram makes the modes explicit: On WhatsApp the table reduced to two rows after retirement: not_connected (no WABA connected — any send is refused 422 CHANNEL_NOT_CONFIGURED before a message row is written) and production (your WABA connected). The test_verifying/test_active rows exist there only for organizations with a legacy verified link, and GET /api/v1/whatsapp/get-status reports them as retired: true — readable, permanently inert. The transitions, and who advances each (Telegram used as the worked example):
  • Start verification — you call POST /api/v1/telegram/test/start-verify, which mints a single-use linking token (five-minute TTL) and returns a deep link plus the same value to render as a QR code. Opening the link and pressing Start redeems the token; the inbound webhook binds that chat to your organization and advances the mode to test_active.
  • Paste a token — you call POST /api/v1/telegram/connect-bot with the BotFather token. The token is validated, encrypted, and the webhook subscription registered. The mode becomes production. Production wins over test: when both exist, the state machine reports production and the test block drops out of the response.
  • Disconnect — POST /api/v1/telegram/test/disconnect clears the verified-chat link (back to not_connected), and POST /api/v1/telegram/disconnect-bot revokes the webhook registration and retires the BYO row.

The two gate classes

On channels with a shared ladder, every capability falls into one of two gate classes. This is the part of the model your code encounters as 403s.

Send-permitted — reachable from both test and production

The send gate permits the call when either the organization owns a connected asset or the organization has an active test account. It answers one question: is this organization allowed to push traffic on this channel at all? Dashboards and preflight checks use it so they fan out without duplicating the test-mode allowance logic. In test_active, per-send enforcement still applies underneath: sends go only to the verified recipient, against the curated demo allowlist, bounded by a daily cap. On WhatsApp after 2026-10-08 this gate has only one branch: own-WABA-connected. There is no second branch — nothing treated as permitted on the shared asset remains.

Asset-owner-only — production only

The ownership gate rejects with 403 FORBIDDEN unless the organization owns the channel asset. Telegram groups these Bot API features in it, because each one mutates the bot as an asset or operates the bot’s economy:
  • setMyCommands beyond the curated demo command set — the shared bot carries a demo command list; redefining it is owner-only.
  • setChatPhoto, setChatTitle, setChatDescription — chat identity mutations.
  • Payments: sendInvoice, answerPreCheckoutQuery, answerShippingQuery.
  • Business connections.
  • Sticker-pack management.
  • getMe / setMyName mutations — anything that changes the bot identity itself.
On WhatsApp and RCS the same gate covers WABA-/agent-bound assets: template CRUD and approval submission, flow publish, catalog product CRUD, WABA-bound calling. Meta ties asset ownership to the WABA owner, so a shared WABA could never be the asset-owner-of-record for a tenant — one of the reasons the shared lane came down. You meet the ownership gate as a 403 FORBIDDEN with a connect-your-own message. On Telegram it is “Connect your own Telegram bot to use this feature”; on WhatsApp “Connect your WhatsApp Business account to use this feature”. Both are permanent, deterministic responses: connect the asset and the same call passes.

The shared-asset identity lock, and the $0 ledger rows

Two consequences of running on an asset Orbit owns (applies to whichever channels still ship one): Identity mutations are a 403 even for an active test organization. A shared asset has one identity serving every evaluating tenant. If test_active organizations could rename it or redefine its commands, one tenant’s experiment would be every tenant’s asset. So the ownership gate does not ask “are you allowed to send?” (the send gate does) but “do you own this asset?” — and on a shared-lane organization the answer is always no. Zero-cost rows exist on purpose. Telegram’s Bot API is free at the provider level, so every Telegram cost-ledger row is $0.00 — but the row is written anyway, marked tier: "test" when the send rode the shared bot. Three reasons:
  • Cost analytics stay complete. The per-channel cost rollup aggregates message prices by channel; without an explicit $0 row, Telegram volume would vanish from the chart and skew per-message-cost denominators.
  • Dev capacity is measurable. Test-tier rows let the platform account for the shared asset’s operating cost.
  • Future paid features have a wiring point. When a paid Telegram surface lands, the tiers and consumers already exist.

What your code branches on

One read, one contract, no code change between test and production — on channels that still have a ladder:
  • GET /api/v1/<channel>/channel-state is the single source of truth for where your organization sits. On Telegram it reports mode, the shared bot username, the daily test-send counter and limit, the verified chat when test-linked, and the pending verification token while verifying. The state read is deliberately degrade-safe: if an upstream fault hits it, Orbit returns the well-defined not_connected empty state rather than a 5xx, so your dashboard card renders “Connect Telegram” instead of an error.
  • Branch on the returned mode, never on your own record of what you connected. The per-mode capability matrix is exactly the gate table above: test_active gets send-permitted features to the verified recipient; production gets both gate classes.
  • The same endpoint routes differently per mode. POST /api/v1/telegram/test/send dispatches: on test_active it sends via the shared bot to the verified chat and counts the daily cap; on production it sends through your own bot. Your integration’s send calls are identical before and after graduation.
On WhatsApp the same discipline applies with a simpler matrix: read channel-state, treat anything but production as “connect a WABA”, and never construct a test_active branch in new code — the retired endpoints answer 410 GONE the moment you call them.

Cross-references