API Docs
Home Support Report a bug Download docs (.md)

DOCUMENTATION / LOCAL AUTOMATION

Your browser.
In your code.

Meet the Vela Automation API. Open tabs, interact with pages, manage profiles, and follow browser events—from the language or tools you already use.

RESTWebSocketLocal by defaultExplore the reference ↘

START HERE

A first request.

  1. Open Vela → Settings → Automation.
  2. Enable the API and copy your API key.
  3. Check your browser’s status.
# Replace YOUR_KEY with your Vela API key. curl http://127.0.0.1:1306/api/status \ -H "X-API-Key: YOUR_KEY"

Authentication

All requests require an API key sent via the X-API-Key header or ?apiKey=YOUR_KEY query parameter.

Default: http://127.0.0.1:1306 — Configure in Settings → Automation

Download the full reference

The complete reference — endpoints with parameters, examples, error codes, events, data models and operating modes — in one Markdown file you can keep offline or hand to an AI agent: api-reference.md (view). Generated from the app's route table for version 1.5.238.

Errors & Diagnostics

Error responses are JSON with a stable code: NOT_FOUND (404 — only for ids that do not exist), BAD_REQUEST (400 — e.g. a proxy port outside 0–65535, a null settings value, a non-object eventInit), TAB_SUSPENDED (409 — the tab exists but its web view could not be restored; call POST /api/tabs/:id/wake or /activate, then retry), JS_EXCEPTION (500 — the page script threw; includes exception.message, line, column, sourceURL and partialSideEffectsPossible: true), JS_RESULT_UNSUPPORTED, WEB_PROCESS_TERMINATED, WEBVIEW_INVALIDATED, CONFLICT (409 — e.g. the tab was closed while the request was in flight), FORBIDDEN_HOST / FORBIDDEN_ORIGIN (403 — requests whose Host/Origin is not local are refused, so web pages cannot drive the API by DNS rebinding or cross-site fetch).

Every response carries an X-Request-Id header; requests are logged to the macOS unified log under subsystem com.vela.browser.automation (method, path, status, duration — never bodies, keys or scripts): log stream --predicate 'subsystem == "com.vela.browser.automation"'. Response bodies are always valid JSON (dates → ISO-8601, binary → base64/summary, NaN → null). Malformed HTTP gets 400 and the connection is closed.

Timing: wait, wait-for-*, type and mouse/move|random|path respond only once the page-side action has finished; a request may legitimately take up to 300 s and is never reaped while in flight. Suspended tabs (memory saver) are woken automatically by page-level endpoints, and API-created tabs are kept alive. Cookies of a suspended tab resolve to the tab's own profile store (a suspended private tab answers 409).

New Tabs from Clicks

Click endpoints follow normal browser targeting. Links with target="_blank", named targets such as target="report", or window.open() open real API-visible tabs instead of replacing the current tab.

When an API click creates a tab, the JSON response includes openedTabId, openedTabIds, and openedTabs. WebSocket clients also receive tab.created.

File Uploads (Native Picker)

Use filesystem paths with POST /api/tabs/:id/upload. Vela completes WKWebView's native file chooser so React / Facebook catalog receive a real FileList. Do not send MP4s as base64 (the HTTP body cap is 10 MB).

Stage first, then click Replace: {"paths":["/abs/video.mp4"]}. Or one-shot with selector and timeout. See File Chooser.

Status

GET /api/status Health check with version, uptime, counts ▶

Returns server status, browser version, uptime, and current window/tab counts.

Response
{ "status": "running", "version": "1.0.0", "uptime": 3600.5, "windowCount": 2, "tabCount": 8, "automationPort": 1306, "bindAddress": "127.0.0.1" }
curl
curl -H "X-API-Key: YOUR_KEY" http://127.0.0.1:1306/api/status
GET /api/docs Built-in HTML documentation (no auth) ▶

Returns a built-in HTML documentation page. No API key required.

curl
curl http://127.0.0.1:1306/api/docs

Tabs

GET /api/tabs List all tabs ▶

Returns all open tabs across all windows. Optionally filter by window ID. Includes tabs opened by target="_blank", named targets, and window.open() during API clicks.

Query Parameters
ParamDescription
windowId optionalFilter tabs to a specific window
Response
[{ "id": "550e8400-...", "windowId": "...", "title": "Example", "url": "https://example.com", "isLoading": false, "canGoBack": true, "isPinned": false, "isPrivate": false, "isSuspended": false }]
curl
curl -H "X-API-Key: YOUR_KEY" http://127.0.0.1:1306/api/tabs
GET /api/tabs/:id Get tab details ▶

Returns full details for a specific tab by ID.

curl
curl -H "X-API-Key: YOUR_KEY" http://127.0.0.1:1306/api/tabs/TAB_ID
POST /api/tabs Create a new tab ▶

Opens a new tab. Optionally specify URL, target window, profile, or private mode.

API-created tabs are appended at the end of the window's tab strip, so programmatic ordering stays predictable. (Tabs a user opens from a link/target="_blank" are instead placed next to the current tab.)

Body
{ "url": "https://example.com", "windowId": "...", "profileId": "...", "private": false }
curl
curl -X POST -H "X-API-Key: YOUR_KEY" -H "Content-Type: application/json" \ -d '{"url":"https://example.com"}' \ http://127.0.0.1:1306/api/tabs
DELETE /api/tabs/:id Close a tab ▶

Closes the specified tab.

curl
curl -X DELETE -H "X-API-Key: YOUR_KEY" http://127.0.0.1:1306/api/tabs/TAB_ID
POST /api/tabs/:id/navigate Navigate to URL ▶

Navigates the tab to a new URL.

Body
{ "url": "https://example.com" }
curl
curl -X POST -H "X-API-Key: YOUR_KEY" -H "Content-Type: application/json" \ -d '{"url":"https://example.com"}' \ http://127.0.0.1:1306/api/tabs/TAB_ID/navigate
POST /api/tabs/:id/reload Reload page ▶

Reloads the current page in the tab.

curl
curl -X POST -H "X-API-Key: YOUR_KEY" http://127.0.0.1:1306/api/tabs/TAB_ID/reload
POST /api/tabs/:id/back Go back ▶

Navigate back in history.

POST /api/tabs/:id/forward Go forward ▶

Navigate forward in history.

POST /api/tabs/:id/stop Stop loading ▶

Stops the current page from loading.

POST /api/tabs/:id/activate Switch to tab ▶

Makes this tab the active/selected tab in its window. Also restores the web view of a suspended tab.

POST /api/tabs/:id/wake Restore a suspended tab ▶

Recreates the web view of a tab whose view was released to save memory (isSuspended: true in GET /api/tabs) and reloads its URL, waiting briefly for the page. Page-level endpoints do this automatically; use /wake to pre-warm a tab. If the view cannot be restored, page-level endpoints answer 409 TAB_SUSPENDED instead of an ambiguous 404.

Response
{ "tabId": "…", "woken": true, "hasWebView": true, "isSuspended": false, "url": "https://…" }
POST /api/tabs/:id/duplicate Duplicate tab ▶

Creates a copy of the tab with the same URL.

POST /api/tabs/:id/pin Toggle pin ▶

Toggles the pinned state of the tab.

POST /api/tabs/:id/mute Toggle mute ▶

Toggles the muted state of the tab.

GET /api/tabs/:id/source Get page HTML source ▶

Returns the full HTML source of the current page.

Response
{ "source": "<!DOCTYPE html>..." }

Page Automation

POST /api/tabs/:id/execute Execute JavaScript ▶

Runs arbitrary JavaScript in the page context and returns the result.

Body
{ "script": "document.title" }
Response
{ "result": "Example Domain" }
curl
curl -X POST -H "X-API-Key: YOUR_KEY" -H "Content-Type: application/json" \ -d '{"script":"document.title"}' \ http://127.0.0.1:1306/api/tabs/TAB_ID/execute
POST /api/tabs/:id/click Click element ▶

Clicks the first element matching the CSS selector. If the click opens a target="_blank", named-target, or window.open() tab, the response includes openedTabId, openedTabIds, and openedTabs.

Body
{ "selector": "button.submit" }
Response when a new tab opens
{ "success": true, "openedTabId": "550e8400-...", "openedTabIds": ["550e8400-..."], "openedTabs": [{ "id": "550e8400-...", "url": "https://example.com" }] }
POST /api/tabs/:id/fill Fill input field ▶

Sets the value of an input field using native value setter (compatible with React, Angular, Vue).

Body
{ "selector": "input[name='email']", "value": "[email protected]" }
curl
curl -X POST -H "X-API-Key: YOUR_KEY" -H "Content-Type: application/json" \ -d '{"selector":"input[name=email]","value":"[email protected]"}' \ http://127.0.0.1:1306/api/tabs/TAB_ID/fill
POST /api/tabs/:id/select Select dropdown option ▶

Selects option(s) in a <select> element. Provide one of: value (string), values (array, for multi-select), label (visible text), or index.

Body
{ "selector": "select#country", "value": "US" }
POST /api/tabs/:id/check Set checkbox state ▶

Sets the checked state of a checkbox or radio input.

Body
{ "selector": "#agree", "checked": true }
POST /api/tabs/:id/type Type text character by character ▶

Types text into the focused element with optional per-keystroke delay (simulates real typing).

Body
{ "selector": "input#search", "text": "Hello World", "delay": 50 }
ParamDescription
selectorCSS selector for the target element
textText to type
delay optionalDelay in ms between keystrokes (default: 0)
POST /api/tabs/:id/wait Wait for element ▶

Waits until an element matching the selector appears in the DOM (uses MutationObserver).

Body
{ "selector": ".loaded", "timeout": 10000 }
ParamDescription
selectorCSS selector to wait for
timeout optionalTimeout in ms (default: 10000)
POST /api/tabs/:id/extract Extract data from elements ▶

Extracts text content or attribute values from one or more elements.

Body
{ "selector": "a.link", "attribute": "href", "multiple": true }
Response
{ "result": ["/page1", "/page2"] }
POST /api/tabs/:id/scroll Scroll page ▶

Scrolls to absolute coordinates or to a specific element.

Body (coordinates)
{ "x": 0, "y": 500 }
Body (selector)
{ "selector": "#footer" }
POST /api/tabs/:id/login Auto-fill credentials ▶

Looks up saved credentials for the domain and auto-fills the login form.

Body
{ "domain": "github.com" }
GET /api/tabs/:id/screenshot Take screenshot (base64 PNG) ▶

Captures a screenshot of the visible page and returns it as base64-encoded PNG.

Response
{ "data": "iVBORw0KGgo..." }
GET /api/tabs/:id/pdf Export page as PDF ▶

Exports the current page as a PDF document returned as base64.

Response
{ "data": "JVBERi0..." }
GET /api/tabs/:id/cookies Get cookies ▶

Returns all cookies for the tab's data store.

POST /api/tabs/:id/cookies Set cookies ▶

Sets one or more cookies in the tab's data store.

Body
{ "cookies": [{ "name": "session", "value": "abc123", "domain": "example.com", "path": "/" }] }
DELETE /api/tabs/:id/cookies Clear cookies ▶

Removes all cookies from the tab's data store.

GET /api/tabs/:id/text Get visible text ▶

Returns the visible text content of the page (innerText of body).

Response
{ "text": "Page content here..." }
GET /api/tabs/:id/snapshot Agent-friendly page snapshot ▶

One call to understand a page: URL, title, scroll position, headings, readable text, and the visible interactive elements numbered as refs (e1, e2…) with role, name, value and link. Each listed element is stamped data-vela-ref, so any selector-taking endpoint can act on it with {"selector": "[data-vela-ref=\"e7\"]"}. Refs reset on the next snapshot. Password values are never returned.

Query
?text=6000 (characters of page text; 0 = none, max 60000) ?all=1 (include off-screen elements)
Response
{ "url": "https://example.com/", "title": "Example", "scroll": { "y": 0, "height": 4200, "viewport": 900 }, "elements": [ { "ref": "e1", "tag": "a", "name": "Sign in", "href": "https://example.com/login" } ], "moreElements": 12, "headings": ["h1: Example Domain"], "dialogOpen": false, "frames": 0, "text": "..." }
POST /api/tabs/:id/upload Upload files via native picker ▶

Hands files to the page through WKWebView's native file picker. This is required for Facebook catalog / Meta Commerce video replacement and any site that .click()s a hidden file input. Prefer filesystem paths for MP4s — do not send videos as base64 (the HTTP body cap is 10 MB and in-page decode freezes the tab).

Body
{ "paths": ["/Users/me/video.mp4"], "selector": "button", "timeout": 20000 }
ParamDescription
pathsAbsolute filesystem paths. Best for videos.
pathSingle path shorthand
files[].pathFilesystem path as an item in files
files[].name / data / typeBase64 fallback for small images only
selector optionalClicks the Replace/upload control first (searches same-origin iframes and shadow roots)
timeout optionalMilliseconds to wait when selector is set (default 15000). Ignored for stage-only uploads, which return immediately.

Catalog video replacement: POST /upload with paths (stages the MP4 and returns immediately), then click Replace video. Or pass the button selector in the same request. Vela completes the native picker with a real FileList so Meta's editor can reach a savable state.

Response (stage-only)
{ "success": true, "staged": true, "method": "staged", "filesSet": 1 }
Response (picker completed)
{ "success": true, "method": "fileChooser", "filesSet": 1 }
curl
curl -X POST -H "X-API-Key: YOUR_KEY" -H "Content-Type: application/json" \ -d '{"paths":["/Users/me/video.mp4"]}' \ http://127.0.0.1:1306/api/tabs/TAB_ID/upload

Windows

GET /api/windows List all windows ▶

Returns all open browser windows.

Response
[{ "id": "...", "tabCount": 5, "isPrivate": false, "profileId": "...", "currentTabId": "..." }]
GET /api/windows/:id Get window details ▶

Returns details for a specific window.

POST /api/windows Open new window ▶

Opens a new browser window, optionally with a specific profile or in private mode. Returns the new window ID and active tab ID.

Body
{ "profileId": "...", "private": false }
Response
{ "opened": true, "windowId": "UUID", "tabId": "UUID" }
Supports creating many windows concurrently (e.g. bulk automation of 20+ accounts at once). In GUI mode, real windows are opened. In hidden/headless mode, windows are created instantly without UI. All three modes return windowId and tabId reliably.
POST /api/windows/:id/activate Activate window (bring to front) ▶

Brings the window to the foreground and activates the Vela app. In headed mode, makes the window the key window and calls NSApplication.activate(ignoringOtherApps: true). In hidden/headless mode this is a no-op.

Response
{ "activated": true }
Useful for ensuring headed-mode windows are visible during automation. Without activation, macOS may keep API-created windows behind other apps.
DELETE /api/windows/:id Close window ▶

Closes the specified browser window and all its tabs. Uses a direct NSWindow reference for reliable closing, and always deregisters the window from the API even if the physical close fails.

Window → Active Tab (Convenience)

All tab operations are mirrored at the window level. These endpoints automatically target the window's active tab, so you don't need to look up the tab ID first. Same request/response format as the tab equivalents. Click endpoints return openedTabId/openedTabs when a target-new or window.open() action creates a tab.

POST /api/windows/:id/navigate Navigate active tab ▶

Navigate the window's active tab to a URL. Body: { "url": "https://..." }

POST/api/windows/:id/reloadReload active tab▶

Reload the current page on the window's active tab.

POST/api/windows/:id/backGo back on active tab▶

Navigate back in the active tab's history.

POST/api/windows/:id/forwardGo forward on active tab▶

Navigate forward in the active tab's history.

POST/api/windows/:id/stopStop loading▶

Stop loading the page on the active tab.

POST/api/windows/:id/executeRun JavaScript on active tab▶

Execute JavaScript on the active tab. Body: { "script": "document.title" }

POST/api/windows/:id/clickClick element on active tab▶

Click an element. Body: { "selector": "button.submit" }. If it opens a target-new tab, the response includes openedTabId and openedTabs.

POST/api/windows/:id/fillFill input on active tab▶

Fill an input field. Body: { "selector": "input[name='email']", "value": "[email protected]" }

POST/api/windows/:id/selectSelect dropdown on active tab▶

Select a dropdown option. Body: { "selector": "select#country", "value": "US" }

POST/api/windows/:id/checkToggle checkbox on active tab▶

Set checkbox state. Body: { "selector": "#agree", "checked": true }

POST/api/windows/:id/typeType text on active tab▶

Type text character by character. Body: { "selector": "#search", "text": "hello", "delay": 50 }

POST/api/windows/:id/waitWait for element on active tab▶

Wait for an element to appear. Body: { "selector": ".loaded", "timeout": 10000 }

POST/api/windows/:id/extractExtract content from active tab▶

Extract text or attributes. Body: { "selector": "h1", "attribute": "textContent", "multiple": false }

POST/api/windows/:id/scrollScroll on active tab▶

Scroll by offset or to element. Body: { "y": 500 } or { "selector": "#footer" }

POST/api/windows/:id/loginAuto-fill credentials on active tab▶

Fill saved credentials. Body: { "domain": "example.com" }

GET/api/windows/:id/screenshotScreenshot active tab▶

Capture a screenshot of the active tab as base64 PNG.

GET/api/windows/:id/pdfExport PDF from active tab▶

Export the active tab as a base64-encoded PDF.

GET/api/windows/:id/cookiesGet cookies from active tab▶

Get cookies for the active tab's current domain.

POST/api/windows/:id/cookiesSet cookies on active tab▶

Set cookies. Body: { "cookies": [{ "name": "...", "value": "...", "domain": "..." }] }

DELETE/api/windows/:id/cookiesClear cookies on active tab▶

Clear all cookies for the active tab's current domain.

GET/api/windows/:id/textGet visible text from active tab▶

Get all visible text content from the active tab.

POST/api/windows/:id/uploadUpload files on active tab▶

Same as /api/tabs/:id/upload on the window's active tab. Prefer { "paths": ["/abs/video.mp4"] } for catalog videos.

POST/api/windows/:id/mouse/moveMove cursor on active tab▶

Move mouse cursor with easing on the active tab. Body: { "x": 500, "y": 300 } or { "selector": "#btn" } + optional "duration", "steps", "easing" (ease|linear|human).

POST/api/windows/:id/mouse/clickClick on active tab▶

Full click sequence on active tab. Body: { "selector": "#btn" } + optional "button" (left|right|middle), "doubleClick". New target tabs are returned as openedTabId/openedTabs.

POST/api/windows/:id/mouse/downMouse down on active tab▶

Dispatches pointerdown + mousedown on the active tab.

POST/api/windows/:id/mouse/upMouse up on active tab▶

Dispatches pointerup + mouseup on the active tab.

POST/api/windows/:id/mouse/randomRandom mouse movement on active tab▶

Human-like random mouse movement on the active tab. Body: { "duration": 3000 } + optional "area", "speed".

POST/api/windows/:id/mouse/pathMouse path on active tab▶

Move along waypoints on the active tab. Body: { "points": [{"x":100,"y":100}, ...], "duration": 1000 }

GET/api/windows/:id/sourceGet HTML source of active tab▶

Return the full HTML source of the window's active tab.

POST/api/windows/:id/duplicateDuplicate active tab▶

Open a copy of the window's active tab.

POST/api/windows/:id/pinToggle pin on active tab▶

Toggle the pinned state of the window's active tab.

POST/api/windows/:id/muteToggle mute on active tab▶

Toggle audio mute on the window's active tab.

POST/api/windows/:id/hoverHover element on active tab▶

Hover over an element. Body: { "selector": ".menu" }

POST/api/windows/:id/focusFocus element on active tab▶

Focus an element. Body: { "selector": "input#email" }

POST/api/windows/:id/dblclickDouble-click on active tab▶

Double-click an element. Body: { "selector": "#item" }. New target tabs are returned as openedTabId/openedTabs.

POST/api/windows/:id/pressPress key on active tab▶

Dispatch a key press. Body: { "key": "Enter" }

POST/api/windows/:id/uncheckUncheck checkbox on active tab▶

Uncheck a checkbox. Body: { "selector": "#agree" }

Profiles

GET /api/profiles List all profiles ▶

Returns all browser profiles with their configuration.

Response
[{ "id": "...", "name": "Work", "icon": "briefcase", "color": "blue", "fingerprintSeed": 12345, "fingerprintEnabled": true, "spoofLanguage": "en-US,en", "spoofTimezone": "America/New_York", "userAgentId": "chrome_mac", "proxyConfig": null }]
GET /api/profiles/:id Get profile details ▶

Returns full details for a specific profile.

POST /api/profiles Create profile with all settings ▶

Creates a new browser profile with isolated cookies, storage, and fingerprint. Accepts ALL profile settings on creation.

Body (all fields optional except name)
{ "name": "Shopping", "icon": "cart", "color": "#10B981", "userAgentId": "chrome_mac", "fingerprintEnabled": true, "fingerprintSeed": 123456, "proxyConfig": { "type": "SOCKS5", "host": "proxy.example.com", "port": 1080, "username": "user", "password": "pass", "isEnabled": true }, "dnsProvider": "cloudflare", "contentBlockerEnabled": true, "blockTrackers": true, "blockAds": true, "blockPopups": true, "httpsFirstEnabled": true, "torEnabled": false, "autofillEnabled": true, "savePasswordEnabled": true, "spoofLanguage": "en-US,en", "spoofTimezone": "America/New_York", "sessionCookies": [{ "name": "session", "value": "abc123", "domain": ".example.com", "path": "/", "isSecure": true }] }
ParamDescription
nameProfile name (required)
icon optionalSF Symbol name (person.circle, briefcase, house, cart, etc.)
color optionalHex color (e.g. #4A90D9)
proxyConfig optionalPer-profile proxy: type (HTTP/SOCKS5), host, port, username, password, isEnabled
dnsProvider optionalDNS: system, cloudflare, google, quad9, adguard, opendns, cleanbrowsing, nextdns
sessionCookies optionalArray of cookies to store with the profile
privacy flags optionalcontentBlockerEnabled, blockTrackers, blockAds, blockPopups, httpsFirstEnabled, torEnabled
autofillEnabled optionalEnable/disable password autofill for this profile (null = use global)
savePasswordEnabled optionalEnable/disable save-password prompts for this profile (null = use global)
spoofLanguage optionalOverride navigator.languages / Accept-Language, e.g. "en-US,en" or "fr-FR,fr" (null = use browser default)
spoofTimezone optionalOverride reported timezone via Intl/Date APIs, e.g. "America/New_York" or "Europe/Paris" (null = use system timezone)
curl
curl -X POST -H "X-API-Key: YOUR_KEY" -H "Content-Type: application/json" \ -d '{"name":"Shopping","icon":"cart","color":"#10B981","dnsProvider":"cloudflare","proxyConfig":{"type":"SOCKS5","host":"proxy.example.com","port":1080,"isEnabled":true}}' \ http://127.0.0.1:1306/api/profiles
PUT /api/profiles/:id Update profile ▶

Partially update any profile field (name, icon, color, UA, fingerprint, proxy, DNS, privacy settings).

Body (partial update)
{ "name": "New Name", "fingerprintEnabled": true }
DELETE /api/profiles/:id Delete profile ▶

Deletes a profile. Cannot delete the default profile.

POST /api/profiles/:id/activate Switch window to profile ▶

Switches a window to use this profile.

Body
{ "windowId": "..." }
POST /api/profiles/:id/duplicate Duplicate profile ▶

Creates a copy of the profile with a new fingerprint seed.

POST /api/profiles/:id/regenerate-fingerprint New fingerprint seed ▶

Generates a new random fingerprint seed for the profile.

User Agents

GET /api/user-agents List 48+ UA presets ▶

Returns all available user agent presets organized by category (Desktop, Mobile, Bots, etc.).

Response
[{ "id": "chrome_mac", "name": "Chrome on macOS", "value": "Mozilla/5.0 ...", "category": "Desktop", "isSafeForLogin": true }]
GET /api/user-agents/current Get current global UA ▶

Returns the currently active global user agent.

PUT /api/user-agents/current Set global UA ▶
Body
{ "id": "iphone16promax" }
PUT /api/profiles/:id/user-agent Set profile UA ▶
Body
{ "id": "chrome_windows" }

Fingerprints

GET /api/fingerprint Global fingerprint status ▶
Response
{ "enabled": true, "seed": 42 }
PUT /api/fingerprint Toggle fingerprint protection ▶
Body
{ "enabled": true }
GET /api/profiles/:id/fingerprint Profile fingerprint config ▶

Returns the fingerprint configuration for a specific profile.

Response
{ "enabled": true, "seed": 12345, "spoofLanguage": "en-US,en", "spoofTimezone": "America/New_York" }
PUT /api/profiles/:id/fingerprint Update profile fingerprint ▶
Body
{ "enabled": true, "seed": 99999, "spoofLanguage": "en-US,en", "spoofTimezone": "America/New_York" }
ParamDescription
enabled optionalEnable/disable fingerprint protection
seed optionalInteger seed for deterministic fingerprint values
spoofLanguage optionalOverride navigator.languages, e.g. "en-US,en" (null to clear)
spoofTimezone optionalOverride timezone via Intl/Date APIs, e.g. "America/New_York" (null to clear)
POST /api/profiles/:id/fingerprint/regenerate New random seed ▶

Generates a new random fingerprint seed for the profile.

Real Browser

Real Browser mode presents Vela as a vanilla Safari/WebKit browser — no JavaScript fingerprint overrides are injected. Pages see the same environment millions of real Safari users have, so captcha and bot-detection checks pass cleanly. Mutually exclusive with Fingerprint Protection: enabling one disables the other. Default for new installs: Real Browser on, Fingerprint Protection off.

GET /api/real-browser Global Real Browser status ▶
Response
{ "enabled": true, "fingerprintProtection": false }
PUT /api/real-browser Toggle Real Browser ▶
Body
{ "enabled": true }

Setting enabled: true automatically disables Fingerprint Protection (and vice versa via PUT /api/fingerprint).

GET /api/profiles/:id/real-browser Per-profile override ▶

Returns the per-profile override (null means "follow global") and the effective resolved value used for tabs in this profile.

Response
{ "override": true, "effective": true }
PUT /api/profiles/:id/real-browser Set profile override ▶
Body
{ "enabled": true }
ParamDescription
enabledtrue = force Real Browser on for this profile, false = force off, null = clear override and follow global

Settings

GET /api/settings List all settings ▶

Returns all vela_* UserDefaults settings as key-value pairs.

Response
[ { "key": "vela_automation_port", "value": 1306 }, { "key": "vela_dark_mode", "value": true } ]
GET /api/settings/:key Get specific setting ▶

Returns the value for a single setting key.

PUT /api/settings/:key Update setting ▶
Body
{ "value": true }
curl
curl -X PUT -H "X-API-Key: YOUR_KEY" -H "Content-Type: application/json" \ -d '{"value": true}' \ http://127.0.0.1:1306/api/settings/vela_dark_mode

Bookmarks

GET /api/bookmarks List bookmarks ▶

Returns all bookmarks. Optionally filter by folder.

ParamDescription
folderId optionalFilter to a specific folder
POST /api/bookmarks Create bookmark ▶
Body
{ "title": "Example", "url": "https://example.com", "folderId": "..." }
PUT /api/bookmarks/:id Update bookmark ▶

Update a bookmark's title, URL, or folder.

DELETE /api/bookmarks/:id Delete bookmark ▶

Removes a bookmark.

GET /api/bookmarks/folders List folders ▶

Returns all bookmark folders.

POST /api/bookmarks/folders Create folder ▶
Body
{ "name": "Dev Tools", "parentId": "..." }
DELETE /api/bookmarks/folders/:id Delete folder ▶

Removes a bookmark folder.

History

GET /api/history List history entries ▶
ParamDescription
limit optionalMax results (default: 100)
query optionalSearch text filter
GET /api/history/top Top visited sites ▶

Returns the most frequently visited sites.

DELETE /api/history Clear all history ▶

Permanently deletes all browsing history.

DELETE /api/history/:id Delete single entry ▶

Removes a single history entry by ID.

Downloads

GET /api/downloads List downloads ▶
Response
[{ "id": "...", "fileName": "file.zip", "url": "https://...", "state": "completed", "progress": 1.0, "totalBytes": 1048576, "downloadedBytes": 1048576 }]
GET /api/downloads/:id Get download details ▶

Returns details for a single download including progress, state, and byte counts.

Response
{ "id": "...", "fileName": "file.zip", "url": "https://...", "state": "downloading", "progress": 0.45, "totalBytes": 1048576, "downloadedBytes": 471859 }
POST /api/downloads Start download ▶

Starts downloading a file from the given URL.

Body
{ "url": "https://example.com/file.zip", "fileName": "custom.zip" }
ParamDescription
urlURL of the file to download
fileName optionalCustom file name (defaults to URL's last path component)
POST /api/downloads/:id/pause Pause download ▶

Pauses an in-progress download. The download can be resumed later with the resume endpoint.

curl
curl -X POST -H "X-API-Key: YOUR_KEY" \ http://127.0.0.1:1306/api/downloads/DOWNLOAD_ID/pause
POST /api/downloads/:id/resume Resume download ▶

Resumes a paused download from where it left off.

curl
curl -X POST -H "X-API-Key: YOUR_KEY" \ http://127.0.0.1:1306/api/downloads/DOWNLOAD_ID/resume
DELETE /api/downloads/:id Cancel/remove download ▶

Cancels an in-progress download or removes a completed one.

Passwords

GET /api/passwords List credentials (passwords hidden) ▶

Lists all saved credentials. Passwords are not included in the response for security.

Response
[{ "id": "...", "domain": "github.com", "username": "[email protected]", "title": "GitHub" }]
GET /api/passwords/:id Get full credential (requires extra header) ▶

Returns the full credential including password. Requires the X-Password-Access: true header for additional security.

curl
curl -H "X-API-Key: YOUR_KEY" -H "X-Password-Access: true" \ http://127.0.0.1:1306/api/passwords/PASS_ID
POST /api/passwords/search Search by domain ▶
Body
{ "domain": "github.com" }
POST /api/passwords/generate Generate secure password ▶
Body
{ "length": 24, "symbols": true }
Response
{ "password": "xK9#mP2$vL7@nQ4..." }

Profile Autofill

GET /api/profiles/:id/autofill Get per-profile autofill settings ▶

Returns autofill and save-password settings for a specific profile. null values mean the profile uses the global setting.

Response
{ "profileId": "...", "autofillEnabled": null, "savePasswordEnabled": null, "note": "null values mean the profile uses global settings" }
curl
curl -H "X-API-Key: YOUR_KEY" http://127.0.0.1:1306/api/profiles/PROFILE_ID/autofill
PUT /api/profiles/:id/autofill Update per-profile autofill settings ▶

Enable or disable autofill and save-password prompts for a specific profile. Pass resetToGlobal: true to clear overrides and inherit global settings.

Body
{ "autofillEnabled": false, "savePasswordEnabled": false }
ParamDescription
autofillEnabled optionalEnable/disable password autofill for this profile
savePasswordEnabled optionalEnable/disable save-password prompts for this profile
resetToGlobal optionalSet to true to clear overrides and use global settings
curl
curl -X PUT -H "X-API-Key: YOUR_KEY" -H "Content-Type: application/json" \ -d '{"autofillEnabled":false,"savePasswordEnabled":false}' \ http://127.0.0.1:1306/api/profiles/PROFILE_ID/autofill

Content Blocker

GET /api/content-blocker Get content blocker status ▶
Response
{ "enabled": true, "blockAds": true, "blockTrackers": true, "blockPopups": true, "extraBlockTrackers": false, "extraBlockAds": false, "automaticCookieConsent": true }
PUT /api/content-blocker Update content blocker ▶
Body
{ "enabled": true, "blockAds": true, "blockTrackers": true, "blockPopups": false, "extraBlockTrackers": true, "extraBlockAds": false, "automaticCookieConsent": true }

Note: extraBlockTrackers enables 80+ additional tracker domains. extraBlockAds enables 60+ additional ad networks. Both are off by default. automaticCookieConsent handles cookie consent dialogs on supported sites.

Profile Cookies

GET /api/profiles/:id/cookies Get stored session cookies ▶

Returns all session cookies stored in the profile configuration.

Response
{ "cookies": [{ "id": "...", "name": "session", "value": "abc123", "domain": ".example.com", "path": "/", "isSecure": true, "isHttpOnly": false }], "count": 1 }
PUT /api/profiles/:id/cookies Set session cookies ▶

Replaces all stored session cookies for the profile.

Body
{ "cookies": [{ "name": "session", "value": "abc123", "domain": ".example.com", "path": "/", "isSecure": true, "isHttpOnly": false }] }
curl
curl -X PUT -H "X-API-Key: YOUR_KEY" -H "Content-Type: application/json" \ -d '{"cookies":[{"name":"session","value":"abc123","domain":".example.com"}]}' \ http://127.0.0.1:1306/api/profiles/PROFILE_ID/cookies
DELETE /api/profiles/:id/cookies Clear session cookies ▶

Removes all stored session cookies from the profile.

POST /api/profiles/:id/cookies/inject Inject cookies into live data store ▶

Takes the profile's stored session cookies and injects them into the live WKWebsiteDataStore, making them immediately available to web pages.

Response
{ "injected": 5 }
GET /api/profiles/:id/cookies/live Get ALL live cookies from data store ▶

Returns all cookies currently in the profile's live data store (not just stored ones). Optionally filter by domain.

ParamDescription
domain optionalFilter cookies to a specific domain
curl
curl -H "X-API-Key: YOUR_KEY" "http://127.0.0.1:1306/api/profiles/PROFILE_ID/cookies/live?domain=example.com"

Profile Export / Import

POST /api/profiles/:id/export Export profile as JSON ▶

Exports a complete profile including all settings, proxy config, DNS, privacy flags, session cookies, and ALL live cookies from the data store. The output can be fed directly into the import endpoint.

Response (truncated)
{ "id": "...", "name": "Work", "proxyConfig": { ... }, "dnsProvider": "cloudflare", "sessionCookies": [...], "liveCookies": [...], "liveCookieCount": 42, "exportedAt": "2025-01-15T10:30:00Z" }
curl
curl -X POST -H "X-API-Key: YOUR_KEY" http://127.0.0.1:1306/api/profiles/PROFILE_ID/export
POST /api/profiles/import Import profile from JSON ▶

Creates a new profile from JSON data (same format as export). Live cookies are injected into the new profile's data store.

Body
// Pass the full JSON from the export endpoint as the body { "name": "Work", "proxyConfig": {...}, "liveCookies": [...] }
POST /api/profiles/import-url Import profile from remote URL ▶

Downloads a profile ZIP from a remote URL (Dropbox, Google Drive, direct links) and imports it.

Body
{ "url": "https://dropbox.com/s/.../profile.zip" }

Profile Size & Data

GET /api/profiles/:id/size Get profile data store size ▶

Returns the number of data records, cookies, and data type breakdown for a profile's storage.

Response
{ "profileId": "...", "totalRecords": 15, "cookieCount": 42, "dataTypes": { "WKWebsiteDataTypeCookies": 8, "WKWebsiteDataTypeLocalStorage": 5, "WKWebsiteDataTypeDiskCache": 2 }, "displayNames": ["example.com", "google.com"] }
DELETE /api/profiles/:id/data Clear all profile browsing data ▶

Removes ALL browsing data for the profile (cookies, cache, local storage, indexed databases, etc.).

curl
curl -X DELETE -H "X-API-Key: YOUR_KEY" http://127.0.0.1:1306/api/profiles/PROFILE_ID/data

Profile Permissions / Privacy

GET /api/profiles/:id/permissions Get privacy settings ▶

Returns all privacy/permission settings for the profile. null values mean the profile inherits the global setting.

Response
{ "contentBlockerEnabled": true, "blockTrackers": true, "blockAds": true, "blockPopups": null, "httpsFirstEnabled": true, "torEnabled": false, "fingerprintEnabled": true, "automaticCookieConsent": true }
PUT /api/profiles/:id/permissions Update privacy settings ▶

Update privacy/permission settings for the profile. Pass "resetToGlobal": true to clear all overrides and use global settings.

Body
{ "contentBlockerEnabled": true, "blockTrackers": true, "blockAds": false, "httpsFirstEnabled": true, "torEnabled": false, "fingerprintEnabled": true, "automaticCookieConsent": true }
Reset to global
{ "resetToGlobal": true }

Global Proxy

GET /api/proxy Get global proxy config ▶

Returns the global proxy configuration (applies to profiles without their own proxy).

Response
{ "enabled": true, "type": "HTTP", "host": "proxy.example.com", "port": 8080, "username": "user", "password": "pass" }
PUT /api/proxy Set global proxy ▶

Update the global proxy configuration. All fields are optional (partial update).

Body
{ "enabled": true, "type": "SOCKS5", "host": "proxy.example.com", "port": 1080, "username": "user", "password": "pass" }
curl
curl -X PUT -H "X-API-Key: YOUR_KEY" -H "Content-Type: application/json" \ -d '{"enabled":true,"type":"SOCKS5","host":"proxy.example.com","port":1080}' \ http://127.0.0.1:1306/api/proxy

Global DNS

GET /api/dns Get DNS provider + list all ▶

Returns the current DNS provider and lists all available providers with details.

Response
{ "current": "cloudflare", "currentName": "Cloudflare", "providers": [{ "id": "cloudflare", "name": "Cloudflare", "detail": "1.1.1.1, 1.0.0.1", "isCurrent": true }] }
PUT /api/dns Set DNS provider ▶

Changes the global DNS provider.

Body
{ "provider": "cloudflare" }
Valid providersDNS Servers
systemOS/router default
cloudflare1.1.1.1, 1.0.0.1
google8.8.8.8, 8.8.4.4
quad99.9.9.9, 149.112.112.112
adguard94.140.14.14, 94.140.15.15
opendns208.67.222.222, 208.67.220.220
cleanbrowsing185.228.168.9, 185.228.169.9
nextdns45.90.28.0, 45.90.30.0
POST /api/dns/probe Measure DNS latency ▶

Measures the latency to a specific DNS provider in milliseconds.

Body
{ "provider": "cloudflare" }
Response
{ "provider": "cloudflare", "latencyMs": 12, "reachable": true }
POST /api/dns/probe-all Measure all DNS providers ▶

Tests latency for all DNS providers, sorted by fastest first.

Response
{ "results": [ { "provider": "cloudflare", "latencyMs": 12, "reachable": true }, { "provider": "google", "latencyMs": 18, "reachable": true } ] }

Tor

GET /api/tor Full Tor status ▶

Returns comprehensive Tor status including connection state, SOCKS proxy details, and circuit info.

Response
{ "enabled": true, "isConnected": true, "isConnecting": false, "circuitInfo": "Connected to Tor network", "socksHost": "127.0.0.1", "socksPort": 9150, "controlPort": 9151, "isTorInstalled": true, "errorMessage": null }
PUT /api/tor Enable/disable Tor ▶
Body
{ "enabled": true }
POST /api/tor/new-circuit Request new Tor identity ▶

Requests a new Tor circuit (new exit IP). Tor must be connected. Sends SIGNAL NEWNYM to the Tor control port.

curl
curl -X POST -H "X-API-Key: YOUR_KEY" http://127.0.0.1:1306/api/tor/new-circuit
Response
{ "requested": true }

Mouse Simulation

POST /api/tabs/:id/mouse/move Move mouse cursor with easing ▶

Smoothly moves the simulated mouse cursor to a target position or CSS selector. Dispatches pointermove and mousemove events along the path. Supports multiple easing modes including human-like movement with random jitter.

Request Body
{ "x": 500, "y": 300, "duration": 200, "steps": 10, "easing": "ease" }
ParamTypeDescription
xnumberTarget X coordinate (or use selector)
ynumberTarget Y coordinate (or use selector)
selectorstringCSS selector — moves to element center (alternative to x/y)
durationnumberMovement duration in ms (default: 100)
stepsnumberNumber of intermediate steps (default: 10)
easingstring"linear", "ease", or "human" (adds ±3px jitter)
cURL Example
curl -X POST -H "X-API-Key: YOUR_KEY" -H "Content-Type: application/json" \ -d '{"selector":"#submit-btn","duration":300,"easing":"human"}' \ http://127.0.0.1:1306/api/tabs/TAB_ID/mouse/move
Response
{ "success": true, "x": 500, "y": 300, "element": "button" }
POST /api/tabs/:id/mouse/click Full click sequence at position or selector ▶

Dispatches a complete click event sequence at the target: pointerdown → mousedown → pointerup → mouseup → click. Optionally fires dblclick for double-click. If the click opens a target-new or window.open() tab, the response includes openedTabId and openedTabs.

Request Body
{ "selector": "#login-btn", "button": "left", "doubleClick": false }
ParamTypeDescription
x / ynumberTarget coordinates (or use selector)
selectorstringCSS selector — clicks element center
buttonstring"left" (default), "right", or "middle"
doubleClickbooleanAlso fire dblclick event (default: false)
Response
{ "success": true, "x": 250, "y": 180, "element": "button" }
POST /api/tabs/:id/mouse/down Mouse button down ▶

Dispatches pointerdown and mousedown events at the target position. Use with /mouse/up for drag operations.

Request Body
{ "x": 200, "y": 150, "button": "left" }
Response
{ "success": true, "x": 200, "y": 150, "element": "div" }
POST /api/tabs/:id/mouse/up Mouse button up ▶

Dispatches pointerup and mouseup events at the target position. Use after /mouse/down to complete drag operations.

Request Body
{ "x": 400, "y": 300, "button": "left" }
Response
{ "success": true, "x": 400, "y": 300, "element": "div" }
POST /api/tabs/:id/mouse/random Random human-like mouse movement ▶

Generates random human-like mouse movements for a specified duration. Moves between random targets within a bounding area with ease-in-out curves and micro-jitter. Useful for anti-detection and making automated sessions appear natural.

Request Body
{ "duration": 3000, "area": { "x": 100, "y": 100, "width": 800, "height": 600 }, "speed": "normal" }
ParamTypeDescription
durationnumberHow long to move in milliseconds (required)
areaobjectBounding box { x, y, width, height } — defaults to viewport
speedstring"slow" (80ms), "normal" (40ms), or "fast" (20ms) step intervals
cURL Example
curl -X POST -H "X-API-Key: YOUR_KEY" -H "Content-Type: application/json" \ -d '{"duration":5000,"speed":"slow"}' \ http://127.0.0.1:1306/api/tabs/TAB_ID/mouse/random
Response
{ "success": true, "moves": 87, "finalX": 432.5, "finalY": 287.3 }
POST /api/tabs/:id/mouse/path Move along array of waypoints ▶

Moves the mouse cursor along a sequence of waypoints with proportional timing based on segment distances. Each segment uses the specified easing function.

Request Body
{ "points": [ { "x": 100, "y": 100 }, { "x": 300, "y": 200 }, { "x": 500, "y": 150 } ], "duration": 1000, "easing": "human" }
ParamTypeDescription
pointsarrayArray of { x, y } waypoints (minimum 2, required)
durationnumberTotal duration in ms (default: 500)
easingstring"linear", "ease", or "human"
Response
{ "success": true, "x": 500, "y": 150, "points": 3 }

Element Queries

POST/api/tabs/:id/element/visibleCheck if element is visible▶

Check if an element is visible (has dimensions, not hidden/display:none).

Body
{ "selector": "#submit-btn" }
Response
{ "visible": true }
POST/api/tabs/:id/element/enabledCheck if element is enabled▶

Check if an element is not disabled.

Body
{ "selector": "button" }
POST/api/tabs/:id/element/checkedCheck if checkbox/radio is checked▶

Check the checked state of an input element.

POST/api/tabs/:id/element/hiddenCheck if element is hidden▶

Inverse of visible — returns true if element has no dimensions or is hidden.

POST/api/tabs/:id/element/countCount matching elements▶

Count elements matching the selector.

Response
{ "count": 5 }
POST/api/tabs/:id/element/bounding-boxGet element bounding rectangle▶

Returns x, y, width, height, top, right, bottom, left.

POST/api/tabs/:id/element/inner-htmlGet element innerHTML▶

Returns the innerHTML of the first matching element.

POST/api/tabs/:id/element/inner-textGet element innerText▶

Returns the rendered innerText of the element.

POST/api/tabs/:id/element/text-contentGet element textContent▶

Returns the raw textContent of the element (includes hidden text).

POST/api/tabs/:id/element/attributeGet element attribute▶

Get a specific attribute value.

Body
{ "selector": "a", "name": "href" }
POST/api/tabs/:id/element/screenshotScreenshot a specific element▶

Take a screenshot of a specific element (base64 PNG). macOS only.

Element Actions

POST/api/tabs/:id/hoverHover over element▶

Dispatch pointerenter/mouseover/mouseenter events on element center.

POST/api/tabs/:id/focusFocus element▶

Call el.focus() on the matched element.

POST/api/tabs/:id/dblclickDouble-click element▶

Dispatch full mousedown/mouseup/click/dblclick sequence.

POST/api/tabs/:id/pressPress key on element▶

Dispatch keydown/keypress/keyup on element or active element.

Body
{ "key": "Enter", "selector": "input" }
POST/api/tabs/:id/uncheckUncheck checkbox▶

Set checked=false and dispatch change event.

POST/api/tabs/:id/input-valueRead an input/textarea/select value▶

Playwright inputValue(). Body: { "selector": "#email" } → { "value": "[email protected]" }.

POST/api/tabs/:id/scroll-into-viewScroll an element into view▶

Playwright scrollIntoViewIfNeeded(). Body: { "selector": "#footer" }.

POST/api/tabs/:id/dispatch-eventDispatch a DOM event on an element▶

Playwright dispatchEvent(). Body: { "selector": "#btn", "type": "click", "eventInit": { } }. eventInit is merged into the event.

POST/api/tabs/:id/drag-and-dropDrag one element onto another▶

Synthetic HTML5 drag (dragstart → dragover → drop → dragend). Body: { "sourceSelector": "#a", "targetSelector": "#b" }.

POST/api/tabs/:id/set-contentReplace the page's HTML▶

Playwright setContent(). Body: { "html": "<h1>Hi</h1>", "baseURL": "https://example.com" } (baseURL optional).

POST/api/tabs/:id/add-script-tagInject a <script> tag▶

Playwright addScriptTag(). Body: { "url": "https://..." } or { "content": "window.__x=1" }.

POST/api/tabs/:id/add-style-tagInject a <style>/<link> tag▶

Playwright addStyleTag(). Body: { "url": "https://..." } or { "content": "body{background:red}" }.

POST/api/tabs/:id/add-init-scriptRun a script at document-start on every future navigation▶

Playwright addInitScript(). Body: { "script": "window.__init=true" }. Applies to the next and subsequent page loads in this tab.

GET/api/tabs/:id/contentGet full page HTML▶

Returns document.documentElement.outerHTML.

GET/api/tabs/:id/page-titleGet page title▶

Returns the current tab title.

Keyboard Input

POST/api/tabs/:id/keyboard/pressPress key with optional modifiers▶

Dispatch keydown/keypress/keyup on the active element.

Body
{ "key": "a", "modifiers": ["Control"] }
POST/api/tabs/:id/keyboard/insert-textInsert text at cursor▶

Insert text using execCommand or value setter.

POST/api/tabs/:id/keyboard/downKey down event▶

Dispatch keydown only.

POST/api/tabs/:id/keyboard/upKey up event▶

Dispatch keyup only.

POST/api/tabs/:id/mouse/wheelMouse wheel scroll▶

Dispatch WheelEvent.

Body
{ "deltaX": 0, "deltaY": 100 }

Native OS Input

Dispatch input via macOS native events — produces isTrusted: true events that are indistinguishable from real user input. In headed mode, uses CGEvent (native OS events posted to screen coordinates). In hidden/headless mode, injects NSEvent directly into the WKWebView — still produces isTrusted: true events without needing a visible window. All modes now return "native": true. Safe for 20+ concurrent hidden instances (pasteboard access is serialized). Headed mode requires Accessibility permission (System Settings > Privacy & Security > Accessibility).

POST/api/tabs/:id/native/clickNative mouse click (isTrusted:true)▶

Click an element or coordinates using real OS input events. The element is scrolled into view first. In headed mode, the window is activated and a CGEvent mouse click is posted at screen coordinates. In hidden/headless mode, an NSEvent is injected directly into the WKWebView (still isTrusted:true). Both modes work reliably for concurrent instances. New target tabs are returned as openedTabId/openedTabs.

Body
{ "selector": "#submit-btn" } // or { "x": 150, "y": 300, "button": "left", "doubleClick": false }
Response
{ "success": true, "native": true, "x": 150, "y": 300, "screenX": 650, "screenY": 400 }
Curl
curl -X POST -H "X-API-Key: YOUR_KEY" -H "Content-Type: application/json" \ -d '{"selector": "#submit-btn"}' \ http://127.0.0.1:1306/api/tabs/TAB_ID/native/click
POST/api/tabs/:id/native/typeNative keyboard typing▶

Type text character by character using real keyboard events injected directly into the tab's WKWebView — identical in headed, hidden and headless modes. Each keystroke produces isTrusted: true. No window activation, focus stealing or Accessibility permission is involved, so keystrokes can never land in the URL bar or another app, and no per-key system sounds fire. Newlines are sent as Enter key presses. The optional selector focuses the element first and verifies that an editable element took focus (retrying with a trusted native click for React/contenteditable editors); the result is reported as focused, with a warning when nothing editable is focused. Delay controls milliseconds between keystrokes.

Body
{ "text": "hello world", "selector": "#search", "delay": 50 }
Response
{ "success": true, "native": true, "length": 11, "method": "webview-event", "focused": true }
POST/api/tabs/:id/native/pressNative key press with modifiers▶

Press a key with optional modifiers using a real keyboard event injected directly into the tab's WKWebView (isTrusted: true, identical in all modes — no window activation or Accessibility permission). Supports: Enter, Tab, Escape, Backspace, Space, Arrow keys, F1-F12, Home, End, PageUp, PageDown, all letters and digits. Modifiers: Command, Control, Shift, Alt/Option.

Body
{ "key": "Enter", "modifiers": ["Command"] }
Response
{ "success": true, "native": true, "key": "Enter", "modifiers": ["Command"], "method": "webview-event" }
POST/api/tabs/:id/native/pasteNative paste via clipboard + Cmd+V▶

Sets the system clipboard with text (and optionally HTML), then injects a real Cmd+V keystroke directly into the tab's WKWebView (isTrusted: true, identical in all modes — no window activation). Your clipboard is saved and restored automatically, and pasteboard access is serialized so 20+ concurrent instances can't collide or hang. The optional selector focuses the element first and reports focused back.

Body
{ "text": "Hello", "html": "<b>Hello</b>", "selector": ".editor" }
Response
{ "success": true, "native": true, "length": 5, "hasHtml": true, "method": "webview-paste", "focused": true }
POST/api/tabs/:id/native/moveNative mouse move with interpolation▶

Move the mouse cursor using OS events with smooth interpolation (ease-in-out). Steps controls intermediate points, duration is total time in ms.

Body
{ "selector": "#target", "steps": 10, "duration": 200 } // or { "x": 300, "y": 400 }
Response
{ "success": true, "native": true, "x": 300, "y": 400 }

localStorage / sessionStorage

POST/api/tabs/:id/storage/local/getGet localStorage value▶

Get a value from localStorage.

Body
{ "key": "token" }
POST/api/tabs/:id/storage/local/setSet localStorage value▶
Body
{ "key": "token", "value": "abc123" }
POST/api/tabs/:id/storage/local/removeRemove localStorage key▶

Remove a key from localStorage.

POST/api/tabs/:id/storage/local/clearClear all localStorage▶

Clear all localStorage entries.

POST/api/tabs/:id/storage/session/getGet sessionStorage value▶

Get a value from sessionStorage.

POST/api/tabs/:id/storage/session/setSet sessionStorage value▶

Set a value in sessionStorage.

POST/api/tabs/:id/storage/session/removeRemove sessionStorage key▶

Remove a key from sessionStorage.

POST/api/tabs/:id/storage/session/clearClear all sessionStorage▶

Clear all sessionStorage entries.

Wait Functions

POST/api/tabs/:id/wait-for-functionWait for JS expression to be truthy▶

Polls a JS expression until it returns a truthy value. Max timeout: 300s.

Body
{ "expression": "document.querySelector('.loaded')", "timeout": 30000, "pollingInterval": 100 }
POST/api/tabs/:id/wait-for-timeoutWait for specified duration▶

Simple sleep/delay. Max: 300s.

Body
{ "timeout": 2000 }
POST/api/tabs/:id/wait-for-navigationWait for URL to change and page to load▶

Polls until the tab URL changes and loading completes.

POST/api/tabs/:id/wait-for-load-stateWait for page load state▶

Wait for "load", "domcontentloaded", or "networkidle" state.

Body
{ "state": "domcontentloaded" }
POST/api/tabs/:id/wait-for-urlWait for URL to match pattern▶

Polls until the tab URL contains the given pattern.

Body
{ "url": "dashboard" }

Frames

GET/api/tabs/:id/framesList all frames▶

Enumerate all frames with index, name, and URL. Cross-origin frames show "(cross-origin)".

POST/api/tabs/:id/frames/:index/executeExecute JS in frame▶

Run JavaScript in a specific same-origin frame by index. Returns 403 for cross-origin frames.

POST/api/tabs/:id/frames/:index/clickClick element in frame▶

Click an element within a same-origin child frame. Returns 403 for cross-origin frames.

Accessibility

GET/api/tabs/:id/accessibilityAccessibility tree snapshot▶

Walk the DOM tree and return roles, aria labels, text content, IDs, and class names as a nested JSON tree.

Emulation

PUT/api/tabs/:id/emulation/geolocationOverride geolocation▶

Override navigator.geolocation for the tab.

Body
{ "latitude": 40.7128, "longitude": -74.006, "accuracy": 100 }
PUT/api/tabs/:id/emulation/timezoneOverride timezone▶

Override Intl.DateTimeFormat timezone.

Body
{ "timezoneId": "America/New_York" }
PUT/api/tabs/:id/emulation/localeOverride locale▶

Override navigator.language and navigator.languages.

Body
{ "locale": "fr-FR" }
PUT/api/tabs/:id/emulation/color-schemeSet color scheme▶

Force light or dark mode for the page.

Body
{ "colorScheme": "dark" }

Dialog Handling

PUT/api/tabs/:id/dialog/configConfigure auto-response for dialogs▶

Dialogs never block the API. accept/dismiss resolve instantly; a tab in a visible window shows a non-blocking sheet that /dialog/accept and /dialog/dismiss can also resolve; tabs nobody can see (headless, hidden, API-owned) are auto-dismissed unless the action is none, which queues the dialog for the API (120 s timeout). dialog.opened / dialog.closed WebSocket events are broadcast either way.

Set how alerts/confirms/prompts are handled.

Body
{ "action": "accept", "promptText": "yes" }
GET/api/tabs/:id/dialogCheck for pending dialog▶

Check if there is a dialog waiting for resolution.

POST/api/tabs/:id/dialog/acceptAccept pending dialog▶

Accept the pending dialog (OK/Yes). Optionally provide promptText for prompt dialogs.

POST/api/tabs/:id/dialog/dismissDismiss pending dialog▶

Dismiss the pending dialog (Cancel/No).

File Chooser

Native <input type="file"> handling. Staging via /upload with paths is enough for most flows: the next picker is completed automatically. Use intercept mode for a long catalog session where clicks open pickers before you POST files.

PUT/api/tabs/:id/file-chooser/configConfigure native file picker handling▶

native shows NSOpenPanel (default in headed GUI). intercept queues the picker for the API. accept auto-completes with paths. dismiss cancels. Hidden/headless always intercept.

Body
{ "action": "intercept" }
GET/api/tabs/:id/file-chooserPending native file picker▶

Returns pending, stagedCount, armed, and action.

POST/api/tabs/:id/file-chooser/acceptComplete pending picker with files▶

Body: { "paths": ["/abs/video.mp4"] }. Same effect as /upload when a picker is already open.

POST/api/tabs/:id/file-chooser/dismissCancel pending picker▶

Dismiss the native file chooser so WebKit is not left waiting.

Network Monitoring

POST/api/tabs/:id/network/enableStart intercepting fetch/XHR requests▶

Monkey-patches fetch() and XMLHttpRequest to log all requests with URL, method, status, and timing.

GET/api/tabs/:id/network/logGet network request log▶

Returns logged requests. Use ?clear=true to clear the log after reading.

POST/api/tabs/:id/network/disableStop network monitoring▶

Clear network log and disable monitoring.

Console Capture

POST/api/tabs/:id/console/enableStart capturing console output▶

Intercepts console.log, warn, error, and info. Also captures uncaught JS errors.

GET/api/tabs/:id/console/logGet console entries▶

Returns captured console entries. Filter with ?level=error or clear with ?clear=true.

POST/api/tabs/:id/console/disableStop console capture▶

Clear console log and stop capturing.

Tracing

POST/api/tracing/startStart recording trace▶

Start a trace session. Returns a traceId to use with stop.

Body
{ "tabId": "...", "screenshots": true, "network": true }
POST/api/tracing/stopStop trace and get results▶

Stop the trace and return all recorded actions with timestamps.

Body
{ "traceId": "..." }

WebSocket Events

WS ws://host:port/api/events?apiKey=... Real-time events stream ▶

Connect via WebSocket to receive real-time browser events. API key is passed as a query parameter.

Connection
ws://127.0.0.1:1306/api/events?apiKey=YOUR_KEY
Available Events
EventDescription
tab.createdA new tab was opened, including tabs created by target-new links or window.open during API clicks
tab.closedA tab was closed
tab.activatedActive tab changed
tab.updatedTab title changed
navigation.changedURL changed in a tab
page.loadingPage started loading
page.loadedPage finished loading
page.progressPage load progress updated
page.crashedThe tab's web content process terminated (tabId, url, willReload); Vela reloads up to three times per 30 s, then sets hasCrashed
dialog.openedA JavaScript alert/confirm/prompt appeared (tabId, type, message)
dialog.closedThe dialog was resolved (tabId, type, accepted)
filechooser.openedNative file picker intercepted (pending API accept)
filechooser.closedNative file picker completed or cancelled
Event Format
{ "type": "tab.created", "data": { "tabId": "550e8400-...", "windowId": "...", "url": "https://example.com" }, "timestamp": "2025-01-15T10:30:00Z" }
JavaScript Example
const ws = new WebSocket('ws://127.0.0.1:1306/api/events?apiKey=YOUR_KEY'); ws.onmessage = (event) => { const msg = JSON.parse(event.data); console.log(msg.type, msg.data); }; ws.onopen = () => console.log('Connected'); ws.onclose = () => console.log('Disconnected');
Python Example
import asyncio, websockets, json async def listen(): uri = "ws://127.0.0.1:1306/api/events?apiKey=YOUR_KEY" async with websockets.connect(uri) as ws: async for message in ws: event = json.loads(message) print(f"{event['type']}: {event['data']}") asyncio.run(listen())

Hidden & Headless Mode

Run Vela without a visible UI on macOS — no windows, no Dock icon, just the full Automation API. Perfect for CI/CD pipelines, automated testing, web scraping, and server-side browser control.

Hidden Mode (Recommended)

Runs a full headed browser with real GUI rendering, but all windows are invisible. Sessions, cookies, and localStorage persist normally. Websites cannot detect hidden mode since it uses the exact same rendering pipeline as headed mode.

open -n -g -a Vela --args --hidden

The -g flag prevents bringing the app to the foreground. Hidden mode defaults to port 1307 (GUI uses 1306) so both APIs run without conflict. Sessions and cookies persist exactly like headed mode.

Headless Mode (Persistent)

Runs without any visible UI — no dock icon, no windows. WebKit is pre-warmed for fast startup. When a profile is used, sessions are fully persistent (cookies, localStorage, etc.) — like Chrome's headless mode. Without a profile, uses non-persistent storage (ephemeral sessions).

open -n -a Vela --args --headless

Or run the binary directly:

/Applications/Vela.app/Contents/MacOS/Vela --headless

Optional Flags

--port <number> Override API port (default: 1307 hidden/headless, 1306 normal)
--bind <address> Override bind address (default: 127.0.0.1). Use 0.0.0.0 for network access.

Stdout Output

When launched, connection details are printed to stdout for easy parsing:

Vela Hidden Mode API: http://127.0.0.1:1307 Key: aBcDeFgHiJkLmNoPqRsTuVwXyZ012345

Example: Hidden Mode Automation

# 1. Launch hidden (alongside GUI with -n -g) open -n -g -a Vela --args --hidden # 2. Create a tab and navigate curl -X POST -H "X-API-Key: YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://example.com"}' \ http://127.0.0.1:1307/api/tabs # 3. Wait for page load curl -X POST -H "X-API-Key: YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"state":"load"}' \ http://127.0.0.1:1307/api/tabs/TAB_ID/wait-for-load-state # 4. Take a screenshot curl -H "X-API-Key: YOUR_KEY" \ http://127.0.0.1:1307/api/tabs/TAB_ID/screenshot # 5. Get visible text curl -H "X-API-Key: YOUR_KEY" \ http://127.0.0.1:1307/api/tabs/TAB_ID/text

Python Example

import subprocess, requests, time # Launch Vela hidden (use -n -g to run alongside GUI without focus) proc = subprocess.Popen( ["open", "-n", "-g", "-a", "Vela", "--args", "--hidden"], stdout=subprocess.PIPE, text=True ) time.sleep(2) # Wait for server to start API = "http://127.0.0.1:1307" # Hidden/headless defaults to port 1307 KEY = "YOUR_API_KEY" headers = {"X-API-Key": KEY, "Content-Type": "application/json"} # Create tab and navigate tab = requests.post(f"{API}/api/tabs", json={"url": "https://example.com"}, headers=headers).json() # Wait for load requests.post(f"{API}/api/tabs/{tab['id']}/wait-for-load-state", json={"state": "load"}, headers=headers) # Get page text text = requests.get(f"{API}/api/tabs/{tab['id']}/text", headers=headers).json() print(text["text"])

Note: All 200+ API endpoints work identically in hidden, headless, and normal mode. Stop with pkill -f "Vela.*--hidden", pkill -f "Vela.*--headless", or kill <pid>.