HTTP API reference

Endpoints

Endpoint Method What It Does
/ POST Execute any browser action (see Actions Reference below)
/screenshot/browser GET Browser viewport as PNG (what the page looks like)
/screenshot/desktop GET Full virtual desktop as PNG (including browser chrome)
/state GET Current URL, page title, and window offset as JSON
/events GET Authenticated browser event stream as SSE
/health GET Returns ok while the HTTP server is serving; not a browser health probe.
/mcp/ POST MCP (Model Context Protocol) Streamable HTTP endpoint

Authentication

If AUTH_TOKEN is set, all requests (except /health) require authentication. Query-string auth_token values are rejected. This authentication does not cover noVNC; see installation and security.

Authorization: Bearer <token>

Request correlation headers

Every request gets a trace_id (logged on every line of the server’s JSON log for that request) and a request_id. Both come back in the response so you can correlate your side with server logs.

  • X-Request-Id: pass an incoming value (shape [A-Za-z0-9._-]{1,64}) and the server logs that exact ID and echoes it back. If missing or invalid, the server mints a fresh UUID4-hex and uses that. Use it for end-to-end tracing from your client (n8n, MCP, curl) through to the browser action.
  • X-Trace-Id: the server always sets it on the response (UUID4 hex). Find any log line for this request by grepping the JSON log for the trace ID.

Auto-recovery from browser crashes

If Camoufox/Firefox dies mid-session (OOM, segfault, external SIGKILL), the next browser-dependent request triggers an automatic relaunch. The server tears down the dead context and runs launch_persistent_context again using its profile. Persisted cookies and fingerprint data can survive; unsaved state and a usable login are not guaranteed after a crash. The recovery request includes browser startup time.

Crash diagnostics land in the JSON log at WARNING whenever recovery fires: available kernel OOM messages, /proc/meminfo, /proc/loadavg, and the surviving process inventory. Kernel logs may be unavailable inside the container. Check memory limits and actual container state rather than assuming every crash is an OOM kill. Recovery handles a dead browser while the application remains alive; it does not restart a terminated container.

Cluster mode restriction

When running in cluster mode (NUM_REPLICAS > 1), only run_script, ping, and sleep actions are allowed. All other actions return an error directing you to use run_script. This prevents stale content bugs from calls hitting different browser instances. See cluster-mode.md for details.

Request serialization

HTTP action requests and MCP actions share one execution lock per browser. Additional actions wait for it. Health, state, event streams, and screenshot routes are outside that lock; browser-dependent reads can still wait on browser RPCs or recovery.

Request / response format

Command format:

POST http://localhost:8080/
Content-Type: application/json

{"action": "action_name", "param1": "value1", "param2": "value2"}

Response format:

{
  "success": true,
  "timestamp": 1234567890.123,
  "data": { ... },
  "error": "only present when success is false"
}

The data field contains action-specific results (page text, element coordinates, cookie values, etc.).

Failure responses

POST / expects a JSON object with an action. Most action failures use the same envelope with success: false and an error string while retaining HTTP 200. Authentication failures return HTTP 401. Invalid event-stream cursors return HTTP 400. Browser screenshots return HTTP 503 when no active page is available, HTTP 400 for conflicting selector/full-page options, or HTTP 500 when capture fails. Screenshot failures are plain text rather than the action envelope. Do not assume curl’s HTTP success proves an action succeeded, or that arbitrary malformed request bodies share one error schema.

Example: login flow with OS-level input

API=http://localhost:8080

# 1. Navigate to login page
curl -X POST $API -H 'Content-Type: application/json' \
  -d '{"action": "goto", "url": "https://example.com/login"}'

# 2. Find all interactive elements (buttons, inputs, links)
curl -X POST $API -H 'Content-Type: application/json' \
  -d '{"action": "get_interactive_elements"}'
# Returns elements with x, y (centre), w, h, text, and CSS selectors

# 3. Click the email field (use x, y, w, h from step 2)
curl -X POST $API -H 'Content-Type: application/json' \
  -d '{"action": "system_click", "x": 400, "y": 200, "w": 280, "h": 36}'

# 4. Type email with human-like keystrokes
curl -X POST $API -H 'Content-Type: application/json' \
  -d '{"action": "system_type", "text": "user@example.com"}'

# 5. Tab to password field
curl -X POST $API -H 'Content-Type: application/json' \
  -d '{"action": "send_key", "key": "tab"}'

# 6. Type password
curl -X POST $API -H 'Content-Type: application/json' \
  -d '{"action": "system_type", "text": "not-a-real-password"}'

# 7. Press Enter to submit
curl -X POST $API -H 'Content-Type: application/json' \
  -d '{"action": "send_key", "key": "enter"}'

# 8. Wait for redirect to dashboard
curl -X POST $API -H 'Content-Type: application/json' \
  -d '{"action": "wait_for_url", "url": "**/dashboard", "timeout": 15}'

# 9. Verify we're logged in
curl -X POST $API -H 'Content-Type: application/json' \
  -d '{"action": "get_text"}'

The clicks, typing, and key presses in this example use OS-level input with modeled paths, holds, and rhythm. The coordinates are illustrative; resolve the real target positions before using them. This does not guarantee that the target site accepts the session or classifies it as human.

Actions reference

All actions are sent as POST / with JSON body {"action": "name", ...params}.

Element targets

Actions that operate on one element accept exactly one of selector, element_id, or role. A role target also accepts the accessible name. Selector and role targets accept frames, an ordered list of iframe selectors from the top page to the target frame. Frame chains support up to eight levels. CSS selectors pierce open shadow roots. Closed shadow roots are inaccessible.

create_element_reference returns an opaque element_id that remains tied to the same DOM node while it is attached. References expire after five minutes, are bounded to 512 per process, and become stale after top-level navigation, page close, browser relaunch, or explicit release. get_element, DOM input, waits, uploads, element screenshots, and OS-level target actions accept these references. get_accessibility_tree accepts selector or role targets, but not element_id.

Action Parameters What It Does
goto url, wait_until, referer, timeout, retry_count, retry_delay Navigate to a URL. wait_until: "domcontentloaded" (default), "load", "networkidle". referer: set the HTTP Referer header.
refresh wait_until, timeout, retry_count, retry_delay Reload the current page. Returns URL and title.

Navigation never relies on a Playwright default. Every attempt gets the app’s explicit timeout of 30 seconds unless you set another value. retry_count defaults to 1, so a timed-out navigation gets one retry. Set it to 0 to disable retries. retry_delay defaults to 1 second and doubles before each further retry. timeout must be 0.1-120 seconds, retry_count 0-2, retry_delay 0-10 seconds, and the whole worst-case attempt sequence must fit within 120 seconds. Only Playwright navigation timeouts retry. Other navigation failures return immediately.

System input (OS-level)

These actions drive the real X11 mouse and keyboard with timing modeled on how people actually move, click, type, and scroll. See Humanized Input for the model, the defaults, and every tuning option.

Action Parameters What It Does
system_click x, y, w, h, duration, plus pointer options Moves the mouse to the target centred on viewport x, y along a human path, then clicks with a human pause and button hold. Pass the element’s w and h (from get_interactive_elements) so the click lands at a human offset inside it, never dead centre. Last resort. Prefer click with a CSS selector. The screen offset is re-measured before every move, so no calibrate call is needed. Returns landed_at with the viewport point that was clicked.
mouse_move x, y, w, h, duration, speed, curvature, tremor Moves the mouse like system_click but does not click. Use to hover over elements (dropdown menus, tooltips). Returns landed_at.
mouse_click x, y (optional), w, h, plus pointer options With x, y: moves there like system_click, then clicks. Without them: clicks where the pointer is, with the same human press and hold. Give both x and y or neither. Returns clicked_at (the requested point, or "current") and landed_at.
system_type text, interval, typos, typo_rate, key_hold, rollover Types text with real OS keystrokes: each key is held and released, fast typists overlap keys, Shift goes down before capitals, and the rhythm follows the key pair. Characters outside the US layout (é, ñ, €) are typed too. typos: true makes and corrects occasional typos on plain text fields only (see Typos). You must focus an input field first. Returns typed_len, typos (true when typos were enabled for the field), and typos_disabled_reason when typos were requested but refused.
send_key key, key_hold, instant Sends a keyboard key or combo with a human hold. instant: true presses and releases at once with no human timing, as before 3.0.0. It is faster but easy for a page to spot. Examples: "enter", "tab", "escape", "backspace", "ctrl+a", "ctrl+shift+t". Combo keys go down in order and come up in reverse. Uses PyAutoGUI key names.
scroll amount, x, y, notch_gap Scrolls amount mouse wheel notches (a non-zero integer, default -3), one at a time with a human rhythm. Amounts above 4 are split into several gestures with short pauses. Negative = scroll down, positive = scroll up. If x, y are provided (both or neither), moves the mouse there first (useful for scrolling inside a specific element).
human_pause duration, x, y, w, h, drift, drift_interval, drift_distance, speed, curvature, tremor Represents visible reading, review, or composition time. It optionally parks the pointer inside a viewport rectangle, remains mostly still, and makes sparse bounded human-shaped drifts. It blocks for the requested time and never starts background movement. Use sleep for ordinary waiting. Returns duration, elapsed, drifts, parked, and landed_at.
system_hover element target, plus pointer options Resolves the element’s current viewport rectangle and moves the real pointer into it.
system_double_click element target, plus pointer options Resolves the target, moves there, and sends two real left-button clicks with a valid double-click interval.
system_right_click element target, plus pointer options Resolves the target and sends a real right-button click.
system_mouse_down optional element target, button Moves to the target when supplied, then holds the selected button.
system_mouse_up button Releases the selected held button at the current pointer position.
system_drag source, target, plus pointer options Resolves source and target objects, then performs a real held-button drag with humanized movement.

Playwright input (DOM events, use these first)

Action Parameters What It Does
click selector Preferred click method. Clicks an element by CSS selector or XPath (xpath=//button[@id='submit']). Fast and reliable. Only fall back to system_click if the site actively detects and blocks DOM event injection.
fill selector, value Sets an input field’s value instantly. Clears existing content first. Fast but detectable, because it doesn’t generate individual keystroke events.
type selector, text, delay Types into an element character-by-character via Playwright. Middle ground between fill (instant) and system_type (OS-level). delay defaults to 0.05s between keys.
hover element target Hovers a target through Playwright.
double_click element target Double-clicks a target through Playwright.
right_click element target Right-clicks a target through Playwright.
drag_and_drop source, target Drags one target object to another through Playwright.

Page inspection

Action Parameters What It Does
get_interactive_elements visible_only Scans the page and returns interactive elements with viewport coordinates, dimensions, text, selector, visibility, and reusable element_id where a stable handle can be created.
get_text none Returns all visible text from the page body (truncated to 10,000 chars). Usually the first thing to call after navigating. It tells you what’s on the page without a screenshot.
get_html none Returns the full HTML source of the page. Use when get_text doesn’t give enough structure.
get_page_info none Returns the current URL, title, ready state, viewport/document dimensions, and scroll position.
detect_challenge scroll_into_view Read-only best-effort detection of documented challenge integrations and conservative visible generic cues. Optional viewport reveal never clicks, solves, focuses, or enters a frame.
solve_captcha task, timeout, poll_interval Submit an Anti-Captcha task using the server-side configured key and wait for the provider solution. Requires CAPTCHA_SOLVER_PROVIDER=anticaptcha.
get_element element target Returns one matching element’s reusable ID, tag, text, attributes, bounding box, and visibility.
get_elements selector, frames, limit Returns summaries and reusable IDs for up to limit matches. limit is 1 through 100 and defaults to 20.
get_computed_style selector, properties Returns computed CSS property values. properties accepts up to 50 CSS property names; defaults to display, visibility, color, and background-color.
eval expression, world Executes JavaScript in the page and returns the result. Example: "document.title", "document.querySelectorAll('a').length". world is page (default) or isolated. page runs in the page’s own JavaScript world and can read globals the site defines, such as window.__NEXT_DATA__. isolated sees the DOM but not the page’s variables, and page scripts cannot observe or hook it. Use isolated for DOM reads on sites that watch for automation.
create_element_reference element target Returns metadata and a reusable opaque element_id.
release_element_reference element_id Disposes one stored reference.
get_accessibility_tree optional selector or role target Returns Playwright’s ARIA snapshot for the target or page body.
get_by_role role, name, frames, limit Returns matching accessible elements and reusable IDs.

Challenge detection

detect_challenge is for notifying a human during an authorised QA or compatibility flow, not for bypassing a verification system. It inspects only the top-level document’s bounded iframe/script origins, fixed widget selectors, and published integration globals. It does not traverse cross-origin frames, click any widget, submit a response, or make a network request.

{"action": "detect_challenge"}

Pass "scroll_into_view": true when a person will take over in VNC and the first visible detected frame or widget should be brought into the viewport:

{"action": "detect_challenge", "scroll_into_view": true}

scroll_into_view defaults to false, preserving read-only detection behavior. When requested, the action scrolls only; it never clicks, focuses, solves, submits, or enters a challenge frame. The opt-in response adds data.scrolled_into_view, which is true when a detected target is in the viewport after the action. It is false when no visible detected target with usable geometry is available or the page cannot scroll it into view.

The action returns data.status as "absent", "present", or "unknown". unknown means that the page snapshot could not be evaluated; it is deliberately not reported as absence. When present, matches contain vendor, confidence ("high", "medium", or "low"), locations, fixed evidence labels, and bounded frame/element bounding boxes where available. Frame evidence includes only host plus path. The action excludes query strings, fragments, site keys, response values, element text, attributes, and HTML.

Known high-confidence markers cover Cloudflare Turnstile, Google reCAPTCHA, hCaptcha, Friendly Captcha, ALTCHA, Arkose, AWS WAF, and GeeTest where their documented container, iframe/script origin, or global is present. A visible generic captcha/challenge cue is returned as vendor "unknown" at low confidence. Detection is intentionally best-effort. An absent result does not guarantee that a custom, delayed, or server-side challenge will not appear later.

Anti-Captcha API tasks

solve_captcha is available through HTTP, the dedicated MCP tool, run_script, and YAML script mode. It accepts an Anti-Captcha task object without clientKey. The configured key and fixed provider endpoint cannot be overridden by a request.

{"action":"solve_captcha","task":{"type":"RecaptchaV2TaskProxyless","websiteURL":"https://example.com","websiteKey":"EXAMPLE-DO-NOT-USE"},"timeout":120,"poll_interval":5}

timeout accepts 5 through 600 seconds. poll_interval accepts 1 through 30 seconds. Provider errors and invalid result shapes fail the action without returning the configured key. The action creates a paid provider task, so callers should invoke it only after detecting a challenge and confirming authorization.

Use it inside a script with the existing output condition:

steps:
  - action: detect_challenge
    scroll_into_view: true
    output_id: challenge
  - if:
      condition:
        type: output
        output_id: challenge
        path: [detected]
        equals: true
      then:
        - action: eval
          expression: "'human-review-needed'"
          output_id: next_step

Wait conditions

Use these instead of sleep. They wait for actual page state, not arbitrary time.

Action Parameters What It Does
wait_for_element selector, state, timeout Waits for an element to reach a state. state: "visible" (default), "hidden", "attached", "detached". timeout in seconds (default 30). CSS or XPath.
wait_for_text text, timeout Waits for specific text to appear anywhere on the page (substring match).
wait_for_url url, timeout Waits for the URL to match a glob pattern. * matches any chars except /, ** matches everything. Example: "**/dashboard".
wait_for_network_idle timeout Waits until no network requests have been made for 500ms. Useful for pages that load content dynamically.
wait_for_dom_change element target, condition, attribute, timeout Waits for added, removed, text, attribute, visible, hidden, or moved. attribute is required for attribute changes.

Tab management

Action Parameters What It Does
list_tabs none Returns all open tabs with their index, URL, and which one is active.
new_tab url, wait_until, timeout, retry_count, retry_delay Opens a new tab (becomes the active tab). Optionally navigates to a URL with the same explicit navigation controls as goto.
switch_tab index Switches the active tab by index (0-based). All subsequent actions operate on the active tab.
close_tab index (optional) Closes a tab. If no index, closes the active tab. After closing, the last remaining tab becomes active.
list_windows none Returns visible Camoufox X windows, geometry, page URL, and active state.
focus_window index Raises and focuses the page/window index.
move_resize_window index, x, y, width, height Moves and resizes one visible Camoufox X window. Minimum size is 100 by 100.
wait_for_popup after_id, timeout Waits for a popup event after the supplied event ID.
close_window index Closes the page at the window index.

Firefox opens each tab as its own OS window, so new_tab / switch_tab / close_tab also foreground the target tab’s window (the display, screenshots, recordings, and VNC follow the active tab) and transfer OS-level keyboard focus into its content so send_key / system_type reach the switched tab. Foregrounding uses a brief off-screen mouse gesture that renders no context menu (clean in recordings).

Closing the last tab leaves Firefox with no window, and Firefox cannot open a tab from that state. list_tabs then reports count: 0. The next action that needs a page, including new_tab and GET /screenshot/browser, relaunches the browser on the same profile and runs on the fresh tab. The relaunch takes a few seconds. Cookies and storage survive it, but the closed page’s state does not.

Dialog handling

Browsers have modal dialogs (alert, confirm, prompt). By default, the browser auto-accepts dialogs (clicks OK). Use handle_dialog to dismiss or provide prompt text.

Action Parameters What It Does
handle_dialog accept, text Pre-configures how the browser handles the next dialog. accept: true = click OK, false = click Cancel. text: response for prompt dialogs. Call this before the action that triggers the dialog. If you don’t, the dialog is auto-accepted (clicks OK). You only need this if you want to dismiss (Cancel) or provide prompt text.
get_last_dialog none Returns info about the last dialog: type (alert/confirm/prompt/beforeunload), message, default_value, buttons.

Cookies

Action Parameters What It Does
get_cookies urls (optional) Returns all browser cookies. Optionally filter by URL list. Each cookie includes name, value, domain, path, httpOnly, secure, etc.
set_cookie name, value, url/domain, … Sets a cookie. Needs at minimum: name, value, and either url or domain. Accepts all standard cookie fields (path, httpOnly, secure, sameSite, expires).
delete_cookies none Clears all cookies from the browser context.

Storage

Access the page’s localStorage and sessionStorage. Storage is per-origin, so you must be on the right page.

Action Parameters What It Does
get_storage type Returns all items as key-value pairs. type: "local" (default) or "session".
set_storage type, key, value Sets a single key-value pair.
clear_storage type Clears all items.

Downloads & uploads

Action Parameters What It Does
get_last_download none Returns info about the most recent file download: url, filename, and local path inside the container. Returns null if nothing downloaded yet.
upload_file selector, file_path Programmatically sets a file on an <input type="file"> element without opening the OS file picker. File must exist inside the container (use docker cp to copy files in). You still need to submit the form after.

Network logging

Record all HTTP requests and responses the page makes. Useful for finding API endpoints, debugging, or verifying resources loaded.

Action Parameters What It Does
enable_network_log capture_request_bodies, capture_response_bodies, max_body_bytes Starts recording. Body capture is off by default. The per-body limit defaults to 256 KiB and may not exceed 1 MiB.
disable_network_log none Stops recording. Already-captured entries remain.
get_network_log none Returns all captured entries with their count.
clear_network_log none Deletes captured entries. Keeps logging on if it was on.
getclear_network_log none Returns all captured entries and clears the log in one call.
set_network_rules rules Installs ordered future-request rules. Each rule has a URL glob and action: block, abort, continue, redirect, or fulfill. Optional method and resource_type narrow the match. Redirects require an HTTP(S) redirect_url without credentials. Fulfill rules accept bounded status, headers, content type, and text body.
clear_network_rules none Removes all interception rules.
export_har optional path Returns a bounded HAR 1.2 object assembled from the current network log and optionally writes it to a script-mode path.

Rules affect only requests started after installation. Sensitive request and response headers are replaced with [REDACTED]. URL query values whose names look like credentials are also redacted. Response bodies are base64 in network log and HAR results.

Console logging

Capture console.log, console.error, console.warn, and other console output from the page. Useful for debugging page behavior or extracting data logged by scripts.

Action Parameters What It Does
enable_console_log none Starts capturing console messages. Each entry has: type (log/error/warning/info/debug/trace/table/etc), text, location, timestamp.
disable_console_log none Stops capturing. Already-captured entries remain.
get_console_log none Returns all captured entries with their count.
clear_console_log none Deletes captured entries. Keeps capturing on if it was on.
getclear_console_log none Returns all captured entries and clears the log in one call.

Display & calibration

Action Parameters What It Does
calibrate none Measures the mapping between viewport coordinates (from get_interactive_elements) and screen coordinates (what PyAutoGUI uses) and returns it as window_offset. You do not need to call it: every OS-level pointer action and every viewport recording re-measures the offset first, so fullscreen, new tab windows, and browser relaunches are handled.
get_resolution none Returns the virtual display resolution (width, height).
set_color_scheme scheme Makes every open tab, and every tab opened later, report dark, light, or no-preference for prefers-color-scheme, so sites that follow the system theme switch to it. Firefox has no no-preference media value, so pages see light for it. default turns this off. Takes effect at once and survives a browser relaunch. It changes the page content only, not the browser’s own toolbar. Returns scheme and pages, the number of open tabs it was applied to.
enter_fullscreen none Puts the browser in fullscreen mode (hides address bar and window chrome).
exit_fullscreen none Exits fullscreen mode.

Clipboard

The clipboard actions use the real X11 CLIPBOARD selection shared by Camoufox and noVNC. Browser copy operations are visible to get_clipboard, and text written by set_clipboard can be pasted into the page with Ctrl+V. Clipboard contents are not written to request logs.

For substantial text, prefer set_clipboard, focus the target field, then call send_key with key: "ctrl+v". Use system_type for short edits, fields that reject paste, or tests that specifically need per-keystroke behavior.

Action Parameters What It Does
set_clipboard format, text, content_base64, files Replaces one clipboard target. format defaults to text. Use text for text and html, strict base64 PNG in content_base64 for image/png, or up to 32 absolute existing regular file paths in files. Text and clipboard payloads are limited to 1 MiB. File lists reject symlinks and more than 1 GiB of aggregate file size.
get_clipboard format Reads text, html, image/png, or files. PNG content returns as base64. File content is not returned.

Both actions work as HTTP actions, dedicated MCP tools, YAML script steps, and run_script steps. Cluster mode exposes only run_script, so place clipboard operations in the same script when they must run on one browser instance.

Scrolling

Action Parameters What It Does
scroll_to_bottom delay Scrolls the entire page top-to-bottom using JavaScript (window.scrollBy), then back to top. Useful for triggering lazy-loaded content. delay (default 0.4s) is the pause between scroll steps. This is fast but uses JS, not OS-level input.
scroll_to_bottom_humanized min_clicks, max_clicks, delay, max_gestures, return_to_top, notch_gap, scroll_back, drift Same goal as above but scrolls like a reader with the real mouse wheel: gestures of min_clicks-max_clicks notches (integers 1-50, default 2-6, min_clicks not above max_clicks), a reading pause around delay seconds (default 0.5) after each, idle pointer drift during some pauses, and an occasional short scroll back up. Stops when two gestures in a row move nothing (lazy-loaded pages get one retry) or after max_gestures (1-10000, default 500). With return_to_top (default true) it goes back up with the Home key, or with upward wheel flicks when a text field has focus, instead of a script jump. Returns gestures, scroll_backs, reached_bottom, and returned_to_top (false if the top was not reached within 3 seconds).

Utility

Action Parameters What It Does
run_script steps or yaml, name, on_error Execute multiple actions as a single atomic request. steps may contain action dicts plus if, bounded repeat, and bounded while control nodes. yaml uses the same format as --script mode. on_error: "stop" (default) or "continue". Steps with output_id collect results. See script-mode.md#control-flow.
ping none Health check that returns "pong" and the current page URL.
sleep duration Pauses for N seconds. Prefer wait_for_element or wait_for_text when waiting for page content.
close none Shuts down the browser. The container stops after this.
save_screenshot output_id, path, type, element target, full_page, width, height, whLargest Captures PNG only. type is browser or desktop. Browser capture can select one element or the full page. Element and full-page capture cannot be combined. Optional path writes the PNG. output_id collects base64 in script mode.
get_virtual_media_state none Reports virtual-media configuration and, in dynamic mode, each active source basename and source revision.

Events and human handoff

Action Parameters Result and limits
get_events after_id, types, limit Reads retained events. Defaults: cursor 0, all types, limit 100. Limit is 1 through 1,000.
wait_for_event after_id, types, timeout Returns matching events or an empty list on timeout. Timeout defaults to 30 seconds and must be greater than 0 and at most 300.
begin_handoff none Pauses mutating automation and starts page activity listeners. Fails if already active or no page exists.
handoff_status none Returns active state, start time, and observed activity counts when active.
end_handoff none Returns duration and activity, then resumes automation. Fails if no handoff is active.

get_events reads the bounded 1,000-record process-local event ring. wait_for_event waits without polling. Both accept after_id, an optional types list, and bounded result or timeout controls. Events include navigation, popup and page close, dialogs, downloads, console messages, requests, responses, and handoff state. A cursor older than retained history receives a gap record. A future cursor is rejected.

GET /events streams the same records as server-sent events. Use repeated type query parameters to filter and after_id to resume. The stream uses the same Bearer authentication as the rest of the API. Event storage is process-local and is not shared across cluster replicas.

begin_handoff blocks browser-mutating API and MCP actions while leaving noVNC input available. handoff_status reports trusted pointer, keyboard, and wheel activity observed by listeners on the document where handoff began. Navigation can replace those listeners. end_handoff removes them, returns the final counts, and resumes automation. Screenshot, page reads, event reads, state, and health remain available during handoff. Handoff does not configure viewer access or authentication.

Permissions and geolocation

Action Parameters Result and limits
set_permissions permissions, optional origin Grants a non-empty list from the supported values below. Origin must be bare HTTP(S), without credentials, query or fragment.
clear_permissions none Clears browser permission grants and remembered grants.
set_geolocation latitude, longitude, accuracy Latitude is -90 through 90, longitude -180 through 180, accuracy 0 through 100,000 meters. Accuracy defaults to 0. All values must be finite. Grant geolocation permission separately.

set_permissions grants a non-empty permission list, optionally for one bare HTTP(S) origin. Allowed values are accessibility-events, accelerometer, ambient-light-sensor, background-sync, camera, clipboard-read, clipboard-write, geolocation, gyroscope, magnetometer, microphone, midi, midi-sysex, notifications, and payment-handler. clear_permissions clears all grants. set_geolocation validates finite latitude, longitude, and accuracy before applying them. Configured permission and geolocation state is reapplied after an automatic browser relaunch.

Virtual camera and microphone

Set VIRTUAL_CAMERA_FILE and/or VIRTUAL_MICROPHONE_FILE to make the configured video or audio file available as the corresponding track returned by page navigator.mediaDevices.getUserMedia(). Both files must resolve inside VIRTUAL_MEDIA_DIR (default /media), normally a read-only bind mount. The browser must restart after changing this static source configuration.

With VIRTUAL_MEDIA_DYNAMIC=true, these actions switch a file-backed source while preserving the identities of tracks already returned to a page. Dynamic mode is disabled by default.

Action Parameters What It Does
set_virtual_media_source kind, source Selects an existing source for kind ("camera" or "microphone"). source is a relative name that resolves to a regular file inside VIRTUAL_MEDIA_DIR.
upload_virtual_media kind, filename, content_base64, activate Stores strict base64 file content under VIRTUAL_MEDIA_DIR for kind ("camera" or "microphone"). filename must be a safe basename whose declared media type matches kind; it supplies only the extension. The response returns a generated collision-safe stored basename, and no existing named source is overwritten. Before storage or activation, ffprobe must confirm the decoded content contains the requested video or audio stream. activate optionally switches to the uploaded source immediately. Uploads are limited by VIRTUAL_MEDIA_UPLOAD_MAX_BYTES (50 MiB by default).

Both actions require dynamic mode. Uploads also require a writable VIRTUAL_MEDIA_DIR. They are ordinary authenticated actions: when AUTH_TOKEN is set, include Authorization: Bearer <token> as for every other action. The controller accepts only approved files under the configured media directory. It never accepts arbitrary host paths, remote URLs, WebSocket streams, or other live ingress. get_virtual_media_state reports the active source basename and revision without exposing source paths or upload bytes.

This virtualizes getUserMedia() streams only. It does not add devices to enumerateDevices(). If a request asks for a kind without a configured virtual source, it fails with NotFoundError instead of using a native device. Virtual tracks retain the configured file’s format and do not emulate incompatible exact media constraints. Use get_virtual_media_state to confirm the configured source types and dynamic source state.

Screen recording

ffmpeg x11grab against the Xvfb display captures actual rendered pixels, including the OS-level mouse cursor by default. One recording can be active per container; a second start_recording returns an error. /recordings must exist and be writable. Mount it to retain files outside the container; the recorder checks directory existence and permissions, not whether it is a mount point.

Action Parameters What It Does
start_recording mode, fps, show_cursor Begin recording to /recordings/.tmp-<id>.mp4. The action returns only after ffmpeg creates the temporary MP4, with a 10-second startup limit. mode: "window" (default, full Camoufox window including chrome), "viewport" (crops chrome at the page position from mozInnerScreenX/Y, measured when the recording starts), "desktop" (entire Xvfb screen). fps: 1 to 60, default 15. show_cursor: bool, default true. Set it to false to record without the OS-level mouse cursor (ffmpeg -draw_mouse 0). Returns recording_id, tmp_path, show_cursor, and capture_size.
stop_recording slug Stop the active recording, finalize, and rename tmp file to /recordings/<slug>.mp4. slug must match [a-zA-Z0-9][a-zA-Z0-9_-]{0,62} (no path traversal). If <slug>.mp4 already exists, the file is saved as <slug>-2.mp4, <slug>-3.mp4, etc. Returns path, duration_s, size_bytes.
recording_status none {"active": true, "recording_id", "mode", "started_at", "elapsed_s", "tmp_path"} when recording, {"active": false} otherwise.

Mount and run:

mkdir -p ./recordings
docker run -d -p 8080:8080 \
  -v ./recordings:/recordings \
  psyb0t/stealthy-auto-browse

Script-mode pattern with multiple start/stop pairs in one run. All files land in ./recordings/:

steps:
  - action: start_recording
    mode: window
  - action: goto
    url: https://example.com
    wait_until: networkidle
  - action: stop_recording
    slug: example-page
  - action: start_recording
    mode: viewport
    fps: 30
  - action: goto
    url: https://another.example
  - action: stop_recording
    slug: another-page

Notes:

  • Encoder defaults: H.264 (libx264), -preset ultrafast, -crf 28, yuv420p. Picked for low CPU + reasonable file size on browser footage. To re-encode for distribution, run a second pass externally.
  • viewport mode measures the page’s current screen position (mozInnerScreenX/Y from Firefox) when the recording starts and uses it as the crop top-left, so it crops correctly after fullscreen or tab changes. If the position cannot be read, it uses the last measured offset.
  • In cluster mode (NUM_REPLICAS > 1), recording actions are only usable from inside a run_script call. Outside it, the cluster restriction rejects them. Start and stop must live in the same run_script so they hit the same browser instance.
  • Crashes and forced shutdown abort any active recording and remove its tmp file. At next startup, the app sweeps /recordings/.tmp-*.mp4 files older than 1h.

Humanized input

The OS-level actions above plan every movement and keystroke from published human motor data, then replay it through X11 with each event on its own schedule. Defaults are tuned to look like one consistent person per container; every option below is per request and optional.

Mouse. Each move is a sum of overlapping sigma-lognormal strokes. A curved main stroke lands a little off, a mid-course re-aim sometimes follows, and one or two corrections finish exactly on the target. Movement time follows Fitts’s law (longer moves and smaller targets take longer), speed peaks about a third of the way in, roughly a third of moves overshoot and correct, and a sub-pixel hand tremor rides on top. Events go out at a 125 Hz USB-mouse rate. The defaults were fitted to the SapiMouse dataset of 120 people.

Clicks. After arriving, the press waits a short human pause (usually ~40 ms, sometimes a few hundred while “checking” the target), holds the button ~85-125 ms, and occasionally slips 1-2 px while pressed (Balabit dataset). The aim point is scattered inside the target, never its exact centre.

Typing. Gaps between key presses are right-skewed (lognormal), shorter for common letter pairs and alternating hands, longer for the same finger and at word starts, with a slow drift in pace. Every key has its own hold, so fast typists press the next key before releasing the last; the same key never overlaps itself. Shift is pressed with the opposite hand well before the capital and held across runs of capitals. Each container picks a typist speed between slow and very fast (Aalto 136M keystrokes study).

Wheel. Scrolling happens in gestures of slow controlled notches or fast flicks that speed up and slow down. Two notches are never less than 15 ms apart.

Pauses. human_pause models time spent reading, reviewing, or composing while the page is visible. The pointer stays still most of the time. Optional drifts use the same fitted mouse path planner as other pointer actions, while their sparse, right-skewed timing avoids a periodic jiggle. Use it only when the task naturally includes visible thinking time. It is not a generic delay and should not be inserted between every action.

Pointer options

Accepted by system_click, mouse_click, mouse_move, and human_pause where listed (the click options apply to clicks only).

Option Range Default Effect
w, h > 0 small control Target size in px. The landing point is scattered inside it and movement time follows its size.
duration > 0 Fitts’s law Rescale the whole move to this many seconds. Wins over speed.
speed 0.1-10 1 (session pace) Speed multiplier; 2 moves twice as fast.
curvature 0-5 1 How much the main stroke bends; 0 makes it straight. Corrections keep their own small bend.
tremor 0-5 px 0.35 Hand tremor amplitude.
click_hold 0.01-5 s session ~85-125 ms Median time the button stays down.
click_delay 0-10 s human mix Median pause between arriving and pressing.

Pause options

Accepted by human_pause. With no rectangle, drift stays within the current viewport. When x and y are supplied, both are required and identify the centre of the safe rectangle. w and h default to 80 px.

Option Range Default Effect
duration 0.1-300 s 1 s Time spent in the pause after any initial parking move.
x, y viewport coordinates current pointer Optional centre of the safe rectangle.
w, h 4-10000 px 80 px Safe rectangle size, intersected with the viewport.
drift true/false true Allow sparse pointer drifts during the pause.
drift_interval 1-120 s 12 s Median interval used by the right-skewed drift schedule.
drift_distance 0-500 px 60 px Maximum distance of each drift within the safe rectangle. Zero keeps the pointer still.
speed 0.1-10 1 Speed multiplier for each drift path.
curvature 0-5 1 Curvature of each drift path.
tremor 0-5 px 0.35 Tremor amplitude on each drift path.

Typing options

Accepted by system_type. send_key accepts key_hold (default median 100 ms, held 50-250 ms), or instant: true to skip human timing entirely; the two cannot be combined.

Option Range Default Effect
interval > 0 s session typist speed Median gap between key presses; also picks the matching typist’s spread and key overlap.
key_hold 0.01-2 s session ~75-145 ms Median time each key is held.
rollover 0-1 by typist speed When the next key comes before the last one is released, the chance of keeping that overlap instead of releasing first.
typos true/false false Make and correct occasional typos.
typo_rate 0-0.2 0.02 when typos Typos per letter. Setting it turns typos on; typos: false with a non-zero typo_rate is an error.

Typos

Typos are always corrected. A neighbouring key, a swapped pair, or a doubled or missed letter is noticed right away about half the time, otherwise one to three keys later or at the end of the word, then erased with Backspace and retyped, so the final text is exactly what you asked for. They are only made in a focused plain text/search input or a textarea. They are switched off, and the response says why in typos_disabled_reason (no_focused_text_field, field_type, contenteditable, maxlength, pattern, inputmode, autocomplete, or control_characters), for password, email, number, and other typed inputs, fields with maxlength or pattern, numeric/tel/email/url inputmode, one-time-code, payment, password, email, phone, and URL autocomplete, contenteditable editors, text containing a tab, and newlines in a single-line input. After typing with typos, the field’s value is checked; if a page script changed it so it no longer matches, the action fails with typed value does not match the requested text after typo corrections.

Wheel options

Option Actions Range Default Effect
notch_gap scroll, scroll_to_bottom_humanized 0.015-5 s flick/controlled Median seconds between wheel notches.
scroll_back scroll_to_bottom_humanized 0-1 0.06 Chance per gesture of scrolling back up a little (at most twice per page).
drift scroll_to_bottom_humanized 0-1 0.4 Chance per reading pause of drifting the pointer.

Out-of-range values are rejected with an error naming the option before any input is sent. Existing input options accept numeric strings such as "250"; human_pause numeric fields require JSON numbers. Boolean options must be JSON booleans; "true" is rejected.

OS-level input runs in a worker thread, so /health, screenshots, and MCP stay responsive while a long system_type or scroll plays. Actions themselves still run one at a time.

Screenshots

Both screenshot endpoints return PNG and support resize parameters. The browser endpoint also accepts selector, repeated frame parameters for an ordered iframe chain, and full_page=true. Element and full-page capture cannot be combined. The default resolution is 1920x1080, which is a big image. You almost always want to resize.

# Resize to 512px on the longest side (best default: keeps aspect ratio and size small)
curl http://localhost:8080/screenshot/browser?whLargest=512 -o screenshot.png

# Resize to 800px wide
curl http://localhost:8080/screenshot/browser?width=800 -o screenshot.png

# Exact 400x400 dimensions
curl 'http://localhost:8080/screenshot/browser?width=400&height=400' -o screenshot.png

# Full desktop (includes browser chrome, taskbar, etc.)
curl http://localhost:8080/screenshot/desktop?whLargest=512 -o desktop.png
Parameter What It Does
whLargest=512 Scales so the largest dimension is 512px, keeps aspect ratio. Use this by default.
width=800 Scales to 800px wide, keeps aspect ratio.
height=300 Scales to 300px tall, keeps aspect ratio.
width=400&height=400 Forces exact dimensions (may stretch).