These endpoints create and control simulator sessions, install apps on a live device, and read session history. Authentication, error shapes and rate limits are covered in the REST API overview.
Sessions represent live simulator/emulator instances. The lifecycle is: create -> start -> use -> stop.
Whose session. A session belongs to the member who started it. Every endpoint below that acts
on a session or shows what is on it (start, control, install, launch, uninstall, list installed
apps, /agent/tools, presence, and AI test or suite runs against it) answers 403 to other members of your
organization. Organization admins may act on any session in the organization. A teammate the owner
has sent a collaboration link can act on that session within the link’s role
by sending the link’s token (the part after /collab/) in an X-Collab-Token header alongside
their own credentials: View only allows screenshots, the UI tree, logs and the installed-apps
list; Can control allows everything except opening links. GET /sessions/{id} stays readable
to every member, but it leaves out worker_stream_url for anyone who can’t watch the session and
tunnel_url for anyone who can’t control it. Only the owner or an admin can
stop a session. Sessions started from a share link or an embed key work
differently: every member can watch them, only Developers and Admins can control them, and only
the link’s or key’s creator (as a Developer or Admin) or an admin can stop one. See
Team.
Session lifetime. A session stays up while something holds it (a browser tab, vibeview dev,
the MCP server, API calls, or a test run) and someone is using it. Use is an action on the
device: /control, /install-app, /install-build, /launch-app, uninstalling an app, the debug
endpoints, /agent/tools calls that drive the device (taps, swipes, typing, buttons, opening links,
rotating, relaunching the app, and so on), test steps, app reloads from vibeview dev, and
speaking into the microphone. POST /sessions/{id}/presence and the /agent/tools reads
(ui_tree, screenshot, logs, find_element and wait) only show that a client is there: they
keep a session held but are not use. GET /sessions/{id} is neither: it doesn’t hold a session,
so a script that only watches a session should poll POST /sessions/{id}/presence instead. A
session nobody uses ends after your organization’s idle timeout (Settings › Session
defaults; 15 minutes by default, and never less than five minutes for a session with no browser
tab open), whatever holds it. A session that was started but never reached by any client is ended
a few minutes after it comes up. A session held by vibeview dev (in the foreground or with
--detach) or by the MCP server’s dev_start also ends within about 90 seconds once that process
stops checking in (it was killed or crashed, or the computer went to sleep), unless a browser tab
or a test run is still holding it. From about a minute before an idle end, the session’s responses
carry idle_warning (about a minute before the end; see Session Timeout).
Create Session
POST /api/v1/sessions/
Auth: Bearer token required.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
device_type | string | Yes | ios, android or roku. Apple TV is ios and Android TV is android, both with device_category: "tv". |
device_category | string | No | phone, tablet, or tv. Omit for any handheld. A category alone never allocates a foldable; ask for one with model, or with its device_id on Start Session. |
specific_device_id | string | No | Start on this device (a device_id from List Devices). Create checks that it exists and is free, and fails with 404/409 if not; Start Session then starts on it, unless you pass a different device_id there. It is not reserved in between: if someone else takes it first, Start queues for it. |
model | string | No | Start on any free device of this model (a model from List Device Pools). Use with os_version. |
os_version | string | No | The pool’s os_version (for example iOS 26.4), used with model. A bare version (26.4) is accepted when it names exactly one pool value; a value no device has, or one that could mean two, fails with 422 and lists the valid values. |
app_id | string | No | Public id of an app to install before the session becomes usable |
build_id | int | No | Install this exact build of app_id instead of the latest. A build_id that is not a build of app_id, or one sent without app_id, is ignored. A call made with an API token (or through the MCP server) gets a warnings entry that says so and lists the app’s newest builds. On a session that doesn’t need the app, an app_id that matches none of your apps is reported the same way; a Roku session refuses it with 400, and vibeview dev stops with an App not found message. |
launch_params | object | No | Flat JSON object applied when the pinned app launches. Ignored without a resolvable app_id. |
Response: A session object with status: "pending" and no device yet.
Errors:
400—app_idis an app for another platform thandevice_type(for example an iOS app on anandroidsession), orspecific_device_idis a device of another platform. A TV app (Apple TV, Android TV) on a phone or tablet, or a phone app on a TV, is refused the same way, whether the device is named or asked for withdevice_category. The message names both.404— Specific device not found.409— Specific device is not available.410—build_idis a build ofapp_idthat has expired under your plan’s build retention.detail.errorisbuild_expired;messageandnewer_build_idname the app’s newest build that hasn’t expired (see Build retention). The session isn’t created, and the latest build is never installed instead.422—modeloros_versionmatches no device (oros_versionis ambiguous). The message lists the valid values.
Start Session
Boot the simulator and begin streaming. Must be called after creating a session.
POST /api/v1/sessions/{session_id}/start
Auth: Bearer token required.
Request body (optional):
| Field | Type | Required | Description |
|---|---|---|---|
device_id | string | No | Start on this specific device: a device_id from List Devices. Overrides the specific_device_id the session was created with. |
Without device_id, the session starts on the specific_device_id it was created with, if any. Otherwise any free device matching the session’s device_type and device_category (or its model and os_version, if you created it with them) is used.
Response: A session object with status updated to starting and the device fields filled in.
If no matching device is free, the request returns 202 Accepted and the session is queued:
{
"status": "queued",
"queue_entry_id": 42,
"position": 2,
"priority": "low",
"session_id": "uuid-string",
"queue_reason": "devices_busy"
}
priority is high on paid plans and low on Free. When every device is busy, paid plans’ requests start first, so a Free request’s position can go up.
queue_reason says why the session is waiting, or is null when that can’t be told:
| Value | Meaning |
|---|---|
devices_busy | Every matching device is in use. |
devices_updating | The matching devices (or the one device you asked for with device_id) are being updated or restarted, or reset after the previous session ended, or a free device is about to be handed to you. A session starts shortly. |
plan_limit | Your organization is at its plan’s concurrent session limit. |
link_limit | A share link’s own concurrent session limit is reached (share and embed sessions only). |
The session starts automatically when a device frees up. Use the queue endpoints below to leave the queue.
Errors:
400— Session already started, ordevice_idis a device of another platform than the session’s.404— Session not found, or no device has thatdevice_id.
Leave Device Queue
Remove a queued session start from the device queue. Two equivalent forms are provided; the POST variant exists for navigator.sendBeacon compatibility (leaving the queue on page unload).
DELETE /api/v1/sessions/queue/{queue_entry_id}
POST /api/v1/sessions/queue/{queue_entry_id}/leave
Auth: Bearer token required. Only the user who queued the entry can remove it.
queue_entry_id is the integer returned in the 202 queued response from Start Session.
Response:
{
"status": "ok"
}
Errors:
404— Queue entry not found or already processed.
Get Session
GET /api/v1/sessions/{session_id}
Auth: Bearer token required.
Response: A session object.
Session Object
Every endpoint that returns a session uses this shape. Fields that don’t apply yet (for example the device fields before the session starts) are null.
{
"id": 1,
"session_id": "uuid-string",
"device_type": "ios",
"device_category": "phone",
"status": "active",
"device_id": 42,
"device_name": "iPhone 16",
"device_udid": "ABCD1234-...",
"device_model": "iPhone 16",
"os_version": "iOS 18.0",
"user_email": "dev@example.com",
"origin": "user",
"worker_stream_url": "wss://...",
"tunnel_url": null,
"start_time": "2026-09-25T10:00:00",
"end_time": null,
"duration_minutes": null,
"failure_reason": null,
"end_reason_code": null,
"idle_timeout_s": null,
"warnings": null,
"pinned_install_dispatched": true,
"pinned_install_confirmed": true,
"app_installed_at": null,
"app_install_pending": false,
"idle_warning": null,
"pinned_app_public_id": "my-app-x7k2",
"pinned_app_name": "My App",
"pinned_build_id": null,
"app_sdk_version": "18.0",
"device_capabilities": { "appearance": true, "textSizeSteps": 12, "...": "..." },
"posture": null,
"form_factor": null,
"display_metrics": { "chrome": "phone13", "scale": 3, "screen": { "width": 390, "height": 844 }, "corners": { "ul": 47.33, "ur": 47.33, "ll": 47.33, "lr": 47.33 } },
"embed_app_ready": false,
"embed_app_readiness_revision": 0
}
| Field | Description |
|---|---|
id | Numeric session id. |
session_id | Session identifier used in every /sessions/{session_id} path. |
device_type | ios, android or roku. |
device_category | phone, tablet or tv. null means any handheld. |
status | pending (created, not started), starting (a device is booting), active, stopping (being shut down), stopped or failed. |
device_id | Numeric id of the allocated device. |
device_name, device_model, os_version | The allocated device’s name, model and OS version. |
device_udid | The allocated device’s string identifier (its device_id in List Devices). |
user_email | Who started the session. |
origin | Where the session came from: user (the dashboard or API), embed (an embedded player), share (a public app link) or runner (a CI or AI test run). List Sessions and Get Session work this out; the other endpoints report user. |
worker_stream_url | Address of the session’s live video stream. Treat it as opaque. |
tunnel_url | Set only for vibeview dev sessions that carry a Metro connection. |
start_time, end_time, duration_minutes | Timing. end_time and duration_minutes are set once the session ends. |
failure_reason | Why a failed session failed, for example a queue entry that expired before a device became free, or a device that did not start. null otherwise. |
end_reason_code | Why the session ended, once it is stopped or failed; null before that. The same code as the session.ended webhook’s reason_code and the SDK’s session:ended event, for example stopped, stopped_by_admin, inactivity_timeout, no_client, worker_lost, device_crashed, device_turned_off or device_restarted (see Webhooks). Other codes can appear. |
idle_timeout_s | For a session ended with end_reason_code inactivity_timeout: how long it had been idle when it ended, in seconds (the idle timeout that applied). null for any other end and while the session is live. |
warnings | Messages worth showing the user, such as a plan usage warning or an app install failure. |
pinned_app_public_id, pinned_app_name, pinned_build_id | The app (and exact build, if one was chosen) the session was created with. pinned_build_id is null for the latest build. |
pinned_install_dispatched | true when the session’s app was sent to the device for install as part of starting it. |
pinned_install_confirmed | Nullable boolean for installation during startup: true means installation was confirmed; false means an installation failure was reported; null means no acknowledgement is available, including before startup or when no installation was requested. |
app_installed_at | When the app was confirmed on the device, for devices that install it after the session goes live (Roku). |
app_install_pending | true while the session is live but its app is not on the device yet. Anything that drives a session automatically should wait for this to be false, not just for status: "active". It is only ever true on Roku, where the channel installs after the session goes live. |
idle_warning | null, or {"seconds_remaining": 42, "reason": "inactivity"} from about a minute before the idle timeout ends the session. Set by Get Session and POST /sessions/{id}/presence while the session is active; null elsewhere. /agent/tools results carry the same field, set only for the reads (ui_tree, screenshot, logs, find_element, wait) and always null for a tool that drives the device, since that tool is use. Once someone uses the device, it goes back to null. |
app_sdk_version | The platform SDK the session’s app build was compiled with, when known. Returned by Get Session only. |
device_capabilities | Which device settings controls this device supports. null until the session goes live. |
form_factor | foldable for a device whose display folds, otherwise null. |
posture | For a foldable: closed, partial or open. Otherwise null. |
display_metrics | Screen geometry of the allocated device; see List Devices. |
embed_app_ready, embed_app_readiness_revision | Used by embedded players to know when the app is in the foreground. |
webrtc_room_id, stream_url | Deprecated: use worker_stream_url. They will be removed in a future API version. |
status: "active" and app_build_id alone do not prove installation succeeded: the build identifies the intended pinned or resolved build. Check pinned_install_confirmed together with session state and the selected app/build identity. A true acknowledgement does not establish foreground readiness or track later manual app changes. Roku still uses app_installed_at and app_install_pending for its post-activation installation.
List Sessions
GET /api/v1/sessions/
Auth: Bearer token required.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by status: pending, starting, active, stopping, stopped, failed. Repeat it to match several (?status=pending&status=starting). Any other value returns 422. |
device_type | string | Filter by ios, android or roku (Apple TV is ios and Android TV is android) |
platform | string | Filter by ios (iPhone and iPad), tvos (Apple TV), android (phones and tablets), androidtv or roku. Repeatable. Any other value returns 422. |
source | string | team (the dashboard, agents and vibeview dev), visitors (share links and embeds) or runs (test runs and CI). Any other value returns 422. |
app_id | int | Only sessions that started with this app (numeric app id) |
user_id | int | Filter by user ID |
time_range | string | One of last-hour, last-day, last-week, last-month |
search | string | Part of a session ID or a user’s email |
limit | int | Max results (default 50, max 200; out of range returns 422) |
offset | int | Pagination offset (default 0) |
Response: Array of session objects, newest first, each with these extra fields:
| Field | Description |
|---|---|
source | dashboard, agent, dev_loop, test_run, ci, share_link or embed. |
source_label | The same in words, as the Session history page shows it, for example Share link "Atlas link". |
who | The team member’s email, the API token’s name, Visitor for share-link and embed sessions, or null. |
share_link_id, share_link_label, embed_key_id, embed_key_label | The share link or embed that opened the session. |
source_deleted | true when that share link or embed no longer exists. |
app | {id, public_id, name, platform, icon_url} of the app the session started with, or null. |
build_version | The build’s version and build number, for example 1.2.0 (45). |
run_session, run_public_id | Whether a run created the session, and the run that used it (open /tests/runs/{run_public_id}); run_public_id is null unless source is test_run or ci. |
token_label | The name of the API token that started it. |
end_reason_label | end_reason_code in words, once the session has ended. |
dispatched_at, agent_commanded_at, run_app_opened_at | When the session got its device, when an agent first drove it, and when a run first opened its app. null when it did not happen. |
Run a Test on a Session
Run a saved test, or every test of a group, on a session you already have, for example the one you are driving on the Devices page or from a coding agent. The call returns at once; open run_url, or poll the run, to follow it. To run on devices of their own, use Start a run instead.
POST /api/v1/sessions/{session_id}/runs
Auth: Bearer token required. Developer role or above.
| Field | Type | Required | Description |
|---|---|---|---|
test_id | string | One of the two | public_id of the test to run |
group_id | string | One of the two | public_id of the group to run |
start_from_current_screen | boolean | No | No reset before the first test, so it starts where the device is now. Later tests of a group use their normal start state. Default false |
stop_on_first_failure | boolean | No | For a group: after a test fails, skip the rest. Default false |
mode | string | No | hybrid (Smart), replay (No AI) or ai (AI only) |
model | string | No | Model override |
provider | string | No | Provider override |
environment | string | No | Name or id of one of the app’s environments |
Pass test_id or group_id, not both.
Response:
{
"kind": "test_run",
"run_id": 42,
"run_public_id": "tr_abc123",
"run_url": "https://vibeview.io/tests/runs/tr_abc123",
"status": "started"
}
kind is suite_run for a group. Starting a run counts as use of the session, and the run’s steps keep it in use while they run.
Stop Session
Stop the simulator and release the device.
DELETE /api/v1/sessions/{session_id}
Auth: Bearer token required.
Response:
{
"status": "ok"
}
Export Sessions (CSV)
GET /api/v1/sessions/export/csv
Auth: Bearer token required.
Returns a CSV file download with session history. It takes the same filters as List Sessions (except limit and offset); with none, it exports every session.
The columns are session_id, user_id, device_type, status, start_time, end_time and duration_minutes, then user_email, who, source, source_label, app, app_platform, build_version, device, os_version and end_reason.
On share-link and embed rows, user_id and user_email are the owner of the link or embed, not the visitor; who is Visitor.
Control Session
Send input commands to the running simulator.
POST /api/v1/sessions/{session_id}/control
Auth: Bearer token required.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
action | string | Yes | One of: tap, swipe, rotate, shake, button, key, screenshot, touch_start, touch_move, touch_end |
x | float | No | X coordinate in pixels of the streamed video (0 - 10000) |
y | float | No | Y coordinate in pixels of the streamed video (0 - 10000) |
x2 | float | No | End X coordinate in pixels (for swipe) |
y2 | float | No | End Y coordinate in pixels (for swipe) |
duration | int | No | Duration in milliseconds |
rotation | int | No | Rotation angle: 0, 90, 180, 270 |
button | string | No | iOS button: HOME, LOCK, SIRI |
keycode | string | No | Android keycode: BACK, HOME, MENU, DPAD_UP, etc. |
Response:
{
"status": "ok"
}
Install App (Upload)
Upload and install an app binary directly into the running session.
POST /api/v1/sessions/{session_id}/install-app
Auth: Bearer token required.
Request: multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
file | file | Yes | App binary (.zip/.tar.gz/.tgz iOS simulator bundle, .apk for Android) |
launch_after_install | boolean | No | Launch app after install (default: false) |
Install Build
Install an app from an existing build record (no file upload needed).
POST /api/v1/sessions/{session_id}/install-build
Auth: Bearer token required.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
build_id | int | Yes | ID of the build to install |
launch_after_install | boolean | No | Launch app after install (default: true) |
grant_permissions | boolean | No | Auto-grant permissions (default: true) |
params | object | No | Flat JSON object applied when the app launches |
Errors:
400— The build is of an app for another platform than the session’s device, or for a TV when the device is a phone or tablet (or the reverse). The message lists your newest builds that run on this device.404— Your organization has no such build. The message lists the session’s app’s newest builds (or, for a session with no app, your newest builds that run on this device).410— The build has expired under your plan’s build retention:detail.errorisbuild_expired, withmessageandnewer_build_id(see Build retention). Nothing is installed.
Launch App
Launch an already-installed app on the device.
POST /api/v1/sessions/{session_id}/launch-app
Auth: Bearer token required.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
package_name | string | Yes | App package identifier |
activity_name | string | No | Android activity to launch |
params | object | No | Flat JSON object applied when the app launches |
List Installed Apps
Request the list of installed apps on the device. The result is delivered asynchronously via WebSocket.
GET /api/v1/sessions/{session_id}/apps
Auth: Bearer token required.
Uninstall App
DELETE /api/v1/sessions/{session_id}/apps/{package_name}
Auth: Bearer token required.
Response:
{
"status": "ok",
"message": "Uninstalling com.example.myapp"
}