Instance spec and status
Every create call takes metadata and spec, and every instance reports its URLs and state in status. The spec tables cover iOS simulators and Android emulators; the lifecycle, status, timeout, and placement rules apply to Xcode and Gradle sandboxes too. Build-specific settings for sandboxes live in Build with Xcode and Build with Gradle. For what the fields mean in practice, start with Concepts; for the calls themselves, see SDKs and REST API.
Lifecycle states
Every iOS, Android, Xcode, and Gradle instance moves through the same lifecycle: creating while hardware is provisioned, assigned while it boots, ready when the URLs in status are usable, and finally terminated.
creating ──┬──► assigned ──► ready ──► terminated
│
└──► (error) ──► terminated (status.errorMessage set)| State | Meaning |
|---|---|
unknown | Default placeholder; not returned for live instances. |
creating | Provisioning. URLs in status are not populated yet. |
assigned | Hardware is assigned and the instance is booting. |
ready | Fully booted. All URLs are populated, and a control client can connect. |
terminated | Stopped by an explicit delete, the inactivity timeout, the hard timeout, or an error. |
status.errorMessage is set when the instance terminated because of an error. A create call with wait: true returns once the instance is ready.
To receive notifications when an instance is created, becomes ready, or terminates, configure instance webhooks. They support all four instance types. Ready events can optionally include the instance object and its access token.
Status fields
A ready instance carries the URLs its data plane needs in status. Every URL authenticates with the instance token, never the API key.
| Field | iOS | Android | Xcode and Gradle | Use |
|---|---|---|---|---|
state | ✓ | ✓ | ✓ | The lifecycle state. |
token | ✓ | ✓ | ✓ | Instance token for apiUrl, endpointWebSocketUrl, adbWebSocketUrl, and mcpUrl. Embedded in signedStreamUrl. Returned only to a credential that can control the instance; a read-only API key or a viewer seat gets no token. |
streamToken | ✓ | ✓ | Streaming-only token: opens endpointWebSocketUrl and nothing else, so a browser can hold it while status.token stays on your backend. Returned with token; get and list return it only while the instance runs. | |
apiUrl | ✓ | ✓ | ✓ | HTTP base for the instance's daemon; pass it to the SDK and CLI control clients. On Android, the control WebSocket derives from it as apiUrl + /ws, and recording downloads use the same base. |
signedStreamUrl | ✓ | ✓ | Console-hosted watch URL with the token in the URL fragment, so the token never reaches a server. Opens in a browser without an API key or sign-in. | |
endpointWebSocketUrl | ✓ | ✓ | WebSocket for the <RemoteControl /> component; pass ?token=<status.token>. | |
mcpUrl | ✓ | ✓ | The instance's MCP HTTP endpoint. Authenticate with status.token as Authorization: Bearer. See Per-instance MCP server; for one endpoint that also creates instances, see the remote MCP server. | |
adbWebSocketUrl | ✓ | ADB tunnel endpoint. Pass it as adbUrl to createInstanceClient. | ||
targetHttpPortUrlPrefix | ✓ | ✓ | Prefix for reaching HTTP services that run on the device. Append the target port, with no slash, to forward traffic; Appium uses it to reach WebDriverAgent. | |
sandbox.playwrightAndroid.url | ✓ (when enabled) | Chrome DevTools Protocol URL when spec.sandbox.playwrightAndroid.enabled is true. See Playwright. | ||
errorMessage | ✓ | ✓ | ✓ | Set when the instance terminated because of an error. |
terminationReason | ✓ | ✓ | ✓ | Present once state is terminated, never before. Known values: UserRequested (a delete request), InactivityTimeout, HardTimeout, and Unknown (a cause the platform did not attribute, including instances that failed to become ready; see errorMessage). New values may be added, so treat unrecognized ones as Unknown. |
The iOS and Android status blocks otherwise have the same shape. Reaching a server that runs inside the simulator (through targetHttpPortUrlPrefix) is the opposite direction from letting the device reach your machine; for that, see Connect to local services for iOS or Android.
Example create response
A successful Android create with wait: true returns the full instance resource. An iOS response has the same shape without adbWebSocketUrl:
{
"metadata": {
"id": "android_usw1_01krxvphn8e3rbeq0zewgw209a",
"createdAt": "2026-05-18T19:52:42Z",
"organizationId": "org_01kr52j...",
"displayName": "android_usw1_01krxvphn8e3rbeq0zewgw209a",
"labels": { "app": "demo", "session": "docs" }
},
"spec": {
"inactivityTimeout": "10m0s",
"region": "us-west2"
},
"status": {
"state": "ready",
"apiUrl": "https://us-west2-1234567.limrun.net/v1/android_usw1_01krxv.../api",
"adbWebSocketUrl": "wss://us-west2-1234567.limrun.net/v1/organizations/org_01kr52j.../android.limrun.com/v1/instances/android_usw1_01krxv.../adbWebSocket",
"endpointWebSocketUrl": "wss://us-west2-1234567.limrun.net/v1/organizations/org_01kr52j.../android.limrun.com/v1/instances/android_usw1_01krxv.../endpointWebSocket",
"signedStreamUrl": "https://console.limrun.com/signedStream?url=wss%3A%2F%2Fus-west2-1234567.limrun.net%2F...%2FendpointWebSocket#token=lim_...",
"mcpUrl": "https://us-west2-1234567.limrun.net/v1/android_usw1_01krxv.../mcp",
"targetHttpPortUrlPrefix": "wss://us-west2-1234567.limrun.net/v1/organizations/org_01kr52j.../android.limrun.com/v1/instances/android_usw1_01krxv.../targetHttpPort",
"token": "lim_...",
"streamToken": "lim_..."
}
}The metadata and spec fields in the response:
| Field | Use |
|---|---|
metadata.id | Stable instance identifier. Pass it to get, delete, and connect. |
metadata.labels | Echoed back. Drives reuseIfExists matching and label-selector listing. |
metadata.displayName | Human-readable name shown in the console. |
spec.region | The region the scheduler placed the instance in. |
spec.inactivityTimeout | Returned as a Go duration string ("10m0s"), not the "10m" you passed. |
Create parameters
Every create call, for any instance type, accepts these parameters next to spec:
| Field | Type | Meaning |
|---|---|---|
wait | boolean | When true, the call returns only after status.state === 'ready'. If the response is not ready yet, poll or receive an instance.ready webhook. Default false. |
reuseIfExists | boolean | When true, return the existing instance whose metadata.labels match exactly instead of creating one. The lookup runs in the region that handles the create, so a placement change can miss. Default false. |
metadata.labels | Record<string, string> | Free-form key=value map. Powers reuseIfExists, labelSelector listing, and bulk cleanup. See Labels. |
metadata.displayName | string | Human-readable name shown in the console next to metadata.id. |
Timeouts
Two timeouts end an instance without a delete call. Both take a duration string such as 1m, 10m, or 3h.
spec.inactivityTimeoutterminates the instance after this long without activity. The timer starts once the instance isready. Omitted or'0', it uses your organization's default for the platform: new organizations start at 3 minutes for iOS and Android and 10 minutes for Xcode, and Gradle sandboxes default to 10 minutes. An organization default of0means instances are never terminated for inactivity. The maximum is24h.spec.hardTimeoutterminates the instance after this much wall-clock time, active or not. Default'0', no cap.
A terminated instance reports which one fired in status.terminationReason.
iOS spec
The spec fields accepted by iosInstances.create (TypeScript names; Python uses snake_case). The CLI flags for the same fields are in Run a simulator.
| Field | Type | Meaning |
|---|---|---|
spec.model | 'iphone' | 'iphone-duo' | 'ipad' | 'watch' | Apple Simulator model. Default iphone. iphone-duo must be available to your organization. The Go SDK does not expose this field. |
spec.jurisdiction | 'us' | 'eu' | 'as' | Hard constraint: schedule only inside this jurisdiction; creation fails when no region there has capacity. TypeScript SDK, CLI, and REST only. See Placement. |
spec.region | string | Deprecated. A region preference such as us-east1, not a hard pin. See Placement. |
spec.inactivityTimeout | duration string | See Timeouts. |
spec.hardTimeout | duration string | See Timeouts. |
spec.tunnel | string | Name of a private tunnel. The instance reaches the tunnel's selectors through its connector. Creation fails when the tunnel does not exist, no connector holds it, or it does not attach within 30 seconds, and the instance becomes ready only after the tunnel attaches. TypeScript SDK, CLI, and REST only. |
spec.forceBundleId | string | After this app first enters the foreground, Limrun brings it back whenever it is closed or backgrounded. It does not launch the app at startup. |
spec.clues[] | array | Placement hints: ClientIP with the end user's IP, or ClientLocation with coordinates (TypeScript SDK and REST only). See Client location clues. |
spec.clues[].osVersion | string | Required when kind: 'OSVersion'. The iOS major version, '26' or '27'. iphone and ipad run both, iphone-duo runs '27', and watch runs '26'. Defaults to '26', or '27' on iphone-duo. |
spec.deviceState.key | string | Simulator snapshot key. Shared within your organization; each save replaces the previous snapshot. REST only. See Simulator snapshots. |
spec.deviceState.restoreMode | 'server' | 'client' | Default server: restores before readiness. client: lets the caller restore after readiness; saving requires a restoration acknowledgment. REST only. |
spec.initialAssets[] | array | Apps or keychain state to apply at boot. App entries use kind: 'App' with source URL, AssetName, or AssetID, plus an optional launchMode of ForegroundIfRunning or RelaunchIfRunning; omit launchMode to install without launching. Keychain entries use kind: 'Keychain' and restore an unencrypted .tar.gz archive, or a legacy encrypted archive when encryptionKey is supplied. See Pre-install at boot and Save and restore the keychain. |
spec.sandbox.xcode.enabled | boolean | Removed: an iOS create that sets it to true fails with 400. It used to create an embedded Xcode sandbox with the simulator. Create the sandbox separately and attach instead; see Move off the embedded Xcode sandbox. |

Simulator snapshots
A simulator snapshot saves all installed user apps, their data, shared app group containers, the keychain, and supported privacy permissions. App binaries are included in the snapshot. Creating an instance with the same key restores those apps and their state without separate app assets.
Set spec.deviceState.key in POST /v1/ios_instances to restore a snapshot
and save back to the same key at termination:
{
"wait": true,
"spec": {
"deviceState": { "key": "my-test-device" }
}
}Keys belong to your organization. A new key starts fresh; each successful save
replaces the snapshot under that key. Concurrent saves do not merge changes;
the last successful save wins. Keys are case-sensitive and accept up to 128 ASCII
characters: a letter or digit first, followed by letters, digits, or ._-@:+.
Installing a newer build of an app after restoration updates the app and keeps its data. Apps that you do not update stay at their saved versions. For preview links, see Keep app state between previews.
Snapshots have these limits:
- They exclude system apps, the Photos library, Contacts, simulator settings, caches, temporary files, logs, and system-managed container files.
- Supported privacy grants and denials are restored. Limited Photos and Contacts access is excluded.
- The total uncompressed snapshot, including app bundles, can be up to 4 GiB. If saving fails or exceeds this limit, the previous snapshot stays unchanged.
- Restoration requires the same iOS major version. When a REST create request omits the OS version, Limrun selects the saved major version if it is supported for the requested model. A request for a different major version fails.
By default, restoration runs before readiness. The create response reports
status.deviceState.restore.phase as restored, empty, or failed, or pending
if the call returns early. A failed restore can leave partial state on the new
simulator, but saving stays disabled to protect the previous snapshot. Instance
get and list responses do not include restore or save progress. Termination
attempts to save the snapshot; it cannot save if the simulator is no longer
available.
Manage snapshots
To inspect saved archives, open Snapshots and select iOS. The list shows saved status, compressed size, last save time, iOS version, and last use. Open a key for its details and a reusable preview link. The link opens Playground with the snapshot's organization and recorded iOS version selected. Select Start device to restore it.
The REST API exposes the same information:
| Endpoint | Result |
|---|---|
GET /v1/organizations/{organizationId}/ios_snapshots | Snapshot keys in alphabetical order. Use prefix to filter, limit to set the page size, and the returned nextOffset as the next request's offset. The default limit is 25 and the maximum is 100. |
GET /v1/organizations/{organizationId}/ios_snapshots/{key} | Current archive information for one URL-encoded key. |
DELETE /v1/organizations/{organizationId}/ios_snapshots/{key} | Delete the saved archive and remove the key. Requires iOS control. Returns 404 when the key does not exist. |
The GET endpoints require iOS read access in the organization. A key can exist before
its first save; saved: false means it has no archive. publishedAt is the last
save time, while lastUsedAt is when a simulator last resolved the key. Check
publishedAt to confirm a new save before restoring it on another instance.
The console's Delete action removes the saved apps and data after confirmation. The CLI and TypeScript SDK also support snapshot management:
lim ios snapshot list --prefix preview- --json
lim ios snapshot get preview-alex
lim ios snapshot delete preview-alexconst organizationId = 'org_...';
const page = await lim.iosSnapshots.list(organizationId, { prefix: 'preview-' });
const snapshot = await lim.iosSnapshots.get(organizationId, 'preview-alex');
await lim.iosSnapshots.delete(organizationId, snapshot.key);lim ios snapshot flush is an alias for delete. List output contains
snapshots and an optional nextOffset; pass it as --offset in the CLI or
offset in the SDK to read the next page. The CLI defaults to the login or API
key's organization and accepts --organization to select another accessible one.
Deleting a snapshot does not stop running simulators. They can save the key again. To keep it empty, stop simulators using that key, wait for their saves to finish, then delete the snapshot. Storage errors leave the key available for retry.
Restore from a client
With restoreMode: "client", the caller controls restoration after readiness:
- Create an instance with
spec.deviceState: { key: "my-test-device", restoreMode: "client" }and wait for readiness. - Send the simulator's
importDeviceStatesignaling command with{ "url": "<status.deviceState.restore.downloadUrl>" }from the create response. This installs the saved apps and restores their state. An empty URL means there is no saved snapshot, so skip this command. - After a successful restore, or when there is no saved snapshot, send
POST /v1/ios_instances/{id}/device_state/readywith no body. This enables manual saving and saving at termination. - Install any app updates, then launch the apps you need.
Do not acknowledge a failed or incomplete restore. Check that keychainError
is empty and appsPending has no entries before acknowledging. The download
URL is temporary; keep it private. Older snapshots that contain only data require
the corresponding apps to be installed before restoration can complete.
Android spec
The spec fields accepted by androidInstances.create. The CLI flags for the same fields are in Run an emulator.
| Field | Type | Meaning |
|---|---|---|
spec.model | 'phone' | 'tablet' | Android device model. Default phone. TypeScript SDK, CLI, and REST only; the Python and Go SDKs do not expose it. |
spec.jurisdiction | 'us' | 'eu' | 'as' | Hard constraint: schedule only inside this jurisdiction; creation fails when no region there has capacity. TypeScript SDK, CLI, and REST only. See Placement. |
spec.region | string | Deprecated. A region preference such as us-east1, not a hard pin. See Placement. |
spec.inactivityTimeout | duration string | See Timeouts. |
spec.hardTimeout | duration string | See Timeouts. |
spec.tunnel | string | Name of a private tunnel. The instance reaches the tunnel's selectors through its connector. Creation fails when the tunnel does not exist, no connector holds it, or it does not attach within 30 seconds, and the instance becomes ready only after the tunnel attaches. TypeScript SDK, CLI, and REST only. |
spec.clues[].kind | 'ClientIP' | 'ClientLocation' | 'OSVersion' | Placement hint: bias by Android version or by the end user's location. |
spec.clues[].clientIp | string | Required when kind: 'ClientIP'. The IP of the end user whose browser streams the emulator; the scheduler geolocates it and provisions in the nearest region. spec.region overrides this clue. |
spec.clues[].clientLocation | object | { latitude, longitude } in decimal degrees when kind: 'ClientLocation'. Both coordinates are required. Takes precedence over ClientIP. TypeScript SDK and REST only. |
spec.clues[].osVersion | string | Required when kind: 'OSVersion'. The Android major version, '14' or '15'. |
spec.initialAssets[] | array | APKs to install while provisioning, plus Configuration entries such as Chrome flags. Plural sources install split APKs as one group. See Pre-install APKs. |
spec.sandbox.playwrightAndroid.enabled | boolean | Provision a Playwright sub-sandbox next to the emulator and report its CDP URL in status.sandbox.playwrightAndroid.url. Default false. See Playwright. |
spec.sandbox.playwrightAndroid.version | '1.56.1-lim.1' | '1.60.0-lim.1' | Playwright server version for the sub-sandbox. Omit it for 1.56.1-lim.1. Your Playwright client version must match. |
The CLI's --os-version and --model flags set the OS version and model. It has no flags for the ClientIP and ClientLocation clues or for spec.sandbox.playwrightAndroid; set those from an SDK or REST.
Labels
metadata.labels is a free-form { [key: string]: string } map used for reuse, listing, and cleanup; Labels and reuse explains how. Avoid keys that could collide with metadata Limrun adds itself, such as the created-by: mcp label on instances the remote MCP server creates.
Placement
spec.jurisdiction (us, eu, or as) keeps an instance inside one jurisdiction. It is a hard constraint: creation fails when no region there has capacity.
spec.region is deprecated. It is a preference, such as eu-north1 or us-east1, that is tried first and overflows to other regions by proximity when the preferred ones are full. When both are omitted, Limrun places the instance by spec.clues and current availability.
jurisdiction is available in the TypeScript SDK and the CLI today; the Python and Go SDKs expose only region. The console's Analytics tab breaks down runtime minutes per platform and per region, which shows which regions your organization has used:

Client location clues
When you create an Android, iOS, Xcode, or Gradle instance through the REST API, supply the end user's coordinates as a ClientLocation clue to prefer a nearby region:
{
"spec": {
"clues": [
{
"kind": "ClientLocation",
"clientLocation": {
"latitude": 37.7749,
"longitude": -122.4194
}
}
]
}
}Both coordinates are required and use decimal degrees. Latitude must be between -90 and 90, and longitude between -180 and 180, inclusive. Zero is valid. Missing or invalid coordinates return HTTP 400.
ClientLocation takes precedence over ClientIP regardless of their order in spec.clues. Without coordinates, pass { "kind": "ClientIP", "clientIp": "203.0.113.42" } to locate the end user by IP. For requests without either clue, Limrun uses available visitor coordinates first, then the connecting IP.
Location clues express a preference. spec.jurisdiction still constrains placement, and a full region can lead to creation in another eligible region. A specific spec.region overrides location clues. Use the REST payload above if your installed SDK does not expose ClientLocation yet.
For embedded devices, always pass the end user's location or IP. It places the instance near the person streaming it, which is the difference between a responsive device and one that lags across continents.
Delegated permissions
All four instance create APIs accept an optional inline spec.role with a required permissions array of at most 100 asset permissions. role: { permissions: [] } grants no ambient asset access; omitting role uses the organization's compatibility default. Explicit permissions must be covered by the creator's permissions. A live instance's permission list can later be replaced with PATCH using the ETag returned by GET.
In TypeScript, supply literal permission strings in spec.role.permissions, including both your existing grants and the exact-target upload grants. The CLI always sends an explicit list, empty when no grants are needed. It adds upload grants automatically for --upload and accepts extra grants through repeatable --permission flags.
A supplied role must include a non-null permissions array; role: {} is invalid. Invalid permissions or more than 100 entries return 400 Bad Request; permissions exceeding the creator's grants return 403 Forbidden. Named role references are not supported.
With reuseIfExists, permissions do not participate in label matching. A reused instance keeps its stored role unchanged. The requested role applies only when a new instance is created.
GET returns current permissions and an ETag for live instances. PATCH replaces spec.role.permissions with If-Match; an outdated version returns 412. An empty array removes all delegated grants. Historical records omit the editable ETag. List remains eventually consistent. Already-issued tokens and signed URLs retain their grants until expiry.
See Workload Identity for update examples and token lifetime.
Updating labels
All four instance PATCH endpoints also accept metadata.labels. Send labels,
spec.role.permissions, or both in the same conditional request. Omitted fields
stay unchanged. A supplied labels object replaces the complete customer-label map;
{} clears it. Label updates require control of the instance but no asset grants
when permissions are omitted. Internal routing labels cannot be changed through this API.
const { response } = await lim.xcodeInstances.get(instanceId).withResponse();
const etag = response.headers.get('etag');
if (!etag) throw new Error('Instance is no longer live');
await lim.xcodeInstances.update(instanceId, {
ifMatch: etag,
metadata: { labels: { project: 'my-app', branch: 'release' } },
});curl -X PATCH "https://api.limrun.com/v1/xcode_instances/$INSTANCE_ID" \
-H "Authorization: Bearer $LIM_API_KEY" \
-H 'Content-Type: application/json' \
-H 'If-Match: "ETAG_FROM_GET"' \
-d '{"metadata":{"labels":{"project":"my-app","branch":"release"}}}'On 412, GET the latest instance and recompute the update before retrying. Live GET
and reuse matching see the updated labels; List reflects them after the lifecycle
snapshot reaches the read model. Empty updates and null labels return 400.
Next steps
Look up any field you can set when creating an instance, or any field it reports back while it runs.
Was this guide helpful?