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

# Network inspection

> Stream decoded HTTP and WebSocket traffic from a supported Mobilerun device.

Mobilerun can inspect a device's network traffic and stream decoded HTTP/1.1, HTTP/2, HTTP/3, and application WebSocket events to your local machine. The stream is live-only and does not require adb or direct access to the device.

Unlike the [ADB and Frida tunnels](/integrations/adb), network inspection is session-based. You start a session with the REST API, connect to the returned WebSocket with either your API key or the device's existing stream token, and stop the session when you are finished.

## Requirements

<Note>
  Network inspection must be enabled for the device's hosting pool. Contact Mobilerun support at [contact@mobilerun.ai](mailto:contact@mobilerun.ai) if you need access.
</Note>

* A Mobilerun [API key](/api-keys) (`dr_sk_...`)
* [`curl`](https://curl.se), [`jq`](https://jqlang.org), and [`websocat`](https://github.com/vi/websocat) installed locally

Set the API key and device ID in your shell:

```bash theme={null}
export MOBILERUN_API_KEY='dr_sk_...'
export DEVICE_ID='YOUR_DEVICE_ID'
export MOBILERUN_API='https://api.mobilerun.ai/v1'
```

Check whether the device supports network inspection:

```bash theme={null}
curl --fail-with-body -sS \
  -H "Authorization: Bearer $MOBILERUN_API_KEY" \
  "$MOBILERUN_API/devices/$DEVICE_ID/capabilities" \
  | jq '.capabilities.trafficInspection'
```

The value must be `true`. The capability is granted when a device is placed in an enabled pool. Disabling inspection for that pool also prevents new sessions and stops active sessions.

## Endpoints

| Method   | Endpoint                                              | Purpose                                                                  |
| -------- | ----------------------------------------------------- | ------------------------------------------------------------------------ |
| `POST`   | `/v1/devices/{deviceId}/traffic/sessions`             | Start a session and receive stream credentials.                          |
| `GET`    | `/v1/devices/{deviceId}/traffic/sessions`             | List recent session metadata.                                            |
| `GET`    | `/v1/devices/{deviceId}/traffic/sessions/{sessionId}` | Read status and return the device stream credentials for a live session. |
| `DELETE` | `/v1/devices/{deviceId}/traffic/sessions/{sessionId}` | Stop a session and restore device networking.                            |
| `WSS`    | Returned as `stream.url`                              | Receive live decoded traffic events.                                     |

Use your Mobilerun API key for the REST endpoints. Non-browser WebSocket clients also authenticate with that API key. Browser clients, which cannot set an `Authorization` header on `WebSocket`, use the existing device stream token returned in `stream.token` as a `token` query parameter. Always start with the returned `stream.url` instead of constructing the path yourself.

## Start an inspection session

Create a session with a unique `Idempotency-Key` so retrying the same request does not start another session:

```bash theme={null}
export REQUEST_ID="traffic-$(date +%s)-$$"

SESSION="$(
  curl --fail-with-body -sS -X POST \
    -H "Authorization: Bearer $MOBILERUN_API_KEY" \
    -H "Idempotency-Key: $REQUEST_ID" \
    -H 'Content-Type: application/json' \
    -d '{}' \
    "$MOBILERUN_API/devices/$DEVICE_ID/traffic/sessions"
)"

export SESSION_ID="$(jq -er '.id' <<<"$SESSION")"
jq '{id, state, expiresAt, retention}' <<<"$SESSION"
```

Only one session can be `starting`, `active`, or `stopping` for a device at a time. Starting another returns `409 TRAFFIC_ALREADY_ACTIVE`.

## Wait until the session is active

Starting inspection is asynchronous. Poll the status endpoint until the device producer is ready:

```bash theme={null}
while true; do
  STATUS="$(
    curl --fail-with-body -sS \
      -H "Authorization: Bearer $MOBILERUN_API_KEY" \
      "$MOBILERUN_API/devices/$DEVICE_ID/traffic/sessions/$SESSION_ID"
  )"

  STATE="$(jq -er '.state' <<<"$STATUS")"
  [ "$STATE" = 'active' ] && break

  if [ "$STATE" != 'starting' ]; then
    jq . <<<"$STATUS"
    exit 1
  fi

  sleep 1
done

export STREAM_URL="$(jq -er '.stream.url' <<<"$STATUS")"
export STREAM_PROTOCOL="$(jq -er '.stream.protocol' <<<"$STATUS")"
```

While the session is `starting` or `active`, the status response returns the device's existing stream token. This is the same device-bound credential used by other device WebSocket endpoints; it is not a traffic-specific or session-scoped viewer token. It is revoked when the device is terminated and rotates when device ownership changes. Treat it as a secret and do not log URLs containing it.

## Stream events with websocat

Open the returned WebSocket URL with your Mobilerun API key and the required subprotocol:

```bash theme={null}
websocat --no-async-stdio -B 8388608 \
  -H="Authorization: Bearer $MOBILERUN_API_KEY" \
  -H="Sec-WebSocket-Protocol: $STREAM_PROTOCOL" \
  "$STREAM_URL" \
  | jq --unbuffered -c .
```

Do not send `stream.token` as a Bearer token. For non-browser clients, the Bearer credential is your `dr_sk_...` API key.

The `websocat` options keep large JSON events intact when piping them into `jq`, including on macOS.

### Browser WebSocket clients

The browser `WebSocket` API cannot set an `Authorization` header. Add the returned device stream token to the returned URL instead:

```javascript theme={null}
const streamUrl = new URL(session.stream.url);
streamUrl.searchParams.set('token', session.stream.token);

const socket = new WebSocket(streamUrl, session.stream.protocol);
socket.onmessage = ({ data }) => console.log(JSON.parse(data));
```

The edge authenticates the query token against the device before forwarding the WebSocket upgrade. Devices API then verifies that the traffic session belongs to the same device owner and is still `starting` or `active`.

Exercise the app on the device while this command is running. Each line is a JSON event. The first event is `hello`; subsequent event types are:

| Type                | Meaning                                                          |
| ------------------- | ---------------------------------------------------------------- |
| `ready`             | The on-device traffic producer connected.                        |
| `flow`              | A decoded HTTP exchange or an interception failure.              |
| `websocket-message` | An application WebSocket message associated with a flow.         |
| `gap`               | Events were dropped because the bounded device queue overflowed. |

## Event schema

### Stream hello

The gateway sends a `hello` message immediately after the WebSocket opens. This message uses camelCase fields and is separate from the device event envelope:

```json theme={null}
{
  "type": "hello",
  "schemaVersion": 1,
  "sessionId": "9a675d1d-f919-4454-9523-9715f1fc1057",
  "deviceId": "3fe83936-fb09-49f1-8ec8-c1bdca194cf1",
  "state": "active",
  "retention": "none"
}
```

### Device event envelope

All subsequent device events use snake\_case fields:

| Field          | Type                 | Required | Description                                                                              |
| -------------- | -------------------- | -------- | ---------------------------------------------------------------------------------------- |
| `type`         | String               | Yes      | `ready`, `flow`, `websocket-message`, or `gap`.                                          |
| `session_id`   | String               | Yes      | Traffic session UUID.                                                                    |
| `timestamp_ms` | Non-negative integer | No       | Unix timestamp in milliseconds. The current Android producer includes it on every event. |

The ingest boundary rejects undeclared or duplicate fields, malformed Base64, negative numeric values, and payloads that exceed the session body limit.

A `ready` event has no additional fields:

```json theme={null}
{
  "type": "ready",
  "session_id": "9a675d1d-f919-4454-9523-9715f1fc1057",
  "timestamp_ms": 1787234404000
}
```

### Flow events

A `flow` event adds a `flow` object. Only `id` and `decryption_status` are always present; other fields can be omitted or `null` when an interception fails before that information is available.

```json theme={null}
{
  "type": "flow",
  "session_id": "9a675d1d-f919-4454-9523-9715f1fc1057",
  "timestamp_ms": 1787234404123,
  "flow": {
    "id": "mitm-flow-id",
    "decryption_status": "decrypted",
    "host": "example.com",
    "port": 443,
    "sni": "example.com",
    "scheme": "https",
    "http_version": "HTTP/2",
    "method": "GET",
    "path": "/api/items",
    "status_code": 200,
    "request_headers": [["content-type", "application/json"]],
    "response_headers": [["content-type", "application/json"]],
    "request_body_base64": null,
    "response_body_base64": "eyJvayI6dHJ1ZX0=",
    "request_body_bytes": 0,
    "response_body_bytes": 11,
    "request_body_truncated": false,
    "response_body_truncated": false,
    "duration_ms": 84
  }
}
```

| Field                     | Type                          | Required | Description                                                                                    |
| ------------------------- | ----------------------------- | -------- | ---------------------------------------------------------------------------------------------- |
| `id`                      | String                        | Yes      | Unique flow identifier.                                                                        |
| `decryption_status`       | String                        | Yes      | Current producer values are `decrypted` and `failed`.                                          |
| `error`                   | String or null                | No       | Safe interception or transport error. It can be present even when a response was decoded.      |
| `host`                    | String or null                | No       | HTTP request host.                                                                             |
| `port`                    | Non-negative integer or null  | No       | Upstream server port.                                                                          |
| `sni`                     | String or null                | No       | TLS Server Name Indication.                                                                    |
| `scheme`                  | String or null                | No       | Request scheme, such as `http` or `https`.                                                     |
| `http_version`            | String or null                | No       | Negotiated protocol reported by the app connection, such as `HTTP/1.1`, `HTTP/2`, or `HTTP/3`. |
| `method`                  | String or null                | No       | HTTP request method.                                                                           |
| `path`                    | String or null                | No       | Request path including its query string.                                                       |
| `status_code`             | Non-negative integer or null  | No       | HTTP response status code; absent or null if no response was received.                         |
| `request_headers`         | Array of string pairs or null | No       | Request headers as `[[name, value], ...]`. Repeated names remain separate pairs.               |
| `response_headers`        | Array of string pairs or null | No       | Response headers in the same shape.                                                            |
| `request_body_base64`     | Base64 string or null         | No       | Captured request body, limited by the session's `maxBodyBytes`.                                |
| `response_body_base64`    | Base64 string or null         | No       | Captured response body, limited by the session's `maxBodyBytes`.                               |
| `request_body_bytes`      | Non-negative integer or null  | No       | Original request-body size before truncation.                                                  |
| `response_body_bytes`     | Non-negative integer or null  | No       | Original response-body size before truncation.                                                 |
| `request_body_truncated`  | Boolean or null               | No       | `true` when the request body exceeded `maxBodyBytes`.                                          |
| `response_body_truncated` | Boolean or null               | No       | `true` when the response body exceeded `maxBodyBytes`.                                         |
| `duration_ms`             | Non-negative integer or null  | No       | Time from request start to response completion in milliseconds.                                |

There is no combined `url` field. Construct one from `scheme`, `host`, optional `port`, and `path` if your parser needs it.

A null body field means that no body bytes were captured. Use the corresponding `*_body_bytes` and `*_body_truncated` fields to distinguish an empty body from one clipped by the session limit.

### WebSocket message events

Application WebSocket messages refer back to their HTTP upgrade flow by `flow_id`:

```json theme={null}
{
  "type": "websocket-message",
  "session_id": "9a675d1d-f919-4454-9523-9715f1fc1057",
  "timestamp_ms": 1787234405123,
  "message": {
    "flow_id": "mitm-flow-id",
    "direction": "client",
    "opcode": "text",
    "payload_base64": "aGVsbG8=",
    "payload_bytes": 5,
    "payload_truncated": false
  }
}
```

| Field               | Type                         | Required | Description                                          |
| ------------------- | ---------------------------- | -------- | ---------------------------------------------------- |
| `flow_id`           | String                       | Yes      | ID of the associated HTTP upgrade flow.              |
| `direction`         | String                       | Yes      | Current producer values are `client` and `server`.   |
| `opcode`            | String                       | Yes      | Current producer values are `text` and `binary`.     |
| `payload_base64`    | Base64 string or null        | No       | Captured message payload, limited by `maxBodyBytes`. |
| `payload_bytes`     | Non-negative integer or null | No       | Original payload size before truncation.             |
| `payload_truncated` | Boolean or null              | No       | `true` when the payload exceeded `maxBodyBytes`.     |

### Gap events

A `gap` event reports that the device queue dropped one or more events:

```json theme={null}
{
  "type": "gap",
  "session_id": "9a675d1d-f919-4454-9523-9715f1fc1057",
  "timestamp_ms": 1787234406123,
  "dropped_events": 1
}
```

`dropped_events` is a required positive integer. The event does not identify which flows or messages were dropped.

For example, decode response bodies with `jq`:

```bash theme={null}
websocat --no-async-stdio -B 8388608 \
  -H="Authorization: Bearer $MOBILERUN_API_KEY" \
  -H="Sec-WebSocket-Protocol: $STREAM_PROTOCOL" \
  "$STREAM_URL" \
  | jq --unbuffered -r \
      'select(.type == "flow" and .flow.response_body_base64) | .flow.response_body_base64 | @base64d'
```

<Warning>
  Inspected headers and bodies can contain credentials, personal data, and other secrets. Mobilerun does not retain session events, but anything you print, pipe, or redirect locally may be stored on your machine.
</Warning>

## Interception behavior

Inspection is transparent at the device-networking layer. You do not need to configure an HTTP proxy in the app or Android settings. Starting a session temporarily installs the capture CA and reconfigures the device's existing TUN routing; stopping the session restores the previous routing and trust-store mounts. Android's global `http_proxy` setting is neither required nor modified.

Certificate pinning is not bypassed. A pinned app can reject the inspection certificate before an HTTP request is available, causing the connection to fail without a decoded flow. If the failure reaches the HTTP flow pipeline, the stream reports `decryption_status: "failed"` with an `error`, but clients should not assume every rejected TLS handshake produces an event. Keep using an authorized Frida pinning bypass, or equivalent instrumentation for a native or custom trust store, when inspecting pinned apps.

## Stop the session

Stop inspection when you are finished so the device can restore its normal routing and temporary trust changes:

```bash theme={null}
curl --fail-with-body -sS -X DELETE \
  -H "Authorization: Bearer $MOBILERUN_API_KEY" \
  "$MOBILERUN_API/devices/$DEVICE_ID/traffic/sessions/$SESSION_ID" \
  | jq '{id, state}'
```

The request returns `202 Accepted` while cleanup runs. Repeating it is safe. Sessions also stop automatically when they expire.

## Troubleshooting

* **`TRAFFIC_NOT_SUPPORTED`** — The device image does not support network inspection.
* **`TRAFFIC_NOT_ENTITLED`** — `capabilities.trafficInspection` is `false`; create a device in an enabled pool or contact support.
* **`TRAFFIC_ALREADY_ACTIVE`** — Another session is starting, active, or stopping. Use the session ID in `details.sessionId`, or list sessions with `GET /devices/{deviceId}/traffic/sessions`.
* **`TRAFFIC_DEVICE_NOT_READY`** — Wait until the device reaches the `ready` state and retry with the same idempotency key.
* **`401 Unauthorized` on the WebSocket** — For CLI clients, send your `dr_sk_...` API key as `Authorization: Bearer ...`. For browsers, append the returned `stream.token` as the URL's `token` query parameter. Do not send `stream.token` as a Bearer token.
* **WebSocket subprotocol error** — Send `Sec-WebSocket-Protocol: mobilerun.traffic.v1` with either authentication method.
* **`Incoming message too long` or a `jq` parse error** — Increase `websocat`'s message buffer with `-B`; do not use `-S`, because strict mode drops oversized inspection events. Use `--no-async-stdio` when piping large messages on macOS.
* **The socket closed with `1013`** — Reconnect with the same authentication method. Read the session status again first if the device may have been transferred or replaced. Missed events are not replayed.
