Troupe Cloud
Device sessions
A device session in Troupe Cloud holds one hosted simulator or emulator until you release it or it times out. This page covers starting one in the web app and over the API, watching and driving it from a browser, and its limits.
The same sessions, hosted
A cloud session is the session a local runtime gives you: sessions.create acquires a device, sessions.wait
waits for it, the device procedures drive it, and sessions.release ends it. Sessions
describes that life. Three things differ in the cloud:
- A session belongs to one project. People, keys and tokens that reach the project see it; nobody else does.
- When every matching device is busy the session is
queuedinstead of refused, and starts when one frees up. - An app comes from a build's artifact or from a URL, not from a path on the host.
What you can start
runtime.capabilities lists the platforms your caller can start a device on now, and the catalog lists the
device types and OS versions of each:
curl "https://api.troupe.ali.ve/v1/catalog/device-types?platform=ios-simulator" \
--header "Authorization: Bearer $TROUPE_API_KEY" \
--header "Troupe-Contract: 1.0"Platform ids are the runtime's, such as ios-simulator and android-emulator; the aliases ios and
android are accepted too. A spec no host can serve, such as a device type or an OS version the catalog does
not list, is refused with CAPACITY_UNAVAILABLE (no-matching-host).
In the web app
- Open a project and choose Start a device.
- Pick the platform, the device type and the OS version. To start with your app installed, paste a build's
artifact id (
art_…) into App artifact. - The session's page opens. While the device starts it says so; as soon as the stream is open you see the
screen and can click, drag and type on it, before the session is
ready. Rotation and actions on elements wait forready. - End session releases the device.
The page's side card shows the platform, the resolved device and OS, who holds the session, and the two deadlines: when it idles out and when it ends at the latest. The project page lists the project's sessions, live and ended.
Over the API
Start a session with a key that holds devices:acquire (and apps:install to install an app). A key for
every project names the project in the Troupe-Project header; see
API keys.
import { createClient } from '@alivelabs/troupe-client'
const troupe = createClient({
baseUrl: 'https://api.troupe.ali.ve',
token: () => process.env.TROUPE_API_KEY,
headers: { 'Troupe-Project': 'prj_…' }, // not needed with a key for one project
})
let session = await troupe.call('sessions.create', {
spec: { platform: 'ios-simulator', deviceType: 'iPhone 18 Pro' },
install: [{ artifactId: 'art_…' }],
})
while (session.status === 'queued' || session.status === 'starting') {
session = (await troupe.call('sessions.wait', { sessionId: session.sessionId })).session
}
if (session.status !== 'ready') throw new Error(`the session ended: ${session.reason}`)
const { sessionId } = session
await troupe.call('apps.launch', { sessionId, bundleId: 'com.example.shop' })
const shot = await troupe.call('media.screenshot', { sessionId }) // { bytes, contentType }
await troupe.call('sessions.release', { sessionId })sessions.wait answers after 30 seconds unless you pass another timeoutMs, with done: false when the
session is still starting, so the loop asks again. Every device procedure works on the session: the apps,
ui, input, system, media and logs groups are in the runtime reference,
and the client and calling the API directly show both ways
to call them.
Installing an app
install on sessions.create, and apps.install later, take one of two sources:
| Source | Meaning |
|---|---|
{ artifactId } | An installable artifact of a build in the same project |
{ url, sha256 } | A file the device host downloads, checked against its SHA-256 |
A path source is for a local runtime and is refused here with VALIDATION. A session installs at most five
apps at creation, while it is starting.
Watching and driving in a browser
The live stream is a WebSocket from the browser to the host that runs the device. A stream ticket opens it:
streams.ticket returns the listener's url and a ticket for one of two capabilities, watch or drive.
A watch ticket needs devices:read; a drive ticket also needs devices:drive.
The stream opens before the session is ready. Wait for it with sessions.wait and until: 'stream'; a
ticket asked for earlier is refused with SESSION_NOT_READY.
In your own page
Keep the API key on your server. The page needs only the ticket:
// On your server, for a session your user may see:
const { url, ticket } = await troupe.call('streams.ticket', { sessionId, cap: 'drive' })
// Send { url, ticket } to the page, and serve a route that mints the next one.In the page, DeviceScreen shows the device and sends pointer and keyboard input,
and asks your route for a fresh ticket when it reconnects. The live stream
describes the stream client underneath.
From Troupe Studio
Troupe Studio can sign in to Troupe Cloud and show a cloud project's devices as it shows local ones, with the inspector and the logs. Its sign-in is for one project, holds the five device scopes and lasts at most 12 hours.
From a coding agent
The device tools are served over MCP: see MCP in the cloud.
Limits and timeouts
| Limit | Value |
|---|---|
| Idle timeout | 10 minutes without activity; idleTimeoutMs on sessions.create sets another, up to 24 hours |
| Longest session | 60 minutes; maxDurationMs sets another, up to 24 hours |
| Time in the queue | 15 minutes, then the session ends errored (queue-timeout) |
| Time to start | 10 minutes, then the session ends errored (start-timeout) |
| Apps installed at creation | 5 |
One call's timeoutMs | Up to 300 seconds; 50 seconds for an MCP tool call |
| Devices at once | The organization's and the project's devices limit: see limits |
Activity is any call by the session's holder that names the session, and drive input on the stream. Watching
a stream and reading event streams are not activity. sessions.heartbeat keeps a session alive while your
program waits for something else. The session's expiresAt and maxEndsAt show both deadlines.
A session of a test worker (holder kind test-worker) lives by heartbeats instead: one every 10 seconds, and
the session ends 30 seconds after the last.
What sessions.create answers when it cannot start a device:
| Answer | Meaning |
|---|---|
A session with the status queued | Every matching device is busy; it starts when one is free |
QUOTA_EXCEEDED | A limit of the organization or the project is reached; the data names it |
CAPACITY_UNAVAILABLE | No host serves this spec now; retry after retryAfterMs |
PROJECT_REQUIRED | The caller reaches several projects and named none |
ORG_SUSPENDED, PROJECT_SUSPENDED | A limit of zero, or a suspension |
Releasing a session is never refused. Device time is metered from the moment a host takes the session until it ends, so release a device when you are done with it: see usage.