Appearance
Session extension (long-term session)
A long-term session ties your extension logic to the current broadcast for a bounded time. While it is active, the platform exposes a stable sessionId, remaining ttl, and session-scoped APIs (v1.session.*) and events (v1.session.updated, v1.session.whispered.model). Only one session can be active per model at a time.
This guide describes a typical session-based extension flow: the model starts a session, paying viewers join, clients exchange custom payloads through session whispers, and the model (or expiry) ends the session.
Quick choice: activity vs session
- Use
v1.ext.activity.requestfor short visual actions (reactions, short animations, brief status indicators) that do not need persistence. - Use
v1.session.*when the flow must survive reboots, remain consistent for newly joined users, or keep long-running shared state.
Shared slots and moderation
An active long-term session locks the viewer-facing overlay slot your extension uses for that flow, for example EXTENSION_SLOT_RIGHT_OVERLAY, or another overlay slot your manifest declares for that mechanic, depending on category and setup. While the session is running, no other extension can use that same slot on the broadcast; competing extensions that declare the same slot are blocked from showing their UI there until the session ends.
Because of that shared-resource impact, only adopt a session when your design truly needs stream-scoped coordination and APIs like v1.session.*. Prefer short, purposeful TTLs, end sessions promptly with v1.session.finish.model when the experience is over, and avoid holding a session open without a clear viewer benefit.
Moderation actively monitors appropriate use of sessions and shared overlay slots; misuse or unnecessary locking of shared UI may be subject to review under platform rules.
Session snapshot
v1.session.get always returns a session object (TV1Session):
When session is active and belongs to this extension:
owner: 'self'meta.sessionId— Platform session idmeta.ttlSec— Remaining lifetime in seconds
When session exists but belongs to a different extension:
owner: 'other'
When owner: 'other', your extension must treat the session as unavailable for its own flow (for example disable or hide the Join/Start controls).
When no session is active:
sessionisnull
Who can call what
| Method | Role | Purpose |
|---|---|---|
v1.session.join.model | Model only | Create/start the session when none is active. If a session is already active, this call does not recreate or replace it. Pass ttl (seconds, minimum 1). |
v1.session.join.user | Logged-in viewer | Join after a successful token payment; pass payment (TV1PaymentData) from the payment success path and ttl. If no session is active, this call can create one. |
v1.session.finish.model | Model only | Clear the session for this stream. |
v1.session.whisper.user | User only | Propose a session action; the model receives v1.session.whispered.model, validates, updates state, then typically broadcasts the result (e.g. v1.ext.whisper). |
v1.session.get | Either | Read the current snapshot. |
Session events
Subscribe with subscribe (and unsubscribe when disposing). Session-related events:
| Event | Who receives it | Purpose |
|---|---|---|
v1.session.updated | Model and viewers (every client with this extension on the stream) | Lifecycle updates: data.action is join or finish; data.session is the current TV1Session snapshot (or null when inactive); data.userId ties the update to a user when relevant (TV1SessionUpdate). |
v1.session.whispered.model | Model (broadcaster) only | A viewer called v1.session.whisper.user; payload is TV1SessionWhisperData. Validate the proposal, update authoritative state, then publish to the room (e.g. v1.ext.whisper). Viewers do not get this event. |
Model vs viewer: Viewers join participation through v1.session.join.user (after payment). If no session exists, either model join or user join can create it. Once active, the session is unique per model and cannot be recreated/replaced by another extension until it finishes. The active TTL is set by whoever created the session first (model or user).
Handling session state changes
Subscribe to v1.session.updated in the background slot both as a model and as a user. The background slot should normalize the session state by checking the TV1SessionUpdate object, and then notify other slots by using v1.ext.whisper.local.
Different session states
Carefully handle possible sesstion updates, especially by handling combinations of session being null, different session.owner and data.action values.
Model: start and stop
Model starting the session
This example flow shows how to start a session from the model UI. You don't need to use v1.payment.tokens.spend for the model to join, since they always have access to the session on their stream. Note, that you should handle v1.session.updated for both model and user clients to properly sync the UI on the stream.
After starting a session with v1.session.join.model, rely on v1.session.updated with action: 'join' for fresh session.meta.ttl and session.meta.sessionId (when session.owner === 'self'). See the following flow on how to restore the session state correctly on the user side, and how to sync it across different users.
Model finishing the session
The finish flow is simpler: you just need to call v1.session.finish.model from the model client, and handle v1.session.updated with action: 'finish' on all clients to update the UI and clear the state.
User: paid join
This flow shows how to join a session as a user. Note, that you should save the playing user in the storage as part of the joining proccess, in order to be able to restore the session state later and sync it across different users.
ts
extHelper.subscribe('v1.payment.tokens.spend.succeeded', async ({ paymentData, tokensSpendData }) => {
if (tokensSpendData.action === 'join-session-user') {
await extHelper.makeRequest('v1.session.join.user', {
ttl: '200',
payment: paymentData,
});
}
});Use any stable tokensSpendData.action value you define, as long as the spend handler and your UI agree on it.
Custom actions in-session
Viewers call v1.session.whisper.user with extension-defined data (e.g. a proposed move or vote). Only the model (broadcaster) client receives v1.session.whispered.model with that payload—use it to validate the action, apply rules, and update authoritative state. After that, publish the updated state to everyone (typically v1.ext.whisper to the room, plus v1.ext.whisper.local for model slots).
For coordination only between the model's slots (no viewer proposal), use v1.ext.whisper.local so you never rely on the viewer-only session whisper path.
The platform does not interpret your payloads—you define the schema (commands, state sync messages, etc.).
Reference architecture
A common pattern uses a background slot as the single authority for shared extension state:
- On load it reads
v1.ext.context.getandv1.session.get, decides whether the current client is the model, and hydrates state fromv1.storage.string.getusing a key derived fromsession.meta.sessionId(whensession.owner === 'self'), so a new session starts with a clean key. - It subscribes to
v1.session.updated: onjoin, it refreshes TTL and records the joining user id; onfinish, it resets local state. - It subscribes to
v1.payment.tokens.spend.succeededin the background slot and performsv1.session.join.userthere so the join completes in one place. - Viewer proposals arrive on the model via
v1.session.whispered.modelafterv1.session.whisper.user; model-only coordination usesv1.ext.whispered.local. The reducer updates state, persists to storage (model only), then broadcasts authoritative state withv1.ext.whisperto the room and syncs model slots withv1.ext.whisper.local. - Overlay and menu slots stay thin: they render state from
v1.ext.whisper/v1.ext.whispered.local, while controls callv1.session.join.modelonly to open a new session andv1.session.finish.modelto end it.
When the TTL counts down to zero, treat the session as ended locally and have the model client call v1.session.finish.model to clear the platform session (avoid duplicate finishes with a small guard).
Game session restoration
Use this flow to correctly initialize or restore extension state. The background slot should fetch the context, storage and session data, and verify that the current session belongs to this extension and that the current user participates in the game, then notify other slots using v1.ext.whisper.local. If the user closes and opens Main Games & Fun tab again, the UI should request the latest state again and update accordingly.
Checklist
- Use
v1.session.getbefore Start/Join UI decisions. Ifsession.owner === 'other', block controls for this extension. - If no session is active, either
v1.session.join.model(model) orv1.session.join.user(viewer after payment) can create one. - Only one session can be active per model. Once active, it cannot be recreated/replaced by another extension until it finishes.
- The active TTL is defined by the first creator (model or user) and applies while that session is active.
- Viewers pay, then join with
v1.session.join.user+paymentfrom the success event. - Keep authoritative state in one place (often background), key persisted state by
session.meta.sessionId(whenowner === 'self') if sessions must not bleed into each other. - Viewers propose actions with
v1.session.whisper.user; the model handlesv1.session.whispered.model, then pushes authoritative updates withv1.ext.whisper/v1.ext.whisper.local. Usev1.ext.whisper.localalone when only model slots need to coordinate. - End explicitly with
v1.session.finish.model(model) or let TTL drive your UX and still call it when the session should release the slot.
See also
- Requests — all
v1.session.*methods - Events —
v1.session.updated,v1.session.whispered.model - Entities —
TV1Session,TV1SessionUpdate - Slots communication — whisper patterns across slots