> ## Documentation Index
> Fetch the complete documentation index at: https://docs.run.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# Open URLs and Deep Links

> Open HTTPS URLs and application deep links on authenticated iOS Simulator and Android Emulator sessions.

Use the same operation to open an ordinary HTTPS URL or hand a custom-scheme
deep link to an installed app. It is available on iOS Simulator and Android
Emulator through the CLI, TypeScript SDK, and REST API.

## Authenticate

For an interactive terminal, sign in once and confirm that the account can use
mobile sessions:

```bash theme={null}
runcloud login
runcloud account --json
```

For CI, the TypeScript SDK, or direct REST calls, create an API key in the
[dashboard](https://run.cloud/dashboard) and expose it to the process:

```bash theme={null}
export RUN_CLOUD_API_KEY="rc_live_..."
```

Do not put the key in source code, a browser bundle, command output, or a
committed environment file. CLI commands can use either the saved login or the
environment key. The SDK and REST examples below use the environment key.

You also need an active session that belongs to the authenticated account. A
custom-scheme target needs an installed app that registers that scheme. The
maintained [iOS proof app](https://github.com/newly-app/run-cloud-examples/tree/main/ios-app-screenshot)
and [Android proof app](https://github.com/newly-app/run-cloud-examples/tree/main/android-app-screenshot)
register `runcloudproof` and display the complete URI they receive.

## Use the CLI

Pass the complete target as one quoted argument. These examples preserve an
encoded path separator, a space, a nested return URL, its query string, and a
fragment:

<CodeGroup>
  ```bash iOS theme={null}
  HTTPS_URL='https://example.com/search?q=run%20cloud&return=%2Fdocs%3Ftab%3Dmobile'
  DEEP_LINK='runcloudproof://open/items%2F42?message=hello%20world&return=https%3A%2F%2Fexample.com%2Fdone%3Fx%3D1%26y%3Dtwo#proof'

  runcloud ios open-url "$HTTPS_URL" --id "$IOS_SESSION_ID" --json
  runcloud ios open-url "$DEEP_LINK" --id "$IOS_SESSION_ID" --json
  ```

  ```bash Android theme={null}
  HTTPS_URL='https://example.com/search?q=run%20cloud&return=%2Fdocs%3Ftab%3Dmobile'
  DEEP_LINK='runcloudproof://open/items%2F42?message=hello%20world&return=https%3A%2F%2Fexample.com%2Fdone%3Fx%3D1%26y%3Dtwo#proof'

  runcloud android open-url "$HTTPS_URL" --id "$ANDROID_SESSION_ID" --json
  runcloud android open-url "$DEEP_LINK" --id "$ANDROID_SESSION_ID" --json
  ```
</CodeGroup>

Successful JSON identifies the authenticated session, device, and lease that
accepted the operation. `url` is the exact input string, without decoding or
normalization:

```json theme={null}
{
  "ok": true,
  "platform": "ios",
  "sessionId": "sim_example",
  "device": "device_example",
  "leaseId": "lease_example",
  "url": "runcloudproof://open/items%2F42?message=hello%20world&return=https%3A%2F%2Fexample.com%2Fdone%3Fx%3D1%26y%3Dtwo#proof"
}
```

The acknowledgement confirms that the operation ran against that live lease.
When app-specific navigation matters, inspect the signed viewer or capture a
screenshot to confirm the visible result.

## Use the TypeScript SDK

Install `@run-cloud/sdk` on Node.js 20 or newer:

```bash theme={null}
npm install @run-cloud/sdk
```

Platform clients return a typed `SimulatorOpenUrlResult`:

```ts theme={null}
import { Client, type SimulatorOpenUrlResult } from "@run-cloud/sdk";

const cloud = new Client();
const target =
  "runcloudproof://open/items%2F42?message=hello%20world&return=" +
  "https%3A%2F%2Fexample.com%2Fdone%3Fx%3D1%26y%3Dtwo#proof";

const iosResult: SimulatorOpenUrlResult = await cloud.ios.openUrl(
  process.env.IOS_SESSION_ID!,
  target,
);
const androidResult: SimulatorOpenUrlResult = await cloud.android.openUrl(
  process.env.ANDROID_SESSION_ID!,
  target,
);

if (iosResult.url !== target || androidResult.url !== target) {
  throw new Error("run.cloud did not preserve the deep link");
}
```

When the platform is selected at runtime, use the generic simulator client:

```ts theme={null}
const result = await cloud.simulators.openUrl(session.id, target, {
  platform: session.platform,
});
```

Release every metered session in `finally`; see the
[TypeScript SDK guide](/cli/typescript-sdk) for a complete create-and-cleanup
example.

## Call the REST API

The CLI and SDK use the public authenticated endpoint rather than a separate
URL-opening contract:

```text theme={null}
POST /run-cloud/{ios|android}/{sessionId}/open-url
```

Build the JSON body with `jq` so quotes and URI delimiters remain data:

```bash theme={null}
TARGET_URL='runcloudproof://open/items%2F42?message=hello%20world&return=https%3A%2F%2Fexample.com%2Fdone%3Fx%3D1%26y%3Dtwo#proof'

jq -n --arg url "$TARGET_URL" '{url: $url}' | \
  curl --fail-with-body --silent --show-error \
    --request POST \
    --header "Authorization: Bearer $RUN_CLOUD_API_KEY" \
    --header 'Content-Type: application/json' \
    --data-binary @- \
    "https://api.run.cloud/run-cloud/ios/$IOS_SESSION_ID/open-url"
```

Replace `ios` and `IOS_SESSION_ID` with `android` and the Android session id for
an emulator. The response has the same six fields as SDK and CLI JSON output.

## Platform behavior

| Target        | iOS Simulator                                                                                    | Android Emulator                                                                                         |
| ------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
| `https://...` | Opens the system handler, normally Safari. A configured universal link can open its app instead. | Resolves an Android `VIEW` target, normally the browser. A configured App Link can open its app instead. |
| Custom scheme | Opens the installed app that registered the scheme.                                              | Resolves the exported activity whose intent filter registered the scheme.                                |

Opening a URL does not install an app or register its scheme. If no application
can handle a custom scheme, the request fails instead of reporting success.

On iOS, the first custom-scheme handoff can show an **Open in “App”?** system
confirmation after the URL acknowledgement succeeds. Confirm **Open** in the
viewer before asserting app state. Automated visual proof can wait for the
prompt and send an authenticated normalized tap to its button. The maintained
iOS proof waits at least five seconds for the prompt, taps once, then allows at
least five seconds for the app to render before capture. Its HTTPS proof also
allows five seconds for the browser to reach its final state. Android does not
receive that iOS-only tap.

## Preserve encoding safely

* Supply one absolute URI with a standard scheme. Relative paths and blank
  values are rejected.
* Percent-encode individual path, query, and fragment values. Do not
  percent-encode the entire URI, because that also hides its `:` and `/`
  delimiters.
* Keep the CLI argument quoted. Characters such as `&`, `#`, `?`, `;`, and
  command-substitution text remain URL data. The URL is passed as one inert
  argument; Android's platform bridge quotes shell metacharacters so they are
  not evaluated as commands.
* The API accepts targets from 1 to 4,096 characters and rejects raw ASCII
  whitespace, control characters, malformed schemes or percent escapes, and
  HTTP(S) URLs without a hostname. It also rejects `about:`, `data:`, `file:`,
  `javascript:`, and `vbscript:` targets.
* Compare the returned `url` with the input when an automation run must prove
  transport fidelity. If they match but the app displays a different route,
  inspect the app's own link parser.

## Troubleshooting

Public errors include `ok: false`, outer `action: "open-url"`, `platform`,
available session/device/lease scope, and a nested `error` object with `code`,
`message`, `retryable`, and sometimes `action` with a concrete next step.

| Code                                                                          | HTTP | Retry? | What to do                                                                                                                                                                               |
| ----------------------------------------------------------------------------- | ---- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_url`                                                                 | 400  | No     | Use an allowed absolute URI, remove raw whitespace/control characters, correct malformed percent escapes, percent-encode spaces, and keep it within 4,096 characters.                    |
| `open_url_scope_required`                                                     | 403  | No     | Transport only: the authenticated lease scope does not permit URL opening for that device. Use the public session operation through the CLI or SDK; recreate the session if it persists. |
| `active_session_not_found`                                                    | 404  | No     | The session is inactive, belongs to another account, or is under the other platform. Run `runcloud ios get <id>` or `runcloud android get <id>` with the same credential.                |
| `simulator_session_ended`                                                     | 410  | No     | The authenticated simulator lease has ended. Create a new session and use its new session id.                                                                                            |
| `ios_url_handler_not_found`                                                   | 422  | No     | Install a simulator-compatible app whose `CFBundleURLSchemes` contains the custom scheme.                                                                                                |
| `android_url_handler_not_found`                                               | 422  | No     | Install an APK with an exported activity whose `VIEW`, `DEFAULT`, and `BROWSABLE` intent filter registers the scheme.                                                                    |
| `simulator_unavailable`                                                       | 503  | Yes    | Confirm that the session is still active, then retry with a bounded backoff.                                                                                                             |
| `open_url_timeout`                                                            | 504  | Yes    | The platform did not finish before the deadline. Check session state before retrying.                                                                                                    |
| `ios_open_url_failed` or `android_open_url_failed`                            | 502  | Yes    | Read `message` and `action`, inspect the signed viewer and platform logs, then retry only after correcting the reported condition.                                                       |
| `open_url_transport_error`, `open_url_invalid_response`, or `open_url_failed` | 502  | Yes    | The gateway could not complete or verify the platform request. Check session health before a bounded retry.                                                                              |

If a link is truncated in a terminal, quote the entire URI. An unquoted `&`
backgrounds a shell command, and an unquoted `#` can start a shell comment. If
the request succeeds but the expected screen is missing, inspect the viewer or
take a screenshot and check the app's own route configuration.

SDK failures throw `RunCloudError`. Inspect `status`, `code`, `detail`,
`retryable`, `platform`, and `sessionId` rather than matching a complete error
sentence. `action` identifies the failed SDK operation (`"open-url"`);
`suggestedAction` contains the human-readable remediation returned as
`error.action` by the API.
