Browser

Browser Infrastructure

Your browser, the Control Plane.

Orchestrate Oya Cloud, Browserbase, Steel, Anchor, Browser Use, and private Chrome behind one API. Deterministic personas, zero-rewrite failover, and sub-second live takeover.

Control Plane Architecture

Raw browser runners like Browserbase, Steel, Anchor, and Browser Use are execution targets: they spin up headless Chromium instances inside isolated containers or VMs.

Oya is the Control Plane. It sits above the execution targets and manages the state, identity, authentication, challenge resolution, and orchestration that production agent fleets require:

  • Universal Router: Exposes unified CDP (/connect), MCP, and REST interfaces. Route requests across providers with priority order and automatic failover.
  • Deterministic Personas: Mathematically seeded device profiles. Canvas, WebGL, audio, and client rects stay byte-identical across restarts, bound to a dedicated cookie jar and proxy.
  • Sign-In-Once Desktop Pairing: Transfer authenticated sessions from real desktop Chrome (with WebAuthn, passkeys, and Google SSO) to remote personas via single-use encrypted codes.
  • Two-Tier Challenges: Automatic native delegation to CAPTCHA solvers, automated TOTP and SMS/email relays, and sub-second interactive live stream handoffs for human intervention.
  • Fleet Governance: High-density console for 1,000+ browsers, real-time command activity logs, Prometheus metrics (/metrics), and hourly spend attribution per tenant key.
By decoupling the control plane from the execution engine, your agent codebase never has to know or care which cloud provider or bare-metal machine runs a session.

Why Oya: The 10x Advantage

Directly coding agents to single-vendor browser runners creates brittle architectures. Here is why an orchestrating control plane is 10x better than relying on raw point solutions:

DimensionRaw Runners (Browserbase, Steel, Anchor, Browser Use)Oya Control Plane
ArchitectureSingle-vendor lock-in. Outages or regional IP blocks halt all agents.Unified control plane. Dynamic routing across multiple providers with automatic failover.
Device IdentityEphemeral dumb sessions or random fingerprints that trigger bot-farm heuristics.Deterministic Personas. Cryptographically seeded hardware fingerprints byte-identical across restarts.
AuthenticationFragile scripted headless logins that fail on Google SSO, passkeys, and Cloudflare.Sign-In-Once Desktop Pairing. Log in once on desktop; cookies sync securely to cloud personas.
Challenges & 2FAFails or hangs on push approvals or unexpected verification prompts.Two-Tier Engine + Live Takeover. Automated TOTP/SMS relay + sub-second interactive takeover.
ObservabilityOpaque session IDs, black-box execution, post-mortem static videos.1,000+ browser console, real-time activity log, Prometheus metrics, hourly spend attribution.
Protocol FreedomProprietary SDKs and bespoke API wrappers.Universal Gateway: Native CDP (/connect), MCP streamable HTTP, TS SDK, and CLI.
Stealth TestingUnverifiable marketing claims of "undetectable" scrapers.Open benchmark suite (oya stealth-test --live) scored against CreepJS and Bot.Sannysoft.

Multi-Provider Routing & Failover

Configure providers in the dashboard under Control → Providers or via the API. Each provider has a unique route name, vendor type, priority (0 goes first), and session capacity.

Example
// Point any CDP client at the Oya Control Plane gateway:
const browser = await chromium.connectOverCDP(
  "wss://oyabrowser.com/connect?token=YOUR_OYA_KEY"
);

// Oya selects the highest-priority available provider.
// If Steel errors or hits rate limits, Oya instantly fails over to Browserbase or Oya Cloud.

When a connection attempt to an upstream vendor fails, the control plane immediately catches the error, puts the failing route into a cooldown period, and dispatches the connection to the next healthy provider in priority order. Your client application never observes a disconnect.

Stealth & Live Benchmarks

Rather than making unsubstantiated marketing claims about detection resistance, Oya includes an open testing suite that benchmarks browser evasion against real detectors:

Example
oya stealth-test            # Score local probe suite
oya stealth-test --live     # Benchmark live against Bot.Sannysoft and CreepJS

The suite tests canvas noise, WebGL renderer and vendor strings, AudioContext noise, client rects, plugins, navigator.webdriver, userAgentData, media devices, and Function.prototype.toString masking.

Oya deliberately does not double-layer custom stealth over providers that already ship tuned anti-bot stealth (Anchor, Browserbase, Steel, Browser Use). Double-masking causes internal contradictions that anti-bot heuristics detect. On those providers, Oya manages the persona identity, cookie jar, residential proxy, and concurrency limits.

Quickstart

Example
npm i @oya-ai/browser
npm i -g @oya-ai/cli && oya login && oya init
Example
import { Oya } from "@oya-ai/browser";

const oya = new Oya();                                    // OYA_API_KEY
const browser = await oya.browser.start({ persona: "auto", captcha: "auto" });
await browser.goto("https://example.com");

That is the whole surface. Which provider actually runs the browser, Oya Cloud, your own machines, Browser Use, Browserbase, Steel, Anchor, or a CDP URL you hand us, is a setting on your API key, chosen once during . Your code never branches on it.

The API key is the identity for everything: browsers, personas, cookies, settings, usage and audit history are all scoped to it, and one key can never see another's.

SDK

@oya-ai/browser is TypeScript with no runtime dependencies, shipped as ESM, CJS and types. Element IDs come from analyze() and are only valid until the page changes, after a navigation or a click that redraws, analyze again.

Example
const page = await browser.analyze();      // markdown + numbered elements ({ format: 'toon' } for TOON)
const els  = await browser.elements();     // just the visible ones

await browser.click(13);
await browser.type(9, "hello");
await browser.pressKey("Enter");
await browser.waitFor("[data-testid=results]");
await browser.scroll("bottom");

const png = await browser.screenshot();    // base64
const answer = await browser.ask("find the pricing page");

await browser.solveCaptcha();              // { solved, method }
await browser.completeMfa();               // { completed, method, liveViewUrl }
await browser.close();

Bring your own tools

browser.cdpUrl is our gateway URL, not the vendor's, point Playwright, Puppeteer, Stagehand or browser-use at it and you get routing, profile capture and session recording without any of them knowing this exists.

Example
const browser = await oya.browser.start();
const pw = await chromium.connectOverCDP(browser.cdpUrl);

The gateway also answers /json/version and /json/list, which is what lets those clients treat it as an ordinary browser.

CLI

Example
oya login                       Save an API key for this machine
oya init                        Model, browser provider, solver, desktop sign-in
oya start [--persona auto]      Start a browser and print its id
oya goto <url>                  Navigate (defaults to the newest browser)
oya ask "<prompt>"              Drive it in plain language
oya ls                          What is running
oya rm <id> | --all             Stop browsers
oya personas [new|rm <id>]      Identities and their concurrency
oya open                        Watch a browser work
oya config [key=value ...]      This key's settings
oya usage                       What this key has spent
oya stealth-test [--live]       Score this deployment against bot detectors

Flags and OYA_API_KEY / OYA_BASE_URL beat the saved file, so CI never needs oya login. The key is stored at ~/.oya/config.json, mode 600.

Create API Key

Go to the dashboard. Open the API key menu to create or select a key for your workspace.

Your key is scoped: you only see browsers connected with your key. Other users' browsers are invisible to you.

Save your key somewhere safe. If you lose it, you'll need to generate a new one. The old key still works for any browsers already connected with it.

Desktop Sign-in

For browsers on Oya infrastructure, the desktop app is a one-time step: log into the sites your agents need, and those cookies move to the remote browsers, which run the same fingerprint as that identity. The agent arrives already signed in, and the site sees one device returning rather than a fleet sharing an account.

Onboarding and Settings both have an Open the desktop browser button. It builds an oya:// link carrying a single-use pairing code, never your API key, because a protocol URL is reachable by any page you visit and lands in OS logs on the way. The app exchanges that code over HTTPS with the server the link names.

The desktop app asks before connecting, naming the destination host, with Cancel as the default. Connecting shares that browser's cookies and logged-in sessions with the control plane it dials, so if a web page opened the dialog rather than your own dashboard, cancel it.
PlatformDownload
macOS (Intel + Apple Silicon)Oya Browser.dmg
Windows (x64)Oya Browser.exe
Linux (x64)Oya Browser.AppImage

macOS: Open the .dmg and drag the app to Applications. The build is signed and notarized, so it opens normally. If an older download is blocked, right-click the app → Open → Open.

Linux: chmod +x the AppImage and run it.

Running multiple instances

To open multiple browser windows (e.g. different accounts or different API keys):

Example
# macOS, open another instance
open -n "/Applications/Oya Browser.app"

# With separate sessions (own cookies, own config)
open -n "/Applications/Oya Browser.app" --args --user-data-dir=/tmp/oya-2
open -n "/Applications/Oya Browser.app" --args --user-data-dir=/tmp/oya-3

# Linux
./Oya-Browser.AppImage --user-data-dir=/tmp/oya-2

Each --user-data-dir gets its own cookies, logins, and config, fully isolated sessions.

Connect

Open Oya Browser. The setup screen appears on first launch.

FieldValue
Server URLwss://oyabrowser.com/ws
API KeyThe key you generated in the dashboard
Browser NameOptional, how it shows in the dashboard

Click Connect. The green dot in the toolbar confirms the connection. Your browser now appears in the dashboard.

MCP Setup

One endpoint for everything you have. It starts browsers itself, so there is no id to look up first:

Example
https://oyabrowser.com/mcp/pool

Seventeen tools: fourteen that act on a page, round-robined across your browsers, plus start_browser, stop_browser and pool_status. Tab tools and read_elements are not on this endpoint; to use those, point at one browser instead, /mcp/{BROWSER_ID}, with the id from the dashboard, which serves nineteen.

Cursor

Add to .cursor/mcp.json in your project:

Example
{
  "mcpServers": {
    "oya-browser": {
      "url": "https://oyabrowser.com/mcp/pool",
      "transport": "streamable-http",
      "headers": {
        "Authorization": "Bearer YOUR_API_KEY"
      }
    }
  }
}

Claude Desktop

Add to Claude Desktop's MCP config (Settings → Developer → Edit Config):

Example
{
  "mcpServers": {
    "oya-browser": {
      "url": "https://oyabrowser.com/mcp/pool",
      "transport": "streamable-http",
      "headers": {
        "Authorization": "Bearer YOUR_API_KEY"
      }
    }
  }
}

Claude Code

Same config in .claude/mcp.json, or install the plugin, which brings the MCP server and a skill that teaches the workflow:

Example
claude plugin marketplace add OyadotAI/oya-browser
claude plugin install oya-browser@oya

# any agent that reads skills, without the plugin:
npx skills add OyadotAI/oya-browser

analyze_page

Analyzes the current page. Returns the full page with every interactive element numbered, as markdown (the default), TOON or JSONL. The default is the one picked in the Oya Browser's settings, unless the server's OYA_PAGE_FORMAT pins one.

Example
analyze_page()                  // markdown
analyze_page({ format: 'toon' }) // TOON: fewer tokens
analyze_page({ format: 'jsonl' }) // one JSON object per line

Returns:

  • Page metadata, URL, title, viewport size, scroll position
  • Full page content with element tags like [#5 button "Submit"] in markdown, or one blocks[N]{id,region,kind,text,target,state} row per block in TOON, or one JSON object per block in JSONL
  • Element index, all elements listed with IDs, types, labels, visibility flags
Always call analyze_page before using click or type. Element IDs only exist after analysis and reset on every call.

Navigate the browser to a URL.

Example
navigate({ url: "https://example.com" })
After navigating, call analyze_page again, old element IDs are invalid on the new page.

click

Click an interactive element by its ID number from analyze_page.

Example
click({ element_id: 13 })

The element was tagged with data-ac-id="13" during analysis, so the click resolves via a single querySelector.

type

Type text into an input element. Clears existing content first, then types character by character with realistic key events.

Example
type({ element_id: 9, text: "hello world" })

press_key

Press a keyboard key. Useful for submitting forms (Enter), dismissing dialogs (Escape), or navigating (Tab, arrows).

Example
press_key({ key: "Enter" })

Supported keys: Enter, Escape, Tab, Backspace, ArrowDown, ArrowUp, or any character.

screenshot

Capture the visible tab as a base64 PNG image.

Example
screenshot()

scroll

Scroll the page up or down.

Example
scroll({ direction: "down", amount: 500 })
ParamTypeDescription
direction"up" | "down"Scroll direction
amountnumber (optional)Pixels to scroll, default 500

Tab Management

list_tabs

List all open tabs with ID, title, URL, and which is active.

Example
list_tabs()

open_tab

Open a new tab, optionally at a URL.

Example
open_tab({ url: "https://gmail.com" })

switch_tab

Switch to a tab by ID (from list_tabs).

Example
switch_tab({ tab_id: 2 })

close_tab

Close a tab. Closes the active tab if no ID specified.

Example
close_tab({ tab_id: 3 })

wait

Wait for an element matching a CSS selector to appear on the page.

Example
wait({ selector: ".results", timeout: 10000 })

Personas

A persona is one identity: a fingerprint, a cookie jar and a proxy, bound together and stable for its life. One persona is one device.

There are two ways to get caught, and they are mirror images of each other:

ShapeSignal
One account seen from many device fingerprintsTextbook bot farm
One device fingerprint across many accounts, or 1,000 concurrent sessionsDevice farm

Binding the fingerprint to your API key avoids the first and walks straight into the second. Binding it to each browser avoids the second and walks into the first. So the binding sits at the level that actually corresponds to a device:

Example
persona = fingerprint + cookie jar + proxy       # one identity, one device
API key = a group of personas                    # your fleet

A persona's fingerprint is derived from a stored seed, so it is byte-identical across restarts, a returning session looks like a returning device, not a new one.

Example
const p = await oya.personas.create({ name: "acme-ops" });
const browser = await oya.browser.start({ persona: p.id });

await oya.personas.list();     // includes activeBrowsers and maxConcurrent
await oya.personas.remove(p.id);
Every API key has a default persona whose seed reproduces the fingerprint that key had before personas existed. If you run a single account, nothing changed for you.

Rotation and concurrency

Rotation means picking a different persona, never giving one persona a new fingerprint. persona: 'auto' selects the least recently used persona that is still under its concurrency cap.

Concurrency is capped per persona, because one laptop cannot be in a thousand places at once. Named personas default to 2 (a phone and a laptop is plausible); the default persona is uncapped so an existing fleet does not break on upgrade. Past the cap you get a clear 429 rather than a silent breach, and activeBrowsers is visible in the dashboard and as a Prometheus metric.

CAPTCHA

Example
await browser.solveCaptcha();                    // explicit
oya.browser.start({ captcha: 'auto' });          // solve as they appear

Detects reCAPTCHA v2/v3, hCaptcha and Turnstile. Providers that solve natively, Anchor, Browserbase, Steel, Browser Use, are left to do it rather than paying twice and racing their attempt. Everything else goes to your configured solver (CapSolver or 2Captcha).

Returns { solved, method: 'provider' | 'solver' | 'none' }. A failure returns solved: false: a silent no-op that leaves an agent stuck is worse than a clear answer.

Automated solving conflicts with some sites' terms of service. Sessions that used it are recorded in the audit trail so you can see which.

MFA

Example
await oya.personas.setMfa(id, { type: 'totp', secret: 'JBSWY3DPEHPK3PXP' });
await oya.personas.setMfa(id, { type: 'email', url: 'https://mail.example/api/latest' });

const r = await browser.completeMfa();
if (!r.completed) open(r.liveViewUrl);   // finish it by hand

TOTP is generated locally (RFC 6238). Email and SMS one-time codes are polled from a relay endpoint you supply, within a bounded window, because the code does not exist yet when the prompt appears. When nothing automated can answer, liveViewUrl is where a person finishes; that is also the only workable answer for push-approval MFA.

TOTP seeds are credential material of the same weight as a password: sealed at rest with AES-256-GCM, audited on use, and never returned by the API. The relay URL is checked against private and link-local ranges when you store it and on every poll, because a public name says nothing about where it resolves later.

Anonymity

Create and manage browser profiles with unique fingerprints, proxy routing, and isolated cookie stores. Each profile is a complete identity, different canvas hash, WebGL renderer, navigator properties, and session storage. Switch identities with a single MCP call.

Every browser runs as a persona: a fingerprint, cookie jar and proxy bound together and stable for its life. Rotation means choosing a different persona, never re-rolling one.

Fingerprint Spoofing

Each profile generates a coherent set of browser fingerprints that are internally consistent per platform. A Win32 profile gets Windows GPU strings, Windows fonts, and matching screen resolutions.

  • Canvas: deterministic pixel noise on toDataURL and toBlob
  • WebGL: spoofed vendor/renderer strings from real GPU database
  • AudioContext: noise on OfflineAudioContext.startRendering
  • ClientRects: sub-pixel noise on getBoundingClientRect (bypassed internally for click accuracy)
  • Navigator: platform, hardwareConcurrency, deviceMemory, languages, vendor
  • Screen: width, height, colorDepth, devicePixelRatio
  • WebRTC: ICE candidates stripped to prevent local IP leak
  • Fonts: platform-consistent font sets

Proxy Support

A persona can take an HTTP, HTTPS or SOCKS5 proxy, given as one url. Chromium cannot authenticate to a SOCKS5 proxy, so a proxy that needs a username and password must be HTTP or HTTPS. The proxy is applied at the session level, so all traffic routes through it, including DNS for SOCKS5. Timezone and locale are matched to the proxy's location over CDP, and a mismatch is reported rather than silently shipped.

Example
const proxy = await oya.proxies.create({
  url: "http://user:pass@1.2.3.4:8080",
  geo: "us",
  kind: "datacenter",
});

const persona = await oya.personas.create({
  name: "us-desktop",
  prefs: { platform: "Win32", timezone: "America/New_York" },
});
await oya.personas.pinProxy(persona.id, proxy.id);

Anti-Detection Stealth

Always active, no configuration needed. The stealth layer removes automation indicators that anti-bot systems check for:

  • navigator.webdriver removed
  • Electron globals (window.process, window.require) deleted
  • window.chrome fixed to match real Chrome (app, runtime, csi, loadTimes)
  • navigator.plugins populated with PDF viewers
  • navigator.permissions.query patched
  • Sec-CH-UA headers rewritten to hide Electron
  • Google telemetry domains blocked at the network level

Choosing a persona

A persona is made through the API and chosen when a browser starts. There is no tool that switches one mid-session: the device, cookie jar and proxy are bound together for the persona's life, so changing them would make the browser a different machine halfway through a run. To vary the device, clone the persona.

Example
const personas = await oya.personas.list();
const browser = await oya.browser.start({ persona: personas[0].id });

// A new device, fixed from here on:
const fresh = await oya.personas.create({ prefs: { platform: "MacIntel" } });

// The same device, a second cookie jar:
const twin = await oya.personas.clone(fresh.id);
The console calls this workspace Profiles; the API and these docs call it a persona. Same thing.

Dashboard

Browsers, commands, and CDP sessions
A browser is a running desktop or cloud instance. REST commands, including curl requests to /api/browsers/:id/command, appear in that browser’s Activity history and count toward Usage. A CDP session is a persistent client connection through /connect, typically from Playwright or Puppeteer. Find these under Control → CDP sessions.

The dashboard at /dashboard is the control panel. It shows your connected browsers and lets you interact with them.

Built to hold a thousand browsers and let you act on any one of them:

  • Browsers: a health strip (every number is a filter) over a dense table: health, persona, provider, current page, commands · errors, seen, uptime. Select a row to open the panel: URL bar, a bounded interactive live view, screenshot, elements, stats, and the activity log, what that browser has been doing, newest first.
  • Personas: one identity each. Create with a chosen device and a live fingerprint preview; edit name, cap, proxy pin and MFA; the device itself is locked, with Clone for when you want a different one.
  • Control: health, gateway sessions, providers and routing, per-key usage, the audit trail, recordings.

Adding a provider

Open Control → Providers → Add provider. Give the route a unique name, choose a vendor, and enter its API key. A credential already saved in Settings can be reused. For your own Chrome, supply its CDP WebSocket URL instead.

Set the session capacity and routing priority (0 goes first). Providers and your routing strategy are saved for your Oya key across restarts; credentials and connection URLs are encrypted. Saving a provider does not launch a browser or verify its credentials. Its first connection does that. End active sessions before removing a route.

These routes serve new CDP connections to /connect?token=YOUR_OYA_KEY. The Start browser action uses your provider selection in Settings → Browsers. Attaching with ?browser=ID connects to that existing browser.

Driving a browser from the live view

Choose Stream in a browser panel, or Open live stream in a tab from its menu, to open an interactive viewer in a separate tab. Your dashboard key authorizes the viewer. The /api/live/:id endpoint is the raw event stream for integrations.

Click to control. Clicks land at the page pixel under the cursor, a drag is a drag, the wheel scrolls, typing is batched into keyboard_type and the named keys go as press_key. Esc hands the keyboard back. What was typed is never written to the activity log, it records 2 chars, not the text.

Connect to a browser that is already running

Right-click any row (or press Connect in the panel) for code that targets that exact browser: SDK, CLI, an MCP config, curl, and for CDP-backed browsers a Playwright connectOverCDP URL. Snippets are written for this deployment and your key; the key is masked until you ask, and copy always copies the real one.

Example
// Attach through the gateway to one browser in the fleet. Closing your
// client leaves the browser running.
const browser = await chromium.connectOverCDP(
  "wss://<host>/connect?token=<api-key>&browser=<browser-id>",
);

Only CDP-backed browsers (Browserbase, Steel, Anchor, your own Chrome) have an endpoint to attach to; an Oya client is driven over its own socket, so use the SDK, CLI or MCP for those.

Stop means stop

One button, one endpoint (POST /browsers/:id/stop). A cloud browser's sandbox is destroyed so billing ends; a CDP browser is handed back to its provider; a desktop browser disconnects. The confirm says which. Bulk stop takes {ids: [...]} or {all: true}.

Keyboard

KeyDoes
⌘/Ctrl 1 · 2 · 3Browsers · Personas · Control
nStart a browser
/Filter the fleet
↑ ↓ or j kMove the selection
xStop the selected browser(s)
l · r · sURL bar · reload · screenshot
EscClose the panel, or release the keyboard from the live view
?The full list

You can sign in with an account, or by pasting an API key: a self-hosted deployment with API_KEYS and no database has no accounts, and still needs its own UI.

Onboarding

A key that has not been set up gets a four-step wizard. Everything it asks is stored against that key, nothing lands in an environment variable, and nothing is inherited from an account.

  1. Model: Claude or OpenAI, your key, your default model
  2. Browsers: Oya Cloud, Oya self-hosted, Browser Use, Browserbase, Steel, Anchor, or your own CDP URL
  3. Challenges: a CAPTCHA solver, or none
  4. Sign in: one click into the desktop browser, for Oya providers only

The same choices are available any time from Settings, and oya init walks the identical flow in a terminal.

Dev Panel (Desktop App)

The desktop app's dev panel ({} button in the toolbar) has four tabs:

  • Chat: natural language browser control with formatted responses and tool badges
  • Actions: quick-fire buttons and input fields for every command: analyze, screenshot, navigate, click by element #, type, press keys, hover, scroll, wait, tab management
  • Network: live WebSocket traffic with IN/OUT badges, expandable payloads, filter by direction or type (All, In, Out, Commands, Results)
  • Source: view the page as AI sees it: toggle between Markdown (analyzePage output) and HTML source, refresh on demand

Live View

Select a browser on the Browsers tab to watch it work. Frames stream as JPEG over SSE at ~2fps. browser.liveViewUrl() is the console deep link for a person to open; await browser.liveStreamUrl() gives you the same frames to embed, with a single-use ticket that expires in 60 seconds, EventSource cannot set headers, and a URL that ends up in browser history should not be a permanent credential.

Settings

The gear icon next to the API key bar. Everything here belongs to that key: model provider and credential, default model, browser provider and its credential, CAPTCHA solver, and the one-click desktop sign-in.

Credentials are sealed at rest with AES-256-GCM and always read back masked. Saving the masked placeholder never overwrites the real value.

A key that has set nothing falls back to the deployment-wide defaults. Changing those affects every key that has not set its own, so it needs OYA_OPERATOR_TOKEN via POST /config/host rather than any API key.

REST API

All endpoints require Authorization: Bearer YOUR_API_KEY header (except health and register). Interactive API testing available at /swagger.

MethodEndpointDescription
GET/healthServer status + browser count
POST/register-keyRegister a new API key ({ "key": "..." })
GET/browsersList your connected browsers
POST/browsers/startStart one ({ "persona": "auto" }), provider comes from your key
GET/browsers/:idOne browser with counters, health and its recent activity
POST/browsers/:id/stopStop it, destroys a cloud sandbox, releases a CDP session
POST/browsers/stopBulk: { "ids": [...] } or { "all": true }
GET/fleetTotals by health, provider and persona; usage and limits
POST/browsers/:id/commandSend command ({ "action": "...", "params": {} })
POST/browsers/:id/chatChat ({ "messages": [...] })
GET/live/:id?ticket=...SSE live view frame stream (single-use ticket)
GET/POST/mcp/:idMCP Streamable HTTP endpoint
GET/POST/personasList or create personas
DELETE/personas/:idDelete a persona (409 while in use)
PUT/personas/:idRename, set the cap or the proxy hint, never the device
POST/personas/:id/cloneA new persona of the same kind of device
POST/personas/previewThe fingerprint a set of choices would produce
GET/personas/optionsPlatforms and their coherent timezones and locales
PUT/personas/:id/mfaStore a second factor
POST/browsers/:id/captchaDetect and clear a CAPTCHA
POST/browsers/:id/mfaAnswer an MFA prompt
GET/usageThis key's usage, bucketed by hour
GET/auditThis key's audit history
GET/configThis key's settings, credentials masked
POST/configUpdate this key's settings

Command API Reference

Send commands via POST /browsers/:id/command. Each action uses only specific params, the rest are ignored.

Navigation Actions

ActionParamsDescription
navigateurl (required)Navigate to a URL
open_taburl (optional)Open a new tab
switch_tabtab_id (required)Activate a tab by ID
close_tabtab_id (optional, defaults to active)Close a tab
list_tabsnoneList all open tabs

Page Analysis Actions

ActionParamsDescription
analyzeformat?Full page + numbered elements, as markdown (default), toon or jsonl
read_pageselector (optional), limit (default 50)Lightweight element listing
screenshotnoneCapture page as PNG

Interaction Actions

ActionParamsDescription
clickselector (e.g. [data-ac-id="3"])Click an element
typeselector + textType into an input
press_keykey (e.g. Enter, Tab, Escape)Press a keyboard key
scrolldirection (up/down), amount (px, default 500)Scroll the page
waitselector, timeout (ms, default 10000)Wait for element to appear

Examples

Example
// Navigate to a page
{ "action": "navigate", "params": { "url": "https://google.com" } }

// Analyze current page (no params needed)
{ "action": "analyze" }

// Click element #3 from analyze results
{ "action": "click", "params": { "selector": "[data-ac-id=\"3\"]" } }

// Type into element #9
{ "action": "type", "params": { "selector": "[data-ac-id=\"9\"]", "text": "hello world" } }

// Press Enter
{ "action": "press_key", "params": { "key": "Enter" } }

// Scroll down
{ "action": "scroll", "params": { "direction": "down", "amount": 500 } }

// Screenshot (no params needed)
{ "action": "screenshot" }

// List all tabs
{ "action": "list_tabs" }

// Open new tab
{ "action": "open_tab", "params": { "url": "https://gmail.com" } }

// Switch to tab
{ "action": "switch_tab", "params": { "tab_id": 2 } }

// Close tab (omit tab_id to close active tab)
{ "action": "close_tab", "params": { "tab_id": 3 } }

// Wait for element
{ "action": "wait", "params": { "selector": ".results", "timeout": 10000 } }

// Read page elements (lightweight)
{ "action": "read_page", "params": { "limit": 20 } }

Typical Workflow

Example
1. navigate → go to the page
2. analyze  → understand the page, get element IDs
3. click / type / press_key / scroll → interact
4. analyze  → re-analyze after page changes (old IDs are invalid)
5. repeat until task is done

WebSocket Protocol

Browsers connect via WebSocket at wss://oyabrowser.com/ws.

Auth

First message from browser:

Example
{ "type": "auth", "api_key": "...", "browser_id": "...", "browser_name": "..." }

Server responds:

Example
{ "type": "auth_ok", "browser_id": "..." }

Commands

Server → Browser:

Example
{ "type": "cmd", "id": "uuid", "action": "analyze", "params": {} }

Browser → Server:

Example
{ "type": "cmd_result", "id": "uuid", "ok": true, "data": { ... } }

Ping/Pong

Both sides send { "type": "ping" } and respond with { "type": "pong" } every 15-20 seconds.

Live Stream

Server → Browser: { "type": "stream_start", "fps": 2 }

Browser → Server: { "type": "frame", "data": "data:image/jpeg;base64,..." }

Server → Browser: { "type": "stream_stop" }