Serve Mode¶
wavexis includes an HTTP API server powered by aiohttp. Start it with:
Endpoints¶
| Method | Path | Description |
|---|---|---|
| GET | /health |
Health check |
| GET | /version |
wavexis version |
| GET | /backends |
Available backends |
| POST | /screenshot |
Take a screenshot (PNG/JPEG bytes) |
| POST | /pdf |
Generate a PDF |
| POST | /eval |
Evaluate JavaScript (requires --api-key) |
| POST | /scrape |
Scrape multiple URLs |
| POST | /dom/get |
Get DOM HTML |
| POST | /dom/query |
Query DOM elements |
| POST | /navigate |
Navigate to URL |
| POST | /har |
Capture HAR data |
| POST | /cookies/get |
Get cookies |
| POST | /cookies/set |
Set a cookie |
| POST | /input/click |
Click an element |
| POST | /input/type |
Type text into an element |
| POST | /perf/metrics |
Get performance metrics |
| POST | /perf/trace |
Capture performance trace |
| POST | /cwv |
Core Web Vitals scoring (LCP, CLS, INP) |
| POST | /auth |
Apply auth context and navigate (requires --base-dir) |
| POST | /user-agent |
Set the user agent |
| POST | /headers |
Set extra HTTP headers |
| POST | /device |
Emulate a device |
| POST | /modify-request |
Modify requests in-flight |
| POST | /modify-response |
Modify responses in-flight |
| POST | /multi |
Run multiple actions (requires --base-dir) |
| GET | /plugins |
List discovered plugins |
| GET | /ws |
WebSocket event streaming (requires --api-key) |
/eval and /ws allow arbitrary JavaScript execution and are only registered
when --api-key is set. Authentication accepts an Authorization: Bearer
header or an X-API-Key header.
Rate limiting¶
Protect the HTTP API from abuse with --rate-limit:
This allows up to 60 requests per minute using a token bucket algorithm.
When the limit is exceeded, the server responds with 429 Too Many Requests
and a Retry-After header indicating when to retry.
Backend degradation¶
wavexis automatically falls back from the preferred backend to an alternative
if it cannot be created (e.g. cdpwave not installed). Use --backend to set
a preference:
wavexis serve --backend cdp # prefers cdp, falls back to bidi
wavexis serve --backend bidi # prefers bidi, falls back to cdp
Core Web Vitals¶
Measure LCP, CLS, INP with scoring via the /cwv endpoint:
curl -X POST http://localhost:8080/cwv \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "budgets": {"lcp_ms": 2500}}'
Examples¶
Screenshot¶
curl -X POST http://localhost:8080/screenshot \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}' \
-o screenshot.png
Eval¶
curl -X POST http://localhost:8080/eval \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "expression": "document.title"}'
Navigate¶
curl -X POST http://localhost:8080/navigate \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'
Server options¶
Besides --host/--port/--api-key/--rate-limit, serve accepts:
| Option | Description |
|---|---|
--backend |
Preferred backend (cdp or bidi, falls back automatically) |
--base-dir |
Restrict file-based endpoints (/multi, /auth) to a directory |
--cors-origins |
Comma-separated allowed CORS origins |
--max-concurrent |
Max concurrent browser backends (default: 5) |
--max-request-size |
Max request body size in bytes (default: 10485760) |
Architecture¶
Backends are pooled via BackendPool (bounded by --max-concurrent); each
request checks out a backend, creates it via BackendManager.select_with_fallback()
when the pool is empty, and returns it to the pool when the action finishes.
Failed backends are discarded and recreated on the next request.
WebSocket streaming¶
The /ws endpoint provides real-time streaming of browser events via WebSocket.
Subscribe¶
Connect to ws://localhost:8080/ws and send a JSON subscribe message:
{
"url": "https://example.com",
"events": ["screenshot", "console", "navigation", "dom_mutation", "perf_metrics"],
"interval": 1.0,
"format": "png",
"quality": 80
}
Streamed events¶
The server sends JSON messages with a type field:
| Type | Description |
|---|---|
ready |
Server is ready, browser launched and navigated |
screenshot |
Base64-encoded screenshot image |
console |
Console message (deduplicated) |
navigation |
URL change detected |
dom_mutation |
DOM content change detected (polled) |
perf_metrics |
Performance metrics snapshot (LCP, FCP, CLS, etc.) |
network_request |
Network request intercepted |
network_response |
Network response intercepted |
dialog |
JavaScript dialog event |
navigated |
Confirmation of client-initiated navigation |
eval_result |
Result of a client-initiated eval |
error |
Error from a stream source |
Client commands¶
Send JSON commands to control the browser:
{"action": "navigate", "url": "https://example.org"}
{"action": "eval", "expression": "document.title"}
{"action": "screenshot"}
{"action": "close"}
Available event types¶
| Event | Description | Streaming method |
|---|---|---|
screenshot |
Periodic screenshots (base64) | Polling |
console |
Console messages (deduplicated) | Polling |
navigation |
URL changes | Polling |
dom_mutation |
DOM content changes | Polling |
perf_metrics |
Performance metrics snapshots | Polling |
network_request |
Network requests | Event subscription |
network_response |
Network responses | Event subscription |
dialog |
JavaScript dialogs | Event subscription |
Python client example¶
import asyncio
import json
import aiohttp
async def stream():
async with aiohttp.ClientSession() as session:
async with session.ws_connect("ws://localhost:8080/ws") as ws:
await ws.send_json({
"url": "https://example.com",
"events": ["screenshot", "console"],
"interval": 2.0,
})
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
data = json.loads(msg.data)
if data["type"] == "screenshot":
print(f"Got screenshot ({len(data['data'])} bytes)")
elif data["type"] == "console":
print(f"Console: {data['data']}")
elif data["type"] == "ready":
print(f"Ready: {data['url']}")
asyncio.run(stream())
JavaScript client example¶
const ws = new WebSocket("ws://localhost:8080/ws");
ws.onopen = () => {
ws.send(JSON.stringify({
url: "https://example.com",
events: ["screenshot", "console", "navigation"],
interval: 1.0,
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === "screenshot") {
console.log(`Screenshot: ${data.data.length} chars`);
} else if (data.type === "console") {
console.log("Console:", data.data);
} else if (data.type === "navigation") {
console.log("Navigated to:", data.url);
}
};