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.
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:
| Dimension | Raw Runners (Browserbase, Steel, Anchor, Browser Use) | Oya Control Plane |
|---|---|---|
| Architecture | Single-vendor lock-in. Outages or regional IP blocks halt all agents. | Unified control plane. Dynamic routing across multiple providers with automatic failover. |
| Device Identity | Ephemeral dumb sessions or random fingerprints that trigger bot-farm heuristics. | Deterministic Personas. Cryptographically seeded hardware fingerprints byte-identical across restarts. |
| Authentication | Fragile 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 & 2FA | Fails or hangs on push approvals or unexpected verification prompts. | Two-Tier Engine + Live Takeover. Automated TOTP/SMS relay + sub-second interactive takeover. |
| Observability | Opaque session IDs, black-box execution, post-mortem static videos. | 1,000+ browser console, real-time activity log, Prometheus metrics, hourly spend attribution. |
| Protocol Freedom | Proprietary SDKs and bespoke API wrappers. | Universal Gateway: Native CDP (/connect), MCP streamable HTTP, TS SDK, and CLI. |
| Stealth Testing | Unverifiable 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.
// 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:
oya stealth-test # Score local probe suite
oya stealth-test --live # Benchmark live against Bot.Sannysoft and CreepJSThe suite tests canvas noise, WebGL renderer and vendor strings, AudioContext noise, client rects, plugins, navigator.webdriver, userAgentData, media devices, and Function.prototype.toString masking.
Quickstart
npm i @oya-ai/browser
npm i -g @oya-ai/cli && oya login && oya initimport { 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.
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.
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.
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
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 detectorsFlags 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.
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.
| Platform | Download |
|---|---|
| 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):
# 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-2Each --user-data-dir gets its own cookies, logins, and config, fully isolated sessions.
Connect
Open Oya Browser. The setup screen appears on first launch.
| Field | Value |
|---|---|
| Server URL | wss://oyabrowser.com/ws |
| API Key | The key you generated in the dashboard |
| Browser Name | Optional, 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:
https://oyabrowser.com/mcp/poolSeventeen 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:
{
"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):
{
"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:
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-browseranalyze_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.
analyze_page() // markdown
analyze_page({ format: 'toon' }) // TOON: fewer tokens
analyze_page({ format: 'jsonl' }) // one JSON object per lineReturns:
- Page metadata, URL, title, viewport size, scroll position
- Full page content with element tags like
[#5 button "Submit"]in markdown, or oneblocks[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
analyze_page before using click or type. Element IDs only exist after analysis and reset on every call.navigate
Navigate the browser to a URL.
navigate({ url: "https://example.com" })analyze_page again, old element IDs are invalid on the new page.click
Click an interactive element by its ID number from analyze_page.
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.
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).
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.
screenshot()scroll
Scroll the page up or down.
scroll({ direction: "down", amount: 500 })| Param | Type | Description |
|---|---|---|
direction | "up" | "down" | Scroll direction |
amount | number (optional) | Pixels to scroll, default 500 |
Tab Management
list_tabs
List all open tabs with ID, title, URL, and which is active.
list_tabs()open_tab
Open a new tab, optionally at a URL.
open_tab({ url: "https://gmail.com" })switch_tab
Switch to a tab by ID (from list_tabs).
switch_tab({ tab_id: 2 })close_tab
Close a tab. Closes the active tab if no ID specified.
close_tab({ tab_id: 3 })wait
Wait for an element matching a CSS selector to appear on the page.
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:
| Shape | Signal |
|---|---|
| One account seen from many device fingerprints | Textbook bot farm |
| One device fingerprint across many accounts, or 1,000 concurrent sessions | Device 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:
persona = fingerprint + cookie jar + proxy # one identity, one device
API key = a group of personas # your fleetA 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.
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);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
await browser.solveCaptcha(); // explicit
oya.browser.start({ captcha: 'auto' }); // solve as they appearDetects 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.
MFA
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 handTOTP 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.
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.
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
toDataURLandtoBlob - 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.
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.webdriverremoved- Electron globals (
window.process,window.require) deleted window.chromefixed to match real Chrome (app, runtime, csi, loadTimes)navigator.pluginspopulated with PDF viewersnavigator.permissions.querypatched- 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.
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);Dashboard
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.
// 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
| Key | Does |
|---|---|
| ⌘/Ctrl 1 · 2 · 3 | Browsers · Personas · Control |
| n | Start a browser |
| / | Filter the fleet |
| ↑ ↓ or j k | Move the selection |
| x | Stop the selected browser(s) |
| l · r · s | URL bar · reload · screenshot |
| Esc | Close 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.
- Model: Claude or OpenAI, your key, your default model
- Browsers: Oya Cloud, Oya self-hosted, Browser Use, Browserbase, Steel, Anchor, or your own CDP URL
- Challenges: a CAPTCHA solver, or none
- 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.
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.
| Method | Endpoint | Description |
|---|---|---|
GET | /health | Server status + browser count |
POST | /register-key | Register a new API key ({ "key": "..." }) |
GET | /browsers | List your connected browsers |
POST | /browsers/start | Start one ({ "persona": "auto" }), provider comes from your key |
GET | /browsers/:id | One browser with counters, health and its recent activity |
POST | /browsers/:id/stop | Stop it, destroys a cloud sandbox, releases a CDP session |
POST | /browsers/stop | Bulk: { "ids": [...] } or { "all": true } |
GET | /fleet | Totals by health, provider and persona; usage and limits |
POST | /browsers/:id/command | Send command ({ "action": "...", "params": {} }) |
POST | /browsers/:id/chat | Chat ({ "messages": [...] }) |
GET | /live/:id?ticket=... | SSE live view frame stream (single-use ticket) |
GET/POST | /mcp/:id | MCP Streamable HTTP endpoint |
GET/POST | /personas | List or create personas |
DELETE | /personas/:id | Delete a persona (409 while in use) |
PUT | /personas/:id | Rename, set the cap or the proxy hint, never the device |
POST | /personas/:id/clone | A new persona of the same kind of device |
POST | /personas/preview | The fingerprint a set of choices would produce |
GET | /personas/options | Platforms and their coherent timezones and locales |
PUT | /personas/:id/mfa | Store a second factor |
POST | /browsers/:id/captcha | Detect and clear a CAPTCHA |
POST | /browsers/:id/mfa | Answer an MFA prompt |
GET | /usage | This key's usage, bucketed by hour |
GET | /audit | This key's audit history |
GET | /config | This key's settings, credentials masked |
POST | /config | Update 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
| Action | Params | Description |
|---|---|---|
navigate | url (required) | Navigate to a URL |
open_tab | url (optional) | Open a new tab |
switch_tab | tab_id (required) | Activate a tab by ID |
close_tab | tab_id (optional, defaults to active) | Close a tab |
list_tabs | none | List all open tabs |
Page Analysis Actions
| Action | Params | Description |
|---|---|---|
analyze | format? | Full page + numbered elements, as markdown (default), toon or jsonl |
read_page | selector (optional), limit (default 50) | Lightweight element listing |
screenshot | none | Capture page as PNG |
Interaction Actions
| Action | Params | Description |
|---|---|---|
click | selector (e.g. [data-ac-id="3"]) | Click an element |
type | selector + text | Type into an input |
press_key | key (e.g. Enter, Tab, Escape) | Press a keyboard key |
scroll | direction (up/down), amount (px, default 500) | Scroll the page |
wait | selector, timeout (ms, default 10000) | Wait for element to appear |
Examples
// 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
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 doneWebSocket Protocol
Browsers connect via WebSocket at wss://oyabrowser.com/ws.
Auth
First message from browser:
{ "type": "auth", "api_key": "...", "browser_id": "...", "browser_name": "..." }Server responds:
{ "type": "auth_ok", "browser_id": "..." }Commands
Server → Browser:
{ "type": "cmd", "id": "uuid", "action": "analyze", "params": {} }Browser → Server:
{ "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" }