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.

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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Query parameters

FieldTypeWhat it is
tabIdstringTab id from GET /profiles/:id/tabs. Omit for the tab with content on it.
maxNodesnumberCap on nodes, 50–5000 (default 1500).
everythingbooleanInclude 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Query parameters

FieldTypeWhat it is
tabIdstringTab id from GET /profiles/:id/tabs. Omit for the tab with content on it.
selectorstringCSS selector to search within.
textstringVisible text the element contains.
rolestringARIA role, e.g. button.
namestringAccessible name.
exactbooleanRequire the name to match exactly.
limitnumberMaximum 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Query parameters

FieldTypeWhat it is
tabIdstringTab id from GET /profiles/:id/tabs. Omit for the tab with content on it.
refstringRef from a snapshot.
selectorstringCSS selector.
maxCharsnumberCap 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Query parameters

FieldTypeWhat it is
tabIdstringTab id from GET /profiles/:id/tabs. Omit for the tab with content on it.
refstringPhotograph this element alone.
selectorstringPhotograph the first match alone.
fullPagebooleanThe 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat it is
refstringRef from a snapshot or find.
selectorstringCSS selector.
textstringVisible text.
firstbooleanWith text, take the first match instead of refusing.
tabIdstringTab 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat it is
refstringRef.
selectorstringCSS selector.
textstringVisible text.
tabIdstringTab 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat it is
textrequiredstringThe characters to type.
refstringRef of the field.
selectorstringCSS selector of the field.
clearbooleanSelect the existing value first, so this replaces it. Needs a target.
tabIdstringTab 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat it is
textrequiredstringThe value to put in the field.
refstringRef of the field.
selectorstringCSS selector of the field.
tabIdstringTab 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat it is
keyrequiredstringEnter, Tab, Escape, Backspace, Delete, the arrows, Home, End, PageUp, PageDown, Insert, Space, F1–F12.
modifiersstring[]Any of Alt, Control, Meta, Shift.
tabIdstringTab 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat it is
valuesrequiredstring[]Option values or labels.
refstringRef of the select.
selectorstringCSS selector of the select.
tabIdstringTab 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat it is
amountnumberPixels to scroll; positive is down (default 400).
refstringBring this element into view instead.
selectorstringBring the first match into view instead.
tabIdstringTab 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat it is
fromrequired{ ref | selector | text }Where to start.
torequired{ ref | selector | text }Where to drop.
tabIdstringTab 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat it is
pathsrequiredstring[]Absolute file paths on this machine.
refstringRef of the file input.
selectorstringCSS selector of the file input.
tabIdstringTab 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat it is
tabIdstringTab 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat it is
deltarequirednumber-1 for back, 1 for forward.
tabIdstringTab 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat it is
selectorstringCSS selector to wait for.
textstringVisible text to wait for.
gonebooleanWait for it to disappear instead.
networkIdleMsnumberWait until no request for this many ms.
timeoutMsnumberGive up after this long (default 10000).
tabIdstringTab 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat it is
expressionrequiredstringA JavaScript expression; an await is fine.
world"isolated" | "page"Default isolated.
refstringHanded to the expression as element; isolated world only.
timeoutMsnumberDefault 20000.
tabIdstringTab 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Query parameters

FieldTypeWhat it is
tabIdstringTab id from GET /profiles/:id/tabs. Omit for the tab with content on it.
limitnumberReturn at most this many, newest last.
kindsstringComma-separated: uncaught, rejection, log, info, warn, error, debug, trace.
clearbooleanEmpty 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat it is
moderequired"off" | "page"page wraps the console; off puts it back exactly as it was.
tabIdstringTab 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat it is
tabIdstringTab 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Query parameters

FieldTypeWhat it is
tabIdstringTab id from GET /profiles/:id/tabs. Omit for the tab with content on it.
limitnumberReturn at most this many, newest last.
filterstringSubstring of the URL, or an exact resource type or method.
clearbooleanEmpty 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat it is
tabIdstringTab 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat it is
urlrequiredstringThe exact URL from the request log.
tabIdstringTab 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Query parameters

FieldTypeWhat it is
tabIdstringTab 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat it is
policy"dismiss" | "accept"Default dismiss.
promptTextstringWhat to type into a prompt when accepting.
tabIdstringTab 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat it is
tabIdstringOne tab; omit to release every tab of this profile.

Returns

{ profileId, tabId?, detached }

Next Try it on your own accounts

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.