# Page control: Mosaic API reference

> Page control in Mosaic’s local REST API: 28 routes, each with its method, path, fields, what it returns and the errors it names.

Source: https://mosaicbrowser.com/docs/api/page-control

**Automation** REST API

## Page control

Drive one page the way a person does: read it as a tree of roles and names, find elements, click and type, watch the network, read the console, wait for things to happen. The same handlers back the `page_*` MCP tools. Two things make it different from Playwright, and both are deliberate. **Nothing here enables the CDP Runtime domain** — measured on this codebase, that alone flips a detector’s verdict to "you are a bot", so refs and every query live in an isolated world the page cannot see. And **clicks and keystrokes go through the human-input engine**, so a click is a real pointer move along a minimum-jerk path at a Fitts’ law duration, and typing has human inter-key timing. It is slower than `element.click()` on purpose. Every route needs a running profile and answers `400` naming it when there is not one.

- [GETSnapshot the page](#page-snapshot)
- [GETFind elements](#page-find)
- [GETRead the HTML](#page-dom)
- [GETScreenshot the page or an element](#page-screenshot)
- [POSTClick](#page-click)
- [POSTHover](#page-hover)
- [POSTType](#page-type)
- [POSTClear and fill a field](#page-fill)
- [POSTPress a key](#page-press)
- [POSTChoose in a select](#page-select)
- [POSTScroll](#page-scroll)
- [POSTDrag](#page-drag)
- [POSTAttach files](#page-upload)
- [POSTNavigate](#page-navigate)
- [POSTReload](#page-reload)
- [POSTBack or forward](#page-history)
- [POSTWait for the page](#page-wait)
- [POSTRun script](#page-evaluate)
- [GETRead the console](#page-console)
- [POSTCapture page console calls](#page-console-mode)
- [POSTStart the request log](#page-network-start)
- [GETRead the request log](#page-network)
- [POSTStop the request log](#page-network-stop)
- [POSTRead a response body](#page-network-body)
- [GETRead dialogs](#page-dialogs-read)
- [POSTSet the dialog policy](#page-dialogs-set)
- [GETList page attachments](#page-attachments)
- [POSTRelease an attachment](#page-detach)

## [Snapshot the page](#page-snapshot)

GET`/api/v1/profiles/:id/page/snapshot`

The page as a tree of roles, names and states, with a `ref` on everything you can act on. Start here: it is a few hundred lines where the HTML is tens of thousands, and every acting route takes the refs it mints. A ref belongs to the snapshot that minted it — navigate or re-snapshot and the old ones are **refused by name**, naming the generation, rather than silently resolving to whatever now sits in that position.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Query parameters

| Field | Type | What it is |
| --- | --- | --- |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |
| `maxNodes` | `number` | Cap on nodes, 50–5000 (default 1500). |
| `everything` | `boolean` | Include layout wrappers with no role of their own. |

#### Returns

`{ tabId, url, title, generation, refs, truncated, tree }`

## [Find elements](#page-find)

GET`/api/v1/profiles/:id/page/find`

Finds elements by CSS selector, visible text, ARIA role, accessible name, or any combination, and mints a ref for each. Text matching returns the **innermost** match, so searching for "Sign in" gives the button rather than the `<body>` that also contains it. At least one filter is required; an unfiltered search would return the first twenty elements on the page, which answers nothing.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Query parameters

| Field | Type | What it is |
| --- | --- | --- |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |
| `selector` | `string` | CSS selector to search within. |
| `text` | `string` | Visible text the element contains. |
| `role` | `string` | ARIA role, e.g. `button`. |
| `name` | `string` | Accessible name. |
| `exact` | `boolean` | Require the name to match exactly. |
| `limit` | `number` | Maximum matches, 1–200 (default 20). |

#### Returns

`{ tabId, total, elements: [{ ref, role, name, text, tag, box }] }`

## [Read the HTML](#page-dom)

GET`/api/v1/profiles/:id/page/dom`

The page’s markup, whole or one subtree by `ref` or `selector`. Use it when you need the markup itself — an attribute, a `data-` value, a hidden input. For deciding what to click, the snapshot is smaller and more useful. Reports `truncated` and `totalChars` rather than silently cutting.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Query parameters

| Field | Type | What it is |
| --- | --- | --- |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |
| `ref` | `string` | Ref from a snapshot. |
| `selector` | `string` | CSS selector. |
| `maxChars` | `number` | Cap on characters (default 40000). |

#### Returns

`{ tabId, url, title, html, truncated, totalChars }`

## [Screenshot the page or an element](#page-screenshot)

GET`/api/v1/profiles/:id/page/screenshot`

A PNG of the viewport, the full page, or one element by `ref` or `selector`, base64.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Query parameters

| Field | Type | What it is |
| --- | --- | --- |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |
| `ref` | `string` | Photograph this element alone. |
| `selector` | `string` | Photograph the first match alone. |
| `fullPage` | `boolean` | The whole page rather than the viewport. |

#### Returns

`{ tabId, of, format: "png", base64 }`

## [Click](#page-click)

POST`/api/v1/profiles/:id/page/click`

Clicks an element by `ref`, `selector` or visible `text`. Scrolls to it with a real wheel first, then moves the pointer along a human trajectory and presses. The target’s own width is fed to the motion model, because a move to a small target genuinely takes a person longer than one of the same distance to a big one. Returns `forced: true` on the rare element only a programmatic scroll could reach — inside an overflow container, say — so a transcript shows the one action that was not a person’s. Targeting by `text` refuses an ambiguous match unless you pass `first`.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `ref` | `string` | Ref from a snapshot or find. |
| `selector` | `string` | CSS selector. |
| `text` | `string` | Visible text. |
| `first` | `boolean` | With `text`, take the first match instead of refusing. |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |

#### Returns

`{ tabId, clicked: { ref, role, name, tag, box, forced } }`

## [Hover](#page-hover)

POST`/api/v1/profiles/:id/page/hover`

Moves the pointer onto an element and leaves it there, for menus and tooltips that open on hover.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `ref` | `string` | Ref. |
| `selector` | `string` | CSS selector. |
| `text` | `string` | Visible text. |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |

#### Returns

`{ tabId, hovered }`

## [Type](#page-type)

POST`/api/v1/profiles/:id/page/type`

Types text as a person types it: real `keydown`, `keypress`, `input` and `keyup` per character, human inter-key timing, occasional typos corrected with a backspace. With a `ref` or `selector` it clicks the field first; with neither it types into whatever already has focus. **Never a paste** — a field that goes from empty to forty characters in one event fires no `keydown` at all, which is among the cheapest bot signals a site can collect.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `text`required | `string` | The characters to type. |
| `ref` | `string` | Ref of the field. |
| `selector` | `string` | CSS selector of the field. |
| `clear` | `boolean` | Select the existing value first, so this replaces it. Needs a target. |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |

#### Returns

`{ tabId, target, typed, cleared }`

## [Clear and fill a field](#page-fill)

POST`/api/v1/profiles/:id/page/fill`

Clears a field and types a value into it, with the same human timing. The clear uses the select-all shortcut for the platform **the page believes it is on**, read through `navigator.platform` rather than the host — so a profile presenting as Windows sends Control+A even on a Mac, and the keystroke agrees with the rest of its fingerprint. Refuses anything that is not an input, a textarea or a contenteditable, naming what it found instead.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `text`required | `string` | The value to put in the field. |
| `ref` | `string` | Ref of the field. |
| `selector` | `string` | CSS selector of the field. |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |

#### Returns

`{ tabId, target, typed }`

## [Press a key](#page-press)

POST`/api/v1/profiles/:id/page/press`

Presses one named key, optionally with modifiers held. An unknown key is a `400` **listing the keys that exist**, because "unknown key" with no list sends the caller to the source. For ordinary characters use the type route.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `key`required | `string` | Enter, Tab, Escape, Backspace, Delete, the arrows, Home, End, PageUp, PageDown, Insert, Space, F1–F12. |
| `modifiers` | `string[]` | Any of Alt, Control, Meta, Shift. |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |

#### Returns

`{ tabId, pressed, modifiers }`

## [Choose in a select](#page-select)

POST`/api/v1/profiles/:id/page/select`

Chooses options in a native `<select>`, by value or by visible label. A native dropdown is drawn by the operating system and is not in the page, so there is nothing to click: this sets the selection and fires the `input` and `change` events a browser would, both bubbling, because a `change` that does not bubble is why this looks like it worked and changes nothing. No matching option is a `400` **listing the options the select offers**. For a custom dropdown, click it and then click the option.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `values`required | `string[]` | Option values or labels. |
| `ref` | `string` | Ref of the select. |
| `selector` | `string` | CSS selector of the select. |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |

#### Returns

`{ tabId, target, selected }`

## [Scroll](#page-scroll)

POST`/api/v1/profiles/:id/page/scroll`

Scrolls the page with a real wheel, or brings an element into view. The wheel deltas match the pointing device this profile was assigned — a trackpad and a notched mouse produce different event streams, and mixing them inside one session describes two different computers.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `amount` | `number` | Pixels to scroll; positive is down (default 400). |
| `ref` | `string` | Bring this element into view instead. |
| `selector` | `string` | Bring the first match into view instead. |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |

#### Returns

`{ tabId, scrolledTo, y }`

## [Drag](#page-drag)

POST`/api/v1/profiles/:id/page/drag`

Drags from one element to another as a press, a human-shaped move and a release.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `from`required | `{ ref \| selector \| text }` | Where to start. |
| `to`required | `{ ref \| selector \| text }` | Where to drop. |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |

#### Returns

`{ tabId, from, to }`

## [Attach files](#page-upload)

POST`/api/v1/profiles/:id/page/upload`

Attaches files to a file input by absolute path on this machine. Point it at the `<input type="file">` itself, which is often hidden behind a styled button — find it with a selector rather than by clicking the button. Anything else is a `400` naming the element it found.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `paths`required | `string[]` | Absolute file paths on this machine. |
| `ref` | `string` | Ref of the file input. |
| `selector` | `string` | CSS selector of the file input. |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |

#### Returns

`{ tabId, target, files }`

## [Navigate](#page-navigate)

POST`/api/v1/profiles/:id/page/navigate`

Navigates the controlled tab and waits for load. Returns the URL it ended on, which is not always the one you asked for. A page that never fires `load` because one tracker is hanging still returns rather than failing — it is a page you can read.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `url`required | `string` | Absolute URL. |
| `timeoutMs` | `number` | How long to wait for load (default 30000). |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |

#### Returns

`{ tabId, url }`

## [Reload](#page-reload)

POST`/api/v1/profiles/:id/page/reload`

Reloads the controlled tab and waits for load.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |

#### Returns

`{ tabId, url }`

## [Back or forward](#page-history)

POST`/api/v1/profiles/:id/page/history`

Steps through the tab’s real history rather than calling `history.go()`, so it crosses an origin boundary the way the back button does instead of being refused by the page’s same-origin scope. Nothing to step to is a `400` saying which end it is at.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `delta`required | `number` | `-1` for back, `1` for forward. |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |

#### Returns

`{ tabId, url }`

## [Wait for the page](#page-wait)

POST`/api/v1/profiles/:id/page/wait`

Waits for a selector to appear or disappear, for text to show up, or for the network to go quiet. Returns `met: false` on a timeout **rather than failing**, because "it never appeared" is frequently the answer you wanted and an exception makes it indistinguishable from a broken call; `why` says which it was. Waiting on network idle needs the request log running — without it there are no events to be idle between, and that is a `400` rather than a wait that can never end.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `selector` | `string` | CSS selector to wait for. |
| `text` | `string` | Visible text to wait for. |
| `gone` | `boolean` | Wait for it to disappear instead. |
| `networkIdleMs` | `number` | Wait until no request for this many ms. |
| `timeoutMs` | `number` | Give up after this long (default 10000). |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |

#### Returns

`{ tabId, met, waitedMs, url, title, why }`

## [Run script](#page-evaluate)

POST`/api/v1/profiles/:id/page/evaluate`

Runs JavaScript against the page. `world: "isolated"` is the default and the page cannot see it — it is also where refs resolve, so passing a `ref` hands the expression that element as `element`. `world: "page"` runs in the page’s own realm, which is the only way to reach a framework’s globals and is the one mode the page could notice. A ref cannot be passed to page-world code, and asking is a `400` saying so rather than a silent `undefined`.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `expression`required | `string` | A JavaScript expression; an await is fine. |
| `world` | `"isolated" \| "page"` | Default `isolated`. |
| `ref` | `string` | Handed to the expression as `element`; isolated world only. |
| `timeoutMs` | `number` | Default 20000. |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |

#### Returns

`{ tabId, world, value }`

## [Read the console](#page-console)

GET`/api/v1/profiles/:id/page/console`

What the page has logged, from two sources with two different costs. **Free and invisible**, always on: the browser’s own entries through the CDP `Log` domain — failed requests, CSP and mixed-content violations, deprecations, DOM warnings — which arrive with a `browser:<source>` kind. **Page-visible, and off by default**: the page’s own `console.log` calls *and* its uncaught errors and unhandled rejections, which need the wrapper that `POST /profiles/:id/page/console-mode` installs. That split is measured, not assumed. An earlier version captured uncaught errors from an isolated world and captured nothing at all — Blink reports an uncaught exception only to the world it was thrown in — and `Log.enable` does not carry them either, because they ride `Runtime.exceptionThrown`, which needs the one domain that makes a profile detectable. `note` always says which sources you are getting. Timestamps on page entries are stamped on read rather than in the page, and ordering between the sources is approximate; that is stated rather than implied, because a timestamp that looks precise and is not is worse than none.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Query parameters

| Field | Type | What it is |
| --- | --- | --- |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |
| `limit` | `number` | Return at most this many, newest last. |
| `kinds` | `string` | Comma-separated: uncaught, rejection, log, info, warn, error, debug, trace. |
| `clear` | `boolean` | Empty the buffer after reading. |

#### Returns

`{ tabId, entries, mode, dropped, note }`

## [Capture page console calls](#page-console-mode)

POST`/api/v1/profiles/:id/page/console-mode`

Turns on capture of everything that happens in the page’s own world: its `console.log` calls, its uncaught errors and its unhandled rejections. **This is the one page-control feature a page could detect**, and it is off by default for that reason — seeing a `console.log` call means being the function that gets called, so `console` is wrapped in the page’s own realm and the error listeners are registered there too. The wrapper’s `toString` reports a native body, but a page holding a reference from before it ran can still tell. There is no invisible alternative for these three, and that is measured rather than assumed: isolated-world listeners receive no uncaught exceptions, and the `Log` domain does not carry them. Turning it off restores the original `console` functions exactly. The reply carries `detectable` and a sentence saying so, because a switch that costs something and does not say so has misled the caller by omission.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `mode`required | `"off" \| "page"` | `page` wraps the console; `off` puts it back exactly as it was. |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |

#### Returns

`{ tabId, mode, detectable, note }`

## [Start the request log](#page-network-start)

POST`/api/v1/profiles/:id/page/network/start`

Starts recording every request this tab makes, and keeps recording across navigations until you stop it. Unlike the fixed-window tab route, this runs in the background, so you can act and then read. Requests from before you started are not in it — reload if you need the ones that built the page. A recording left running stops itself after thirty minutes, and the next read says that it did.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |

#### Returns

`{ tabId, recording: true, note }`

## [Read the request log](#page-network)

GET`/api/v1/profiles/:id/page/network`

The recorded requests with their method, type, status and failure reason. Carries `total`, how many were `dropped` when the buffer filled, and `stoppedBecause` when the log simply ends — a truncated list that reports itself as complete is the same lie as a count over eight failures.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Query parameters

| Field | Type | What it is |
| --- | --- | --- |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |
| `limit` | `number` | Return at most this many, newest last. |
| `filter` | `string` | Substring of the URL, or an exact resource type or method. |
| `clear` | `boolean` | Empty the log after reading. |

#### Returns

`{ tabId, entries, recording, total, returned, dropped, stoppedBecause }`

## [Stop the request log](#page-network-stop)

POST`/api/v1/profiles/:id/page/network/stop`

Stops recording. What has already been recorded stays readable.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |

#### Returns

`{ tabId, recording: false }`

## [Read a response body](#page-network-body)

POST`/api/v1/profiles/:id/page/network/body`

The response body of one recorded request, by URL. Fetched on demand rather than buffered, so recording a busy page does not hold every response in memory — which also means the recording has to still be running, and a `400` says so.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `url`required | `string` | The exact URL from the request log. |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |

#### Returns

`{ tabId, url, body, base64 }`

## [Read dialogs](#page-dialogs-read)

GET`/api/v1/profiles/:id/page/dialogs`

What `alert`, `confirm` and `prompt` have asked, and how they are being answered.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Query parameters

| Field | Type | What it is |
| --- | --- | --- |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |

#### Returns

`{ tabId, policy, promptText, total, dialogs }`

## [Set the dialog policy](#page-dialogs-set)

POST`/api/v1/profiles/:id/page/dialogs`

Sets how JavaScript dialogs are answered. This exists because attaching at all makes it necessary: Chromium dismisses a dialog itself when nothing is attached to the page, but the moment page control attaches, the page **blocks until this client answers** — so a default of doing nothing would hang any page that calls `alert()`, the automation causing the fault it was sent to observe. Dismiss is the default, every dialog is recorded, and the log is readable.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `policy` | `"dismiss" \| "accept"` | Default `dismiss`. |
| `promptText` | `string` | What to type into a `prompt` when accepting. |
| `tabId` | `string` | Tab id from `GET /profiles/:id/tabs`. Omit for the tab with content on it. |

#### Returns

`{ tabId, policy, promptText, total, dialogs }`

## [List page attachments](#page-attachments)

GET`/api/v1/page/attachments`

Every tab page control currently holds open, with whether it is recording and how long it has been idle. Attachments are made on first use and dropped when the profile stops or after ten idle minutes; this exists so one that outlives its usefulness is visible rather than merely forgotten — a leaked DevTools attachment leaves the page marked as inspected.

#### Returns

`{ total, attached: [{ profileId, tabId, recording, consoleMode, idleSeconds }] }`

## [Release an attachment](#page-detach)

POST`/api/v1/profiles/:id/page/detach`

Releases the attachment for a tab, or for every tab of a profile when `tabId` is omitted. Rarely needed — they expire on their own and a stopped profile drops all of its own — but it is here for when you want the page left with nothing attached. Refs from previous snapshots die with it. Releasing something already released is `detached: false`, not a `404`.

#### Path parameters

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `tabId` | `string` | One tab; omit to release every tab of this profile. |

#### Returns

`{ profileId, tabId?, detached }`

## Run the API on your own profiles

The REST API and the MCP server are part of Pro: $49 a month, with the first 14 days free. Free has every browser feature and no API key.

Mosaic is a desktop app

Email yourself the link and open it on your computer.
