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
- GETFind elements
- GETRead the HTML
- GETScreenshot the page or an element
- POSTClick
- POSTHover
- POSTType
- POSTClear and fill a field
- POSTPress a key
- POSTChoose in a select
- POSTScroll
- POSTDrag
- POSTAttach files
- POSTNavigate
- POSTReload
- POSTBack or forward
- POSTWait for the page
- POSTRun script
- GETRead the console
- POSTCapture page console calls
- POSTStart the request log
- GETRead the request log
- POSTStop the request log
- POSTRead a response body
- GETRead dialogs
- POSTSet the dialog policy
- GETList page attachments
- POSTRelease an attachment
Snapshot the page
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 |
|---|---|---|
idrequired | 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
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 |
|---|---|---|
idrequired | 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
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 |
|---|---|---|
idrequired | 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
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 |
|---|---|---|
idrequired | 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
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 |
|---|---|---|
idrequired | 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
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 |
|---|---|---|
idrequired | 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
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 |
|---|---|---|
idrequired | string | The record’s id. |
Body
| Field | Type | What it is |
|---|---|---|
textrequired | 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
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 |
|---|---|---|
idrequired | string | The record’s id. |
Body
| Field | Type | What it is |
|---|---|---|
textrequired | 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
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 |
|---|---|---|
idrequired | string | The record’s id. |
Body
| Field | Type | What it is |
|---|---|---|
keyrequired | 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
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 |
|---|---|---|
idrequired | string | The record’s id. |
Body
| Field | Type | What it is |
|---|---|---|
valuesrequired | 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
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 |
|---|---|---|
idrequired | 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
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 |
|---|---|---|
idrequired | string | The record’s id. |
Body
| Field | Type | What it is |
|---|---|---|
fromrequired | { ref | selector | text } | Where to start. |
torequired | { 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
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 |
|---|---|---|
idrequired | string | The record’s id. |
Body
| Field | Type | What it is |
|---|---|---|
pathsrequired | 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 }
Reload
POST/api/v1/profiles/:id/page/reload
Reloads the controlled tab and waits for load.
Path parameters
| Field | Type | What it is |
|---|---|---|
idrequired | 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
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 |
|---|---|---|
idrequired | string | The record’s id. |
Body
| Field | Type | What it is |
|---|---|---|
deltarequired | 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
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 |
|---|---|---|
idrequired | 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
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 |
|---|---|---|
idrequired | string | The record’s id. |
Body
| Field | Type | What it is |
|---|---|---|
expressionrequired | 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
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 |
|---|---|---|
idrequired | 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
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 |
|---|---|---|
idrequired | string | The record’s id. |
Body
| Field | Type | What it is |
|---|---|---|
moderequired | "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
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 |
|---|---|---|
idrequired | 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
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 |
|---|---|---|
idrequired | 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
POST/api/v1/profiles/:id/page/network/stop
Stops recording. What has already been recorded stays readable.
Path parameters
| Field | Type | What it is |
|---|---|---|
idrequired | 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
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 |
|---|---|---|
idrequired | string | The record’s id. |
Body
| Field | Type | What it is |
|---|---|---|
urlrequired | 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
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 |
|---|---|---|
idrequired | 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
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 |
|---|---|---|
idrequired | 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
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
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 |
|---|---|---|
idrequired | 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.