Use REST /v1/do/* when
- Your orchestrator speaks HTTP
- You want one action per request
- You need queue-friendly browser jobs
Our /v1/do/* endpoints turn full browser automation into stateless REST calls. Open a browser, navigate, click, type, and extract DOMs directly from any orchestrator that speaks HTTP.
POST /v1/do/openPOST /v1/do/navigatePOST /v1/do/markdownPOST /v1/do/closeHumanized REST simplifies browser control without hiding its power. It runs on the exact same infrastructure as our MCP server and Playwright WebSockets, just exposed for HTTP-first agents.
Pointer, keyboard, and form actions humanize physical interaction by default, including modality-aware mouse or touch behavior and natural timing. For the supported actions, pass humanize: false to delegate the complete action once to raw upstream Playwright.
Pass JSON in, get JSON out. Authenticate via Bearer token. Grab a sessionId from result.sessionId, then include it on subsequent requests. Every success uses { result, context, artifacts? }; screenshots, PDFs, and other binary outputs use public-safe base64 artifacts instead of service paths.
const API_BASE = new URL('https://api.browser.city/v1/do/');const apiKey = process.env.BROWSERCITY_API_KEY;async function doAction(action: string, body: unknown) { const url = new URL(encodeURIComponent(action), API_BASE); const response = await fetch(url, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); if (!response.ok) { throw new Error(['Humanized REST action failed:', response.status].join(' ')); } return response.json();}const session = await doAction('open', { browser: 'chromium' });try { await doAction('navigate', { sessionId: session.result.sessionId, url: 'https://example.com' }); const markdown = await doAction('markdown', { sessionId: session.result.sessionId }); console.log(markdown.result.text); const screenshot = await doAction('take_screenshot', { sessionId: session.result.sessionId, type: 'png', }); const image = screenshot.artifacts?.find((artifact: { kind: string }) => artifact.kind === 'image'); console.log(image?.mediaType, image?.dataBase64.length);} finally { await doAction('close', { sessionId: session.result.sessionId });}import osfrom urllib.parse import quoteimport requestsAPI_BASE = 'https://api.browser.city/v1/do'HEADERS = {'Authorization': f"Bearer {os.environ['BROWSERCITY_API_KEY']}"}def action_url(action: str) -> str: return f'{API_BASE}/{quote(action, safe="")}'def do_action(action: str, body: dict) -> dict: response = requests.post(action_url(action), headers=HEADERS, json=body, timeout=30) response.raise_for_status() return response.json()session = do_action('open', {'browser': 'chromium'})try: do_action('navigate', {'sessionId': session['result']['sessionId'], 'url': 'https://example.com'}) markdown = do_action('markdown', {'sessionId': session['result']['sessionId']}) print(markdown['result']['text'])finally: do_action('close', {'sessionId': session['result']['sessionId']})using System.Net.Http.Headers;using System.Net.Http.Json;var apiBase = new Uri("https://api.browser.city/v1/do/");using var http = new HttpClient();http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("BROWSERCITY_API_KEY"));var session = await PostActionAsync<ActionResponse<OpenResult>>(http, apiBase, "open", new { browser = "chromium" });try{ await PostActionAsync<object>(http, apiBase, "navigate", new { sessionId = session.Result.SessionId, url = "https://example.com" }); var markdown = await PostActionAsync<MarkdownResponse>(http, apiBase, "markdown", new { sessionId = session.Result.SessionId }); Console.WriteLine(markdown.Result);}finally{ await PostActionAsync<object>(http, apiBase, "close", new { sessionId = session.Result.SessionId });}static Uri ActionUri(Uri apiBase, string action) => new(apiBase, Uri.EscapeDataString(action));static async Task<T> PostActionAsync<T>(HttpClient http, Uri apiBase, string action, object body){ using var response = await http.PostAsJsonAsync(ActionUri(apiBase, action), body); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync<T>() ?? throw new InvalidOperationException("BrowserCity returned an empty response.");}record ActionResponse<T>(T Result, object? Context);record OpenResult(string SessionId);record MarkdownResponse(MarkdownResult Result);record MarkdownResult(string Text);import com.fasterxml.jackson.databind.JsonNode;import com.fasterxml.jackson.databind.ObjectMapper;import java.net.URI;import java.net.URLEncoder;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;import java.nio.charset.StandardCharsets;import java.util.Map;public class HumanizedRest { private static final URI API_BASE = URI.create("https://api.browser.city/v1/do/"); private static final HttpClient HTTP = HttpClient.newHttpClient(); private static final ObjectMapper JSON = new ObjectMapper(); public static void main(String[] args) throws Exception { var session = post("open", Map.of("browser", "chromium")); var sessionId = session.path("result").path("sessionId").asText(); try { post("navigate", Map.of("sessionId", sessionId, "url", "https://example.com")); var markdown = post("markdown", Map.of("sessionId", sessionId)); System.out.println(markdown.path("result").path("text").asText()); } finally { post("close", Map.of("sessionId", sessionId)); } } private static JsonNode post(String action, Map<String, ?> body) throws Exception { var request = HttpRequest.newBuilder(actionUri(action)) .header("Authorization", "Bearer %s".formatted(System.getenv("BROWSERCITY_API_KEY"))) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(JSON.writeValueAsString(body))) .build(); var response = HTTP.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() / 100 != 2) { throw new RuntimeException("Humanized REST action failed: %d %s".formatted(response.statusCode(), response.body())); } return JSON.readTree(response.body()); } private static URI actionUri(String action) { return API_BASE.resolve(URLEncoder.encode(action, StandardCharsets.UTF_8)); }}POST /v1/do/flow executes a flat, single-session DAG serially from entryStepId. Every step declares its exact onSuccess transition and may declare onFailure; array order never creates an implicit edge. Session-bound step inputs omit sessionId because Flow injects the one effective session.
Read the final session from effectiveSessionId on the Flow envelope. An exact $flowRef contains only { stepId, path }, where path is an array of property names and nonnegative indexes. It is not JSONPath, string interpolation, a predicate, or an expression language.
Valid executed flows return HTTP 200 with succeeded, completed_with_failures, failed, or aborted. Ordinary action failures are step data: a declared onFailure follows the failure branch, while a missing failure transition stops the Flow. Parse, authentication, compile, body-limit, admission, and same-session conflict failures remain non-2xx HTTP errors.
Browser effects are non-transactional. Completed navigation, clicks, typing, uploads, tab changes, opens, and closes are never rolled back; a session opened by the Flow remains open after a later failure or abort unless a close step completed. V1 has no loops, predicates, parallel steps, multiple sessions, durable/resumable execution, rollback, or Flow-level retries.
curl --fail-with-body https://api.browser.city/v1/do/flow \ -H "Authorization: Bearer $BROWSERCITY_API_KEY" \ -H "Content-Type: application/json" \ --data @- <<'JSON'{ "version": "1", "entryStepId": "open", "steps": [ { "id": "open", "action": "browser_open", "input": { "browser": "chromium", "resourceTier": "m" }, "onSuccess": { "stepId": "navigate" }, "onFailure": { "end": true } }, { "id": "navigate", "action": "browser_navigate", "input": { "url": "https://example.com" }, "onSuccess": { "stepId": "read" }, "onFailure": { "stepId": "close" } }, { "id": "read", "action": "browser_markdown", "input": {}, "onSuccess": { "stepId": "close" }, "onFailure": { "stepId": "close" } }, { "id": "close", "action": "browser_close", "input": {}, "onSuccess": { "end": true }, "onFailure": { "end": true } } ]}JSONThe response envelope's effectiveSessionId is the created session ID. If a later step fails or the request aborts before close completes, use that ID for explicit cleanup.
const response = await fetch('https://api.browser.city/v1/do/flow', { method: 'POST', headers: { Authorization: `Bearer ${process.env.BROWSERCITY_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ version: '1', sessionId: process.env.BROWSERCITY_SESSION_ID, entryStepId: 'read', steps: [ { id: 'read', action: 'browser_markdown', input: {}, onSuccess: { stepId: 'copyText' }, onFailure: { stepId: 'copyError' }, }, { id: 'copyText', action: 'browser_type', input: { element: 'Notes field', ref: 'notes', text: { $flowRef: { stepId: 'read', path: ['result', 'text'] } }, }, onSuccess: { stepId: 'close' }, onFailure: { stepId: 'close' }, }, { id: 'copyError', action: 'browser_type', input: { element: 'Notes field', ref: 'notes', text: { $flowRef: { stepId: 'read', path: ['error', 'message'] } }, }, onSuccess: { stepId: 'close' }, onFailure: { stepId: 'close' }, }, { id: 'close', action: 'browser_close', input: {}, onSuccess: { end: true }, onFailure: { end: true }, }, ], }),});if (!response.ok) { throw new Error(`Flow was rejected before execution: ${response.status}`);}const flow = await response.json();console.log(flow.status, flow.effectiveSessionId);if (flow.status === 'completed_with_failures') { console.log('A declared failure branch handled at least one action failure.');}Response steps stay in request declaration order. Executed records carry executionIndex; non-selected branches are skipped with branch_not_selected, and work after an unhandled stop is skipped with flow_stopped.
Our REST layer exposes the exact primitives an agent needs to explore the web, all mounted directly under /v1/do/. File upload accepts 1–10 inline base64 or public HTTP(S) sources, allows at most 8 MiB per file and 20 MiB total, revalidates every redirect, and never accepts host filesystem paths.
POST /v1/do/openPOST /v1/do/closePOST /v1/do/navigatePOST /v1/do/navigate_backPOST /v1/do/markdownPOST /v1/do/htmlPOST /v1/do/snapshotPOST /v1/do/search_snapshotPOST /v1/do/clickPOST /v1/do/typePOST /v1/do/fill_formPOST /v1/do/select_optionPOST /v1/do/file_uploadBuilding a dynamic agent? Call GET /v1/do/tools to fetch a JSON schema of all available actions, ready to drop directly into your LLM's tool array.
We're in private beta — request access and we'll get you set up. Private sessions by default.