Humanized REST

Simple HTTP requests for complex browser actions

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.

REST action flow
  1. 1POST /v1/do/open
  2. 2POST /v1/do/navigate
  3. 3POST /v1/do/markdown
  4. 4POST /v1/do/close
Where it fits

Choose the right connection

Humanized 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.

Use REST /v1/do/* when

  • Your orchestrator speaks HTTP
  • You want one action per request
  • You need queue-friendly browser jobs

Use MCP when

  • Your agent runtime already supports tools
  • You want BrowserCity surfaced as a tool server
  • You prefer model-native tool discovery

Use Playwright WS when

  • You already have Playwright scripts
  • You need low-level browser control
  • You want to reuse existing test automation code
Verified shape

A familiar HTTP rhythm

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.

humanized-rest.ts
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 });}
Browser Flow v1

Send a bounded workflow as one request

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.

Open a session inside the Flow

open-browser-flow.sh
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 }    }  ]}JSON

The 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.

Branch on failure and converge on an existing session

existing-session-flow.ts
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.

Action catalog

Composable browser steps

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.

Start

Create a BrowserCity session when your workflow starts and close it explicitly when the job is done.

POST /v1/do/openPOST /v1/do/close
Move

Send navigation steps as REST calls while keeping state tied to the same sessionId.

POST /v1/do/navigatePOST /v1/do/navigate_back
Read

Pull page content back as structured action results for agent memory, RAG, and workflow decisions.

POST /v1/do/markdownPOST /v1/do/htmlPOST /v1/do/snapshotPOST /v1/do/search_snapshot
Act

Drive normal browser interactions through named REST actions instead of keeping a WebSocket client open.

POST /v1/do/clickPOST /v1/do/typePOST /v1/do/fill_formPOST /v1/do/select_option
Upload

Attach inline base64 files or files fetched from public HTTP(S) URLs without exposing service filesystem paths.

POST /v1/do/file_upload
Discovery

Built for model-native tool discovery

Building 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.

Representative verified actions

opennavigatemarkdownhtmlsnapshotsearch_snapshotclicktypefill_formfile_uploadtake_screenshotpdf_saveclose
[ 08 / 08 ] — Get Started

Give your AI agents the web.

We're in private beta — request access and we'll get you set up. Private sessions by default.