Auto-generated from
src/api-schema.ts- Do not edit directly.
# Is it working?
curl localhost:8700/status
# What tabs are connected?
curl localhost:8700/windows
# What's on the page?
curl -X POST localhost:8700/tree -d '{"mode":"actionable"}'
# Click something
curl -X POST localhost:8700/click -d '{"selector":"#submit"}'
GET /statusServer status
Returns server info and connected browser count.
Response: { serverVersion, ready, windows: […], browsers: n, desktopApp, pid, … }
Use to verify the server is running — but gate a test lane on ready, not on the 200. A
server can be up with zero connected tabs: /status answers fine and there is still nothing to
drive, so a lane that adopts it fails later on a timeout that points at the caller’s own code.
ready is true when at least one top-level tab is connected. hj doctor checks this (plus
ambiguous targeting) and exits non-zero, which is the one-command preflight for a lane.
GET /statsEfficiency and usage statistics
Returns metrics showing Haltija’s efficiency vs raw DOM events.
Response includes:
Use this to verify efficiency claims (99%+ event reduction, etc.) and debug performance.
GET /version[Deprecated] Use /status instead
Deprecated: Version is included in /status response.
GET /docs[Deprecated] Use /api instead
Deprecated: Use /api for complete API documentation.
GET /apiFull API reference
Complete API documentation with all endpoints.
Returns structured JSON with all endpoints, their parameters, and examples.
POST /treeGet DOM tree structure
Returns hierarchical view of page elements. Best for understanding page structure before interacting.
Response structure: { tag, id?, classes?, attrs?, text?, value?, checked?, children?, flags?: { interactive, hidden, hasAria, … } }
Flags help identify interactive elements (buttons, inputs) and hidden content. Form inputs include live value/checked state (not just HTML attribute).
Use ancestors:true to see parent elements when inspecting deep elements.
Parameters:
| Name | Type | Description |
|---|---|---|
selector |
string,null | Root element selector |
depth |
number,null | Max depth (-1 = unlimited). Default: unlimited |
includeText |
boolean,null | Include text content (default true) |
visibleOnly |
boolean,null | Only visible elements (default false) |
interactiveOnly |
boolean,null | Only interactive elements and their ancestors (default false) |
pierceShadow |
boolean,null | Pierce shadow DOM (default true) |
pierceFrames |
boolean,null | Pierce same-origin iframes (default true) |
compact |
boolean,null | Minimal output (default false) |
ancestors |
boolean,null | Include ancestor path from root (default false) |
includeBox |
boolean,null | Include each node’s bounding box { x, y, w, h, visible } (default false) |
mode |
string,null | ‘actionable’ returns a page action summary (url, title, headings, buttons, links, inputs, selects) instead of the DOM tree |
window |
string,null | Target window ID |
Examples:
{"depth":3}
{"selector":"form"}
{"interactiveOnly":true}
{"selector":"#deep-element","ancestors":true}
POST /queryQuery DOM elements by selector
Quick element lookup. Returns basic info: tagName, id, className, textContent, attributes.
Use this to check if an element exists before clicking/typing. For detailed info, use /inspect instead.
Response: { tagName, id, className, textContent, attributes: {…} }
Parameters:
| Name | Type | Description |
|---|---|---|
ref |
string,null | Ref ID from /tree output (e.g., 1, 42) - preferred for efficiency |
selector |
string,null | CSS selector |
all |
boolean,null | Return all matches (default false = first only) |
Examples:
{"ref":"42"}
{"selector":"#submit-btn"}
Response:
{
"success": true,
"data": {
"tagName": "button",
"id": "submit-btn",
"className": "btn primary",
"textContent": "Submit",
"attributes": {
"id": "submit-btn",
"class": "btn primary",
"type": "submit"
}
}
}
{"selector":"input[type=\"text\"]","all":true}
POST /inspectDeep inspection of an element
Get everything about ONE element: geometry, computed styles, ARIA attributes, scroll position, visibility state.
Response includes:
Use before clicking to verify element is visible and enabled.
Parameters:
| Name | Type | Description |
|---|---|---|
ref |
string,null | Ref ID from /tree output (e.g., 1, 42) - preferred for efficiency |
selector |
string,null | CSS selector |
fullStyles |
boolean,null | Include all computed styles (default: false) |
matchedRules |
boolean,null | Include matched CSS rules with specificity (default: false) |
window |
string,null | Target window ID |
Examples:
{"ref":"42"}
{"selector":"#submit"}
Response:
{
"success": true,
"data": {
"selector": "body > form > button#submit",
"tagName": "button",
"classList": [
"btn",
"primary"
],
"box": {
"x": 100,
"y": 200,
"width": 120,
"height": 40,
"visible": true,
"display": "inline-block",
"visibility": "visible",
"opacity": 1
},
"text": {
"innerText": "Submit",
"textContent": "Submit",
"innerHTML": "Submit"
},
"attributes": {
"id": "submit",
"class": "btn primary",
"type": "submit"
},
"properties": {
"disabled": false,
"hidden": false,
"type": "submit"
},
"hierarchy": {
"parent": "form#login",
"children": 0,
"depth": 4
},
"styles": {
"display": "inline-block",
"visibility": "visible",
"opacity": "1"
}
}
}
{"selector":"input[name=\"email\"]"}
POST /inspectAllInspect multiple elements
Deep inspection of ALL elements matching selector (up to limit).
Same detailed info as /inspect, but for multiple elements. Great for:
Response: array of inspection objects
Parameters:
| Name | Type | Description |
|---|---|---|
ref |
string,null | Ref ID from /tree output - returns single element as array |
selector |
string,null | CSS selector |
limit |
number,null | Max elements (default 10) |
fullStyles |
boolean,null | Include all computed styles (default: false) |
matchedRules |
boolean,null | Include matched CSS rules with specificity (default: false) |
window |
string,null | Target window ID |
Examples:
{"selector":"button, [role=\"button\"]","limit":20}
{"selector":"input, select, textarea"}
{"selector":"nav a","limit":15}
POST /findFind elements by text content
Search for elements containing specific text. Saves writing querySelector + filter patterns.
Returns first match by default, or all matches with all:true.
Response: { found: true, selector: “…”, element: {…} } or { found: true, elements: […] }
Parameters:
| Name | Type | Description |
|---|---|---|
text |
string | Text to search for (substring match) (required) |
tag |
string,null | Limit to specific tag (button, a, div, etc) |
exact |
boolean,null | Require exact text match (default false = substring) |
all |
boolean,null | Return all matches (default false = first only) |
visible |
boolean,null | Only visible elements (default true) |
window |
string,null | Target window ID |
Examples:
{"text":"Submit","tag":"button"}
{"text":"Learn more","tag":"a"}
{"text":"OK","tag":"button","exact":true}
{"text":"Delete","tag":"button","all":true}
{"text":"Error:"}
POST /formExtract all form values as structured JSON
Get all form field values without needing to know the component’s API.
Introspects forms and returns structured data:
Response: { fields: { name: value, … }, form: { action, method, id } }
Works with standard forms and most framework components (React, Vue, etc).
Parameters:
| Name | Type | Description |
|---|---|---|
selector |
string,null | Form selector (default: first form on page) |
includeDisabled |
boolean,null | Include disabled fields (default false) |
includeHidden |
boolean,null | Include hidden fields (default false) |
window |
string,null | Target window ID |
Examples:
{}
{"selector":"#login-form"}
{"selector":"form","includeHidden":true}
POST /mapAffordance map — what can be interacted with, and what it is wired to
Returns a map of the page’s affordances. Two tiers, and the difference matters:
Native (source: "tosi-agent") — EXPERIMENTAL. haltija detects this tier by duck-typing one
method and consumes a shape tosijs has not committed to (no version/capability marker yet — see
tosijs#23). When the shape isn’t what haltija expects it returns a warning and agentSurfaceVersion
rather than passing a blank map off as a success, but treat the tier as best-effort. When the page
exposes an agent surface at
globalThis.tosiAgent (a tosijs app calling enableAgentInterface()), the map is the app’s OWN
wiring records. That carries what the DOM cannot: which state path each control is bound to and in
which direction — ⟷ two-way (user-writable), ⟵ bound-to-DOM (display only), absent
(static) — plus the handler path each event calls, and the list of callable actions.
With that you can act through paths instead of synthesized input:
hj eval "tosiAgent.write('app.filter', 'milk')" or tosiAgent.call('app.addItem').
Fallback (source: "dom") — any other page, reconstructed from tags/roles/labels/state, each
node carrying a haltija ref for hj click <ref>. Deliberately approximate: it has NO binding
provenance, because that information does not exist in the DOM. Always check source before
trusting the map as wiring rather than as a guess.
Cheaper and more stable than a screenshot for deciding what to do next: no fonts, themes, viewport or animation timing, and structure (nesting) is carried for free.
A node may carry smallTarget — an interactive control below WCAG 2.5.8’s 24x24 CSS px minimum
(44x44 is the AAA / Apple / Material recommendation), reported as e.g.
"16x16 (WCAG 2.5.8 needs 24x24; 44x44 recommended)" and drawn as an amber bar along the bottom
edge of the schematic. Inline links inside a sentence are exempt, per the spec — they are sized by
their text, and flagging them would fire on every paragraph.
With image and file (both default true), the response carries legendPath: a sibling
*.legend.json written beside the image, holding a flat ref -> facts index of the ref-bearing
nodes. The image says WHERE and WHICH, the legend says WHAT — which is what makes it safe to stop
cramming captions into boxes too small to hold them. Read the number off the picture, look it up
there.
A node may carry zeroSize: true — a real, operable control that occupies no box. The standard
accessible pattern for file inputs and custom checkboxes is a 0x0 <input> driven by a <label>,
so its coordinates are meaningless and clicking it directly may do nothing: click the associated
<label> instead. Genuinely hidden elements (display:none, hidden ancestor) are excluded
entirely, so zeroSize always means operable but invisible, never not there.
Where the walk STOPS — an element with no children may not be empty. A childless node in
nodes reads exactly like a genuinely empty element, and the two are worth telling apart:
attachShadow({mode:'closed'})) is invisible to any script, so its host
is reported childless and nothing can change that.<iframe> has no element children, so it is always a
leaf in the map. /tree pierces same-origin frames (pierceFrames, default true). Where the
frame has its own widget it also registers as a separate window: see /windows for a
windowType: "iframe" entry and target it with window.<canvas>, <video>, images. There is nothing inside to report.maxNodes truncation (default 400) sets truncated: true alongside nodes (so on the
map itself — top level for JSON, under map with --image); subtrees past the cap are simply
absent. Check that flag before reading a short map as a small page.So: when a node looks empty and you expected content, reach for hj tree --shadow --frames (which
pierces more) or hj map --image (which shows you the box that is there).
Parameters:
| Name | Type | Description |
|---|---|---|
global |
string,null | Global to probe for the agent surface (default ‘tosiAgent’) |
maxNodes |
number,null | Cap on DOM-fallback nodes (default 400) |
image |
boolean,null | Also render the map as a schematic PNG (rasterized). NOTE: response.nodes is a TREE (recurse into .children — a <ul> holds its <li>s, a custom element holds its shadow content) while response.legend is a FLAT ref->facts index of only the ref-bearing nodes, so the two legitimately contain different sets. Vision cost scales with PIXELS, roughly (w*h)/750 for Claude — there is NO fixed floor, so a small or size-capped schematic can be very cheap (a 491x480 one is ~314 tokens; with maxWidth 200, ~52). ~1600 is the practical ceiling, since larger images are downscaled before tokenisation. response.cost reports approxJsonTokens and approxImageTokens for THIS page — compare those rather than assuming. |
scale |
number,null | Device-pixel scale for the schematic image (default 1). Raise it to make the captions legible to a vision model on a dense page. |
layout |
string,null | Schematic layout: ‘auto’ (default), ‘geometric’ (boxes at true page coordinates) or ‘structural’ (nested stacked boxes). ‘auto’ picks geometric when most nodes carry bounds — which they do for a DOM map, but never for the tosi-agent tier, which describes wiring and has no geometry. The response reports which was used in layout and the evidence in boundsCoverage. |
fullPage |
boolean,null | Draw the WHOLE document rather than just the viewport (default false). The schematic is laid out at real page coordinates, so a long page becomes a tall thin strip — 1126x22304 is a 1:20 ratio that no downscaling makes readable. Ask for it only when you need the parts that are off screen. |
maxWidth |
number,null | Max width in pixels for the schematic image (aspect ratio preserved) |
maxHeight |
number,null | Max height in pixels for the schematic image (aspect ratio preserved) |
format |
string,null | Schematic image format: png (default), webp, or jpeg |
quality |
number,null | Quality for lossy formats (webp/jpeg). Either scale works: 0-1 (canvas-native) or 0-100 (percentage); anything above 1 is read as a percentage and clamped. Ignored for png. |
file |
boolean,null | With image: save the PNG under |
window |
string,null | Target window ID |
Examples:
{}
POST /clickClick an element
Scrolls element into view, then performs full click sequence: mouseenter, mouseover, mousedown, mouseup, click.
Three ways to target elements:
Options:
Automatically fails if element is not found or is disabled. Check response.success to verify.
Parameters:
| Name | Type | Description |
|---|---|---|
ref |
string,null | Ref ID from /tree output (e.g., 1, 42) - preferred for efficiency |
selector |
string,null | CSS selector of element to click |
text |
string,null | Text content to find (alternative to selector) |
tag |
string,null | Tag name when using text (default: any clickable element) |
autoWait |
boolean,null | Wait for element to appear before clicking (default false) |
timeout |
number,null | Max wait time in ms when autoWait is true (default 5000) |
diff |
boolean,null | Return DOM diff showing what changed after click (default false) |
diffDelay |
number,null | Wait ms before capturing “after” state (default 100) |
window |
string,null | Target window ID |
Examples:
{"ref":"42"}
{"selector":"#submit"}
{"selector":".btn-primary"}
{"text":"Save"}
{"text":"Submit","tag":"button"}
{"text":"Learn more","tag":"a"}
{"selector":"[role=\"button\"][aria-label=\"Close\"]"}
{"selector":".add-item","diff":true}
{"selector":".modal-button","autoWait":true}
{"selector":".slow-element","autoWait":true,"timeout":10000}
POST /typeType text into an element
Focus element and type text character by character with realistic event lifecycle.
Target element via ref (from /tree output) or selector.
Simulates real user behavior:
Handles native inputs, textareas, contenteditable, and framework-wrapped inputs (React, MUI, etc).
Options:
Parameters:
| Name | Type | Description |
|---|---|---|
ref |
string,null | Ref ID from /tree output (e.g., 1, 42) - preferred for efficiency |
selector |
string,null | CSS selector of input/textarea/contenteditable |
text |
string | Text to type (required) |
autoWait |
boolean,null | Wait for element to appear before typing (default false) |
timeout |
number,null | Max wait time in ms when autoWait is true (default 5000) |
humanlike |
boolean,null | Human-like delays and typos (default true) |
focusMode |
string,null | How to focus: mouse (default), keyboard, or direct |
clear |
boolean,null | Clear existing content before typing (default false) |
blur |
boolean,null | Blur element after typing to trigger change event (default true) |
typoRate |
number,null | Typo probability 0-1 (default 0.03) |
minDelay |
number,null | Min ms between keys (default 50) |
maxDelay |
number,null | Max ms between keys (default 150) |
diff |
boolean,null | Return DOM diff showing what changed after typing (default false) |
diffDelay |
number,null | Wait ms before capturing “after” state (default 100) |
window |
string,null | Target window ID |
Examples:
{"selector":"#email","text":"user@example.com"}
{"selector":"input[type=\"password\"]","text":"secret123"}
{"selector":"input","text":"hello","humanlike":false,"focusMode":"direct"}
{"selector":"#search","text":"query","focusMode":"keyboard"}
{"selector":"#name","text":"New Name","clear":true}
{"selector":"[contenteditable]","text":"Hello world"}
POST /keySend keyboard input
Send key press with full event lifecycle: keydown → keypress → beforeinput → input → keyup.
Target element defaults to document.activeElement. Use ref or selector to focus a specific element first.
Supports modifiers (ctrlKey, shiftKey, altKey, metaKey) and repeat count for holding keys.
Common keys: Enter, Escape, Tab, ArrowUp/Down/Left/Right, Backspace, Delete, Home, End, PageUp/PageDown, F1-F12, or any printable character.
Parameters:
| Name | Type | Description |
|---|---|---|
key |
string | Key to press (e.g., “Enter”, “Escape”, “a”, “ArrowDown”) (required) |
ref |
string,null | Ref ID from /tree output (e.g., 1, 42) - preferred for efficiency |
selector |
string,null | Element to focus first (default: activeElement) |
ctrlKey |
boolean,null | Hold Ctrl/Control |
shiftKey |
boolean,null | Hold Shift |
altKey |
boolean,null | Hold Alt/Option |
metaKey |
boolean,null | Hold Meta/Command |
repeat |
number,null | Repeat count for key hold (default 1) |
window |
string,null | Target window ID |
Examples:
{"key":"Escape"}
{"key":"Enter"}
{"key":"s","ctrlKey":true}
{"key":"Tab","selector":"#first-input"}
{"key":"Tab","shiftKey":true}
{"key":"ArrowDown","repeat":3}
{"key":"a","ctrlKey":true,"selector":"#editor"}
POST /dragDrag from an element
Simulates drag gesture: mousedown on element, mousemove by delta, mouseup.
Good for: sliders, resize handles, drag-and-drop reordering, range inputs.
Parameters:
| Name | Type | Description |
|---|---|---|
ref |
string,null | Ref ID from /tree output (e.g., 1, 42) - preferred for efficiency |
selector |
string,null | CSS selector of drag handle |
deltaX |
number,null | Horizontal distance in pixels |
deltaY |
number,null | Vertical distance in pixels |
duration |
number,null | Drag duration in ms (default 300) |
window |
string,null | Target window ID |
Examples:
{"ref":"15","deltaX":100}
{"selector":".slider-handle","deltaX":100}
{"selector":".resize-handle","deltaX":50,"deltaY":50}
{"selector":".drag-item","deltaY":80}
POST /highlightVisually highlight an element
Draw attention to an element with colored border and optional label.
Great for showing users what you found or pointing out issues. Use /unhighlight to remove.
Parameters:
| Name | Type | Description |
|---|---|---|
ref |
string,null | Ref ID from /tree output (e.g., 1, 42) - preferred for efficiency |
selector |
string,null | CSS selector |
label |
string,null | Label text to show |
color |
string,null | CSS color (default #6366f1) |
duration |
number,null | Auto-hide after ms (omit for manual) |
window |
string,null | Target window ID |
Examples:
{"ref":"42","label":"Found it!"}
{"selector":"#login-btn","label":"Click here"}
{"selector":".error","label":"Problem","color":"#ef4444"}
{"selector":"button","duration":3000}
POST /unhighlightRemove highlight
Remove any active highlight overlay created by /highlight.
POST /scrollScroll to element or position
Smooth scroll with natural easing. Multiple modes:
At least one of ref, selector, x, y, deltaX, or deltaY must be provided.
Parameters:
| Name | Type | Description |
|---|---|---|
ref |
string,null | Ref ID from /tree output (e.g., 1, 42) - preferred for efficiency |
selector |
string,null | CSS selector to scroll into view |
x |
number,null | Absolute X position in pixels |
y |
number,null | Absolute Y position in pixels |
deltaX |
number,null | Relative horizontal scroll in pixels |
deltaY |
number,null | Relative vertical scroll in pixels |
duration |
number,null | Animation duration in ms (default 500) |
easing |
string,null | Easing function: ease-out (default), ease-in-out, linear |
block |
string,null | Vertical alignment: center (default), start, end, nearest |
window |
string,null | Target window ID |
Examples:
{"ref":"42"}
{"selector":"#pricing"}
{"y":0}
{"selector":"footer"}
{"deltaY":500}
{"selector":"#section","duration":1000,"easing":"ease-in-out"}
POST /waitWait for time, element, or condition
Flexible wait for async UI scenarios. Multiple modes:
All modes support timeout (default 5000ms). Returns immediately if condition already met.
Passing NEITHER ms nor forElement/selector is an error (HTTP 400), not a no-op success —
a wait that reports success without waiting makes every assertion after it race the page.
Response: { success: true, waited: ms, found?: boolean }
Parameters:
| Name | Type | Description |
|---|---|---|
ms |
number,null | Milliseconds to wait |
forElement |
string,null | CSS selector to wait for |
selector |
string,null | Alias for forElement — accepted so the CLI, the test-runner wait step and this endpoint all take the same field |
hidden |
boolean,null | Wait for element to disappear (default false) |
timeout |
number,null | Max wait time in ms (default 5000) |
pollInterval |
number,null | Polling interval in ms (default 100) |
window |
string,null | Target window ID |
Examples:
{"ms":500}
{"forElement":".modal"}
{"forElement":".loading","hidden":true}
{"forElement":"button[data-ready]","timeout":10000}
{"forElement":".dropdown","ms":100}
POST /callCall a method or get a property on an element
Call a method or access a property on an element by selector. Convenience wrapper around /eval.
This avoids writing querySelector boilerplate. Two modes:
Return value is JSON-serialized. Promises are awaited.
Response: { success: true, data:
Parameters:
| Name | Type | Description |
|---|---|---|
ref |
string,null | Ref ID from /tree output (e.g., 1, 42) - preferred for efficiency |
selector |
string,null | CSS selector of the element |
method |
string | Method name to call or property name to get (required) |
args |
array,null | Arguments to pass (omit to get property value) |
window |
string,null | Target window ID |
Examples:
{"ref":"42","method":"value"}
{"selector":"#email","method":"value"}
{"selector":"#agree","method":"checked"}
{"selector":"#content","method":"innerHTML"}
{"selector":"#item","method":"dataset"}
{"selector":"dialog","method":"open"}
{"selector":"#my-popover","method":"showPopover","args":[]}
{"selector":"#my-popover","method":"hidePopover","args":[]}
{"selector":"video","method":"play","args":[]}
{"selector":"#email","method":"focus","args":[]}
{"selector":"#section","method":"scrollIntoView","args":[{"behavior":"smooth"}]}
{"selector":"#btn","method":"setAttribute","args":["disabled","true"]}
{"selector":"#box","method":"getBoundingClientRect","args":[]}
POST /navigateNavigate to a URL
Navigate the browser to a new URL. Waits for page load to complete.
Use /location after to verify navigation succeeded.
Parameters:
| Name | Type | Description |
|---|---|---|
url |
string | URL to navigate to (required) |
window |
string,null | Target window ID |
Examples:
{"url":"https://example.com/login"}
{"url":"/dashboard"}
POST /refreshRefresh the page
Hard reload the current page, bypassing all caches (CSS, JS, images). Use soft: true for cache-friendly reload.
Parameters:
| Name | Type | Description |
|---|---|---|
soft |
boolean,null | Use cached resources if available (default false = hard refresh that busts all caches) |
window |
string,null | Target window ID |
Examples:
{}
{"soft":true}
GET /locationGet current URL and title
Returns current page info.
Response: { url, title, pathname, search, hash }
Use after /navigate to verify you’re on the expected page.
POST /events/watchStart watching semantic events
Begin capturing high-level user actions. Events are aggregated and meaningful:
Presets control verbosity:
Categories: interaction, navigation, input, hover, scroll, mutation, focus, console
Parameters:
| Name | Type | Description |
|---|---|---|
preset |
string,null | Verbosity: minimal, interactive, detailed, debug |
categories |
array,null | Specific categories to watch |
Examples:
{"preset":"interactive"}
{"preset":"minimal"}
{"categories":["interaction","input","console"]}
POST /events/unwatchStop watching events
Stop capturing semantic events. Events buffer is cleared.
GET /eventsGet captured semantic events
Returns buffered events since watch started.
Response: { events: [{ type, timestamp, category, target?, payload }], since, count }
Event types: interaction:click, input:typed, navigation:navigate, hover:dwell, scroll:stop, etc.
GET /events/statsGet event aggregation statistics
Shows noise reduction metrics.
Response: { rawEvents, semanticEvents, reductionPercent, byCategory: {…} }
Typically see 90%+ reduction (e.g., 2000 raw events → 80 semantic events).
POST /mutations/watchStart watching DOM mutations
Begin capturing DOM changes: elements added/removed, attributes changed, text modified.
Presets filter out framework noise:
Get captured mutations via /mutations/status.
Parameters:
| Name | Type | Description |
|---|---|---|
root |
string,null | Root selector to watch (default body) |
childList |
boolean,null | Watch child additions/removals (default true) |
attributes |
boolean,null | Watch attribute changes (default true) |
characterData |
boolean,null | Watch text content changes (default false) |
subtree |
boolean,null | Watch all descendants (default true) |
debounce |
number,null | Debounce ms (default 100) |
preset |
string,null | Filter preset: smart, xinjs, b8rjs, tailwind, react, minimal, none |
filters |
any | Custom filter configuration |
pierceShadow |
boolean,null | Watch inside shadow DOM (default false) |
Examples:
{}
{"root":"form","preset":"minimal"}
{"preset":"react"}
POST /mutations/unwatchStop watching mutations
Stop capturing DOM mutations. Call this when done to free resources.
GET /mutations/statusGet mutation watch status
Check if mutation watching is active and get captured mutations.
Response: { watching: boolean, mutations: […], summary: { added, removed, changed } }
POST /selectInteractive element selection
Let user point at elements on the page instead of writing selectors.
Actions:
Workflow: start → (user draws region) → result → use returned selectors
Parameters:
| Name | Type | Description |
|---|---|---|
action |
string,null | Selection action to perform (default: result) |
window |
string,null | Target window ID |
Examples:
{"action":"start"}
{"action":"result"}
{"action":"status"}
POST /select/start[Deprecated] Use /select with action:”start”
Deprecated: Use POST /select {“action”:”start”} instead.
POST /select/cancel[Deprecated] Use /select with action:”cancel”
Deprecated: Use POST /select {“action”:”cancel”} instead.
GET /select/status[Deprecated] Use /select with action:”status”
Deprecated: Use POST /select {“action”:”status”} instead.
GET /select/result[Deprecated] Use /select with action:”result”
Deprecated: Use POST /select {“action”:”result”} instead.
POST /select/clear[Deprecated] Use /select with action:”clear”
Deprecated: Use POST /select {“action”:”clear”} instead.
GET /windowsList connected windows
Returns all connected browser windows/tabs with IDs, URLs, and titles.
Response: { windows: [{ id, url, title, focused, active, hidden, windowType }], tabs, count, ready, hint }
The array is windows, not tabs — it includes popups (windowType: "popup") and
iframes ("iframe"), not only tabs. Because the CLI command is hj tabs, d['tabs'] is the
natural first guess and used to KeyError, so tabs is also sent as an alias of the same
array. Prefer windows; tabs exists so the obvious guess works.
Only real tabs can be the target of an UNTARGETED command — a popup or iframe never becomes
focused, so a page opening a popup cannot silently re-route your commands.
active and hidden are exact inverses, and BOTH are sent on purpose: /status historically
emitted only hidden and /windows only active, so code moving between the two silently
inverted its own meaning. Read either; never infer one endpoint’s polarity from the other.
Use window IDs in other endpoints (e.g., /click, /tree) to target specific tabs.
ready is the signal to gate a test lane on, not “is the server up”. A server can be running
with zero connected tabs — it answers /status 200 but there is nothing to drive, and a lane that
adopts it fails later on a confusing timeout. ready is true when at least one top-level tab is
connected. (Hidden tabs count as ready — they’re reachable, just possibly stale; see the hidden-tab
warning.) hj doctor checks this and exits non-zero, so a lane can fail fast on the real cause.
POST /tabs/openOpen a new tab
Opens a new tab with optional URL. If url is omitted, opens a blank tab.
In the Haltija desktop app the new tab gets the widget auto-injected, so it’s immediately
controllable. Anywhere else (widget injected into a normal browser via bookmarklet/dev-server)
there is no tab API, so this falls back to window.open() — the new tab is a plain browser tab
with NO widget unless its own page injects one. In that case the response has fallback: true
plus a reason, and a top-level warning: the tab will NOT appear in hj tabs and hj commands
cannot reach it (they go to the focused widget tab). To control it, inject the widget on that page
(e.g. HALTIJA_DEV / haltijaDev) or use the desktop app.
Parameters:
| Name | Type | Description |
|---|---|---|
url |
string,null | URL to open |
Examples:
{}
{"url":"https://example.com"}
POST /tabs/closeClose a tab
Desktop app only. Closes the specified tab by window ID.
Get window IDs from /windows endpoint.
Parameters:
| Name | Type | Description |
|---|---|---|
window |
string | Window ID to close (required) |
Examples:
{"window":"window-abc123"}
POST /tabs/focusFocus a tab (route untargeted commands to it)
Make the given tab the target of untargeted commands. This is a server-side routing change, not a browser action: because it never dispatches to the target tab it cannot time out, even when that tab is hidden.
In the Haltija desktop app it also physically raises the tab, best-effort: the request goes to a
tab that is awake, which asks the app’s main process to bring the target forward — so a sleeping tab
gets raised without having to run anything itself. Outside the desktop app a background browser tab
cannot be raised remotely, so focus is routing-only there. After this, commands without a window/--window go to this tab until you
focus another or physically switch tabs in the browser. To pin a single command instead, use
--window <id>. If the tab is hidden the response includes a warning (a backgrounded tab’s results
can be stale). Returns { success, focused, active, title }.
Parameters:
| Name | Type | Description |
|---|---|---|
window |
string | Window ID to focus (required) |
Examples:
{"window":"window-abc123"}
POST /recordingRecord user actions and generate tests
Record user interactions and convert them to runnable tests.
Cross-page recording: Recordings now survive page navigations! The server tracks the recording session by window ID, so you can record a multi-page flow (e.g., login → dashboard).
Actions:
Workflow: start → (user interacts, navigates pages) → stop → list → replay
Parameters:
| Name | Type | Description |
|---|---|---|
action |
string | Recording action to perform (required) |
name |
string,null | Test name (for generate action) |
id |
any | Recording ID or index number (for replay action) |
window |
string,null | Target window ID |
Examples:
{"action":"start"}
{"action":"stop"}
{"action":"status"}
{"action":"generate","name":"Login flow"}
{"action":"list"}
{"action":"replay","id":"0"}
{"action":"replay","id":"rec_123456_abc"}
POST /recording/start[Deprecated] Use /recording with action:”start”
Deprecated: Use POST /recording {“action”:”start”} instead.
Parameters:
| Name | Type | Description |
|---|---|---|
name |
string,null | Recording/test name |
POST /recording/stop[Deprecated] Use /recording with action:”stop”
Deprecated: Use POST /recording {“action”:”stop”} instead.
POST /recording/generate[Deprecated] Use /recording with action:”generate”
Deprecated: Use POST /recording {“action”:”generate”} instead.
Parameters:
| Name | Type | Description |
|---|---|---|
name |
string,null | Test name |
events |
array,null | Semantic events to convert. Omit to use the server-side event buffer (see since). |
since |
number,null | When reading from the event buffer, only events after this timestamp |
url |
string,null | Starting URL for the generated test (defaults to the first navigation event) |
description |
string,null | Test description |
tags |
array,null | Tags to attach to the generated test |
createdBy |
string,null | Author recorded in the test (‘human’ by default) |
addAssertions |
boolean,null | Infer assertions from the events (default true) |
suggestAssertions |
boolean,null | Append suggested assertions at the end (default false) |
minDelay |
number,null | Drop inter-step delays shorter than this many ms |
GET /recordings[Deprecated] Use /recording with action:”list”
Deprecated: Use POST /recording {“action”:”list”} instead.
POST /test/runRun a JSON test
Execute a test defined in Haltija JSON format.
Test structure: { “version”: 1, “name”: “Login flow”, “url”: “http://localhost:3000/login”, “steps”: [ { “action”: “type”, “selector”: “#email”, “text”: “user@example.com” }, { “action”: “type”, “selector”: “#password”, “text”: “secret123” }, { “action”: “click”, “selector”: “button[type=submit]” }, { “action”: “assert”, “assertion”: { “type”: “url”, “pattern”: “/dashboard” } } ] }
Step actions: navigate, click, type, check, key, select-text, cut, copy, paste, drag, wait, assert, eval, verify, tabs-open, tabs-close, tabs-focus
Output formats:
Parameters:
| Name | Type | Description |
|---|---|---|
test |
any | Test object with steps (required) |
format |
string,null | Output format: json (structured), github (annotations + summary), human (readable) |
stepDelay |
number,null | Milliseconds between steps (default 100) |
timeout |
number,null | Milliseconds timeout per step (default 5000) |
stopOnFailure |
boolean,null | Stop on first failure (default true) |
patience |
number,null | Total failures allowed before giving up (0 = use stopOnFailure, default 0) |
patienceStreak |
number,null | Consecutive failures to bail immediately (default 2) |
timeoutBonusMs |
number,null | Ms added/removed from timeout on success/failure, capped at initial (default 1000) |
Examples:
{"test":{"version":1,"name":"Click button","url":"http://localhost:3000","steps":[{"action":"click","selector":"#submit"},{"action":"assert","assertion":{"type":"exists","selector":".success"}}]}}
{"test":{"version":1,"name":"Test","url":"http://localhost:3000","steps":[]},"format":"github"}
POST /test/suiteRun multiple tests
Execute a suite of tests, optionally stopping on first failure.
Input: { tests: [test1, test2, …], format?, stopOnFailure? }
Response includes per-test results and overall summary.
Parameters:
| Name | Type | Description |
|---|---|---|
tests |
array | Array of test objects (required) |
format |
string,null | Output format: json (structured), github (annotations + summary), human (readable) |
testDelay |
number,null | Milliseconds between tests (default 500) |
stepDelay |
number,null | Milliseconds between steps (default 100) |
timeout |
number,null | Milliseconds timeout per step (default 5000) |
stopOnFailure |
boolean,null | Stop on first failure (default false for suites) |
patience |
number,null | Total failures allowed per test before giving up (0 = use stopOnFailure, default 0) |
patienceStreak |
number,null | Consecutive failures to bail immediately (default 2) |
timeoutBonusMs |
number,null | Ms added/removed from timeout on success/failure, capped at initial (default 1000) |
Examples:
{"tests":[{"version":1,"name":"Login","url":"http://localhost:3000/login","steps":[]},{"version":1,"name":"Dashboard","url":"http://localhost:3000/dashboard","steps":[]}],"stopOnFailure":false}
POST /test/validateValidate test without running
Check that a test is well-formed and all selectors exist on the current page.
Use this to pre-check tests before running. Returns validation errors without executing steps.
Response: { valid: boolean, errors?: [{ step?, message }] }
Parameters:
| Name | Type | Description |
|---|---|---|
test |
any | Test object to validate (required) |
Examples:
{"test":{"version":1,"name":"Test","url":"http://localhost:3000","steps":[{"action":"click","selector":"#btn"}]}}
POST /session/attachMirror a tmux session into the channel (opt-in, read-only)
Point the session mirror at a running tmux session, so a page — including one on a remote device over a tunnel — can watch the agent work.
Opt-in on purpose. Nothing is mirrored until you attach: the mirror carries EVERYTHING the agent prints, including anything it echoes from a file it just read. There is no default target and no config flag that turns this on.
Writing is a SEPARATE grant. Attaching without allowInput gives a read-only mirror that
/session/write will refuse. Typing into an agent’s console can answer a permission prompt —
indistinguishable from the operator doing it — so it is never implied by the ability to watch.
Requires a token. This endpoint family refuses to run unless the server was started with
--token: it exposes a terminal running an agent with your privileges, and the server binds beyond
loopback by default.
Attaching to a name that does not exist FAILS and lists what does, rather than reporting success for a mirror that would show nothing.
Requires the agent to be running inside tmux (tmux new -s agent, then start it there). tmux can
attach to a session that is already running, so you do not have to restart your agent.
Parameters:
| Name | Type | Description |
|---|---|---|
target |
string | tmux session name, exactly as tmux list-sessions reports it (required) |
allowInput |
boolean,null | ALSO permit typing into the session (default false). A separate grant from reading: typing can answer a permission prompt. |
Examples:
{"target":"agent"}
{"target":"agent","allowInput":true}
POST /session/readRead the mirrored terminal session
Returns the rendered contents of the mirrored tmux pane as plain text.
Already rendered — capture-pane -p resolves the escape sequences, so this is displayable text
rather than a terminal stream. A page can put it in a <pre> without a terminal emulator, which
is what makes it usable on a headset.
Returns an error naming the target if the session has gone away, rather than empty text — an empty mirror reads as “the agent is idle”, which would be the wrong conclusion.
Parameters:
| Name | Type | Description |
|---|---|---|
lines |
number,null | How many lines of scrollback to include (default 200, max 10000) |
Examples:
{}
POST /session/writeType into the mirrored session (requires the input grant)
Sends text to the mirrored tmux session exactly as a local user at the keyboard would.
The agent receives it on stdin as a normal turn — there is no queue, no message API and no await primitive, because typing into the agent’s own console is already all three.
Requires the writeKey returned by attach. The handle is minted when allowInput is granted
and returned once, to the attaching caller. Without it, reaching this endpoint would equal authority
to type into the agent — so a second caller could ride a grant it never requested.
Requires allowInput at attach time. Reading and writing are different risks: reading leaks
what the agent prints, writing decides what the agent DOES — including answering a permission prompt,
where this is indistinguishable from the operator typing. A mirror attached for watching cannot be
typed into.
submit defaults to true (hits Enter), matching /send/message. Pass false to paste without
submitting — useful when a human at the real keyboard should read it before it goes in.
Sent as one send-keys call, not per character: two writers on one pty interleave badly, and
line-at-a-time is correct for this.
Parameters:
| Name | Type | Description |
|---|---|---|
text |
string | What to type (required) |
submit |
boolean,null | Press Enter afterwards (default true) |
writeKey |
string,null | The handle returned by /session/attach when allowInput was granted. Required: reaching this endpoint is not the same as holding the capability. |
Examples:
{"text":"the login button is off-centre in VR"}
POST /session/detachStop mirroring the terminal session
Detach the mirror. Reads then report “no session attached” until you attach again.
Examples:
{}
GET /consoleGet console output
Returns captured console.log/warn/error/info/debug from the page — AND uncaught exceptions and unhandled promise rejections (as level ‘error’), which never route through console.error. Error objects keep their message and stack (a plain console.error(err) used to serialize to {}).
Response: { entries: [{ level, args, timestamp, stack? }] }
Great for debugging — check for errors after actions fail. Note: capture starts when the widget is injected, so errors thrown BEFORE injection are missed for bookmarklet/dev-server injection; the desktop app injects at document-start and catches them.
POST /evalExecute JavaScript
Run arbitrary JavaScript in the browser context. Returns the result.
The code runs in the page’s context with access to window, document, etc. Return values are JSON-serialized.
Async code is supported: a returned Promise is awaited, and top-level await
works. Multi-statement code needs an explicit return to produce a value.
document.title -> the title await fetch(‘/api’).then(r => r.json()) -> the parsed body const r = await fetch(‘/api’); return r.status -> the status code
Parameters:
| Name | Type | Description |
|---|---|---|
code |
string | JavaScript code to execute (required) |
window |
string,null | Target window ID |
Examples:
{"code":"document.title"}
{"code":"document.querySelectorAll(\".item\").length"}
{"code":"document.querySelector(\"#email\").value"}
{"code":"window.localStorage.getItem(\"token\") !== null"}
{"code":"({ x: window.scrollX, y: window.scrollY })"}
POST /fetchFetch a URL from within the tab context
Fetch a URL from within the browser tab’s context. Essential for accessing blob: URLs which are only valid in the tab that created them.
Returns the content as base64 with MIME type. Works with:
Response: { success: true, data: { mimeType, base64, size, url } }
Use this when you see a blob URL in the DOM and need to access its content.
Parameters:
| Name | Type | Description |
|---|---|---|
url |
string | The URL to fetch (blob:, data:, http:, https:) (required) |
window |
string,null | Target window ID |
Examples:
{"url":"blob:https://example.com/abc-123-def"}
{"url":"https://example.com/image.png"}
POST /screenshotCapture a screenshot
Capture the page or a specific element as PNG/WebP/JPEG.
Works automatically in the Haltija Desktop app. In browser widget mode, captures viewport only.
**canvas — capture a
Caveat it handles for you: a WebGL context clears its drawing buffer after compositing unless
created with { preserveDrawingBuffer: true }, so a naive toDataURL can silently return a BLANK
image. Haltija samples the result and returns a warning explaining that (rather than handing you
an empty picture). A canvas tainted by cross-origin content returns a clear error, not a crash.
When file=true (default from CLI), saves to
displaySurface — what the user ACTUALLY shared. On the source: "getDisplayMedia" path (the
🖥-button grant), the picker only defaults to the current tab; the user can pick anything, and the
grant lasts for the session. So a wrong pick means every later capture returns the wrong surface,
confidently, until it is re-picked (click 🖥 twice).
displaySurface |
meaning |
|---|---|
"browser" |
a tab — the expected case, no warning |
"window" |
a WINDOW, not this tab — carries a warning; the pixels are that window |
"monitor" |
a WHOLE MONITOR — carries a warning; may include other apps, won’t follow the page |
null |
this browser does not report it. Not a pass — it means unchecked |
If a warning is present, do not reason about layout from that image. Note "browser" means “a tab”,
not necessarily your tab: confirming that needs setCaptureHandleConfig(), which mutates
document-level state on the page the widget is injected into, and an injected tool should not
quietly change its host’s configuration.
The Electron desktop app uses native capturePage instead and needs no grant, so none of this
applies there.
Response: { success, path?, image?, width, height, source, displaySurface?, canvas?, warning? }
Parameters:
| Name | Type | Description |
|---|---|---|
ref |
string,null | Ref ID from /tree output - capture specific element |
selector |
string,null | Element to capture (omit for full page) |
canvas |
string,null | Selector for a |
format |
string,null | Image format: png (default), webp, or jpeg |
quality |
number,null | Quality for lossy formats (webp/jpeg). Either scale works: 0-1 (canvas-native) or 0-100 (percentage); anything above 1 is read as a percentage and clamped. Ignored for png. |
scale |
number,null | Scale factor (default 1) |
maxWidth |
number,null | Max width in pixels |
maxHeight |
number,null | Max height in pixels |
window |
string,null | Target window ID |
chyron |
boolean,null | Burn page title, URL, timestamp into image (default true, set false for clean screenshot) |
delay |
number,null | Wait ms before capturing (e.g. 1000 to let page settle after navigation) |
file |
boolean,null | Save to disk and return file path instead of data URL (default true — pass false for base64) |
schematic |
boolean,null | Return a schematic of the page INSTEAD of pixels, even when real capture is available. Cheaper, deterministic, and carries the contrast audit; canvases are still embedded as real pixels. |
fallback |
boolean,null | When no pixel capture is available, return a labelled SCHEMATIC of the page instead of failing (default true; canvases are embedded as real pixels since they need no permission). Pass false to hard-fail instead. |
Examples:
{}
{"ref":"42"}
{"selector":"#chart"}
{"scale":0.5,"maxWidth":400}
{"chyron":false}
POST /snapshotCapture page snapshot
Capture current page state for debugging.
Includes: DOM tree, console logs, viewport size, scroll position, URL, timestamp.
Response: { snapshot: { url, title, viewport, dom, console, timestamp } }
Great for debugging test failures - call this when something goes wrong.
Parameters:
| Name | Type | Description |
|---|---|---|
trigger |
string,null | What triggered the snapshot (e.g., “manual”, “test-failure”) |
context |
any | Additional context about the snapshot |
Examples:
{"trigger":"manual"}
{"trigger":"test-failure","context":{"step":3,"error":"Element not found"}}
POST /video/startStart video recording
Start recording the browser tab as WebM video. Requires the Haltija Desktop app.
The recording saves to
Response: { success, recordingId }
Parameters:
| Name | Type | Description |
|---|---|---|
maxDuration |
number,null | Max recording duration in seconds (default 60, max 300) |
window |
string,null | Target window ID |
Examples:
{}
{"maxDuration":120}
POST /video/stopStop video recording
Stop recording and save the video file.
Response: { success, path, duration, size }
Parameters:
| Name | Type | Description |
|---|---|---|
window |
string,null | Target window ID |
Examples:
{}
GET /video/statusCheck video recording status
Check if video recording is active.
Response: { recording, recordingId?, duration?, window? }
Examples:
{}
POST /dialog/configureConfigure native dialog auto-response policy
Set how native browser dialogs (alert, confirm, prompt) are handled.
By default, Haltija intercepts all native dialogs and auto-responds:
Configure the policy before triggering actions that cause dialogs. Each dialog is logged and reported via the dialog/opened push event.
Response: { policy: { alert, confirm, prompt, beforeunload } }
Parameters:
| Name | Type | Description |
|---|---|---|
alert |
string,null | “dismiss” (only option for alerts) |
confirm |
string,null | “accept” or “dismiss” |
prompt |
any | “dismiss” or { “response”: “text” } to auto-fill |
beforeunload |
string,null | “allow” or “block” — controls page unload |
window |
string,null | Target window ID |
Examples:
{"confirm":"accept"}
{"confirm":"dismiss"}
{"prompt":{"response":"my answer"}}
GET /dialog/historyGet recent dialog history
Returns a list of recently intercepted native dialogs.
Each entry includes: type (alert/confirm/prompt), message, response given, timestamp. Buffer holds the last 50 dialogs.
Response: { history: [{ type, message, defaultValue?, response, timestamp }] }
GET /networkGet captured network requests
Returns buffered network entries in compact format.
Each entry: { m: method, s: status, url: trimmed_url, t: time_ms, sz: human_size, type: resource_type } Status -1 means failed (timeout, CORS, canceled). Summary line always included.
Use preset parameter to override the watch preset for this query.
POST /network/watchStart capturing network traffic
Begin monitoring HTTP requests and responses via Chrome DevTools Protocol. Requires the Haltija Desktop app (uses Electron’s CDP access).
Presets control what’s captured:
Output is token-optimized: ~10 tokens per request vs 200+ for raw HAR.
Parameters:
| Name | Type | Description |
|---|---|---|
preset |
string,null | Filter preset: errors, minimal, standard, verbose |
includePatterns |
array,null | URL regex patterns to always include |
excludePatterns |
array,null | URL regex patterns to exclude |
maxBuffer |
number,null | Max entries to buffer (default 200) |
POST /network/unwatchStop capturing network traffic
Stop monitoring network requests. Clears the capture buffer.
GET /network/statsNetwork traffic summary
Returns: total requests, failures, pending, average latency, total bytes, and a one-line summary string.