browser.city intentionally gives you four ways to ship browser automation. They overlap, but they are optimized for different workloads:
- Request API:
POST /v1/requestsandPOST /v1/requests/batchfor “URL -> HTML/markdown” - Sessions API:
POST /v1/sessions, then connect with Playwright for real workflows - Humanized REST tools:
POST /v1/do/*for REST-like actions over a remote session (open, navigate, click, type, markdown, screenshots, pdf, etc.) - MCP server: the agent-friendly wrapper that exposes the Humanized tools to MCP clients (Codex, Claude Code, Cursor, etc.)
This guide helps you choose quickly and avoid building the wrong integration.
Fast rule of thumb
- If you want content extraction, choose Request API.
- If you want full control and already run Playwright, choose Sessions API.
- If you want interactive actions but don’t want Playwright, choose Humanized REST.
- If you want an agent to decide steps using tools, choose MCP.
Decision table
| What you need | Pick | Why |
|---|---|---|
| ”Fetch this URL and return markdown” | Request API | Fastest path, no browser client code |
| Crawl many URLs with retries and batching | Request API (batch) | Shared browser session across up to 100 URLs per call |
| Login, then navigate multiple pages, then download files | Sessions API | You own the Playwright script and session lifetime |
| Click/type/markdown via HTTP calls (no Playwright runtime) | Humanized REST (/v1/do/*) | REST-like actions over a real remote session |
| Test from a specific geo (or BYOP proxy) | Sessions API or Request API | Both accept egress config; pick based on interaction needs |
| Let an LLM agent browse/click/extract inside your editor | MCP | The client discovers tools automatically; minimal glue code |
| You already have a Playwright crawler | Sessions API | Swap launch() for connect() and keep the rest |
Option 1: Request API (best for extraction)
Use the Request API when your output is content, not an interactive browser artifact.
It is ideal for:
- RAG ingestion (URL -> markdown -> chunk)
- scraping pipelines where you don’t need logins
- monitoring jobs (prices, listings, changelogs)
Single URL -> markdown
const res = await fetch("https://api.browser.city/v1/requests", { method: "POST", headers: { Authorization: `Bearer ${process.env.BROWSERCITY_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ url: "https://example.com", markdown: true }),}).then((r) => r.json());console.log(res.content);import osimport requestsapi_key = os.environ["BROWSERCITY_API_KEY"]res = requests.post( "https://api.browser.city/v1/requests", headers={"Authorization": f"Bearer {api_key}"}, json={"url": "https://example.com", "markdown": True},).json()print(res["content"])using System.Net.Http.Headers;using System.Net.Http.Json;var apiKey = Environment.GetEnvironmentVariable("BROWSERCITY_API_KEY")!;var http = new HttpClient();http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);var res = await http.PostAsJsonAsync( "https://api.browser.city/v1/requests", new { url = "https://example.com", markdown = true });var json = await res.Content.ReadFromJsonAsync<RequestResponse>() ?? throw new Exception("bad response");Console.WriteLine(json.content);public record RequestResponse(string content);import com.fasterxml.jackson.databind.ObjectMapper;import java.net.URI;import java.net.http.*;import java.util.Map;public class Request { public static void main(String[] args) throws Exception { var apiKey = System.getenv("BROWSERCITY_API_KEY"); var http = HttpClient.newHttpClient(); var json = new ObjectMapper(); var body = json.writeValueAsString(Map.of("url", "https://example.com", "markdown", true)); var req = HttpRequest.newBuilder() .uri(URI.create("https://api.browser.city/v1/requests")) .header("Authorization", "Bearer %s".formatted(apiKey)) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); var res = http.send(req, HttpResponse.BodyHandlers.ofString()); System.out.println(res.body()); }}
Notes:
- Set
"markdown": trueto get clean markdown. Otherwise you get HTML. - Rendering is on by default. If you want “no JS”, set
"render": false.
Batch URLs with a shared session
POST /v1/requests/batch runs up to 100 URLs in one shared browser session/context. That means cookies and localStorage can carry between URLs in the batch (useful for multi-page flows that don’t require a human login step).
const res = await fetch("https://api.browser.city/v1/requests/batch", { method: "POST", headers: { Authorization: `Bearer ${process.env.BROWSERCITY_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ requests: [ { url: "https://example.com", markdown: true }, { url: "https://example.com/docs", markdown: true }, ], }),}).then((r) => r.json());console.log(res.successCount, res.errorCount);import osimport requestsapi_key = os.environ["BROWSERCITY_API_KEY"]res = requests.post( "https://api.browser.city/v1/requests/batch", headers={"Authorization": f"Bearer {api_key}"}, json={ "requests": [ {"url": "https://example.com", "markdown": True}, {"url": "https://example.com/docs", "markdown": True}, ] },).json()print(res["successCount"], res["errorCount"])using System.Net.Http.Headers;using System.Net.Http.Json;var apiKey = Environment.GetEnvironmentVariable("BROWSERCITY_API_KEY")!;var http = new HttpClient();http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);var res = await http.PostAsJsonAsync( "https://api.browser.city/v1/requests/batch", new { requests = new[] { new { url = "https://example.com", markdown = true }, new { url = "https://example.com/docs", markdown = true }, } });Console.WriteLine(await res.Content.ReadAsStringAsync());import com.fasterxml.jackson.databind.ObjectMapper;import java.net.URI;import java.net.http.*;import java.util.List;import java.util.Map;public class Batch { public static void main(String[] args) throws Exception { var apiKey = System.getenv("BROWSERCITY_API_KEY"); var http = HttpClient.newHttpClient(); var json = new ObjectMapper(); var body = json.writeValueAsString(Map.of( "requests", List.of( Map.of("url", "https://example.com", "markdown", true), Map.of("url", "https://example.com/docs", "markdown", true) ) )); var req = HttpRequest.newBuilder() .uri(URI.create("https://api.browser.city/v1/requests/batch")) .header("Authorization", "Bearer %s".formatted(apiKey)) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); var res = http.send(req, HttpResponse.BodyHandlers.ofString()); System.out.println(res.body()); }}
Option 2: Sessions API (best for interaction)
Use Sessions when you need a real browser workflow:
- authentication
- click/typing flows
- file downloads/uploads
- complex “app” UIs (SPAs, dashboards)
Create a session, then connect with Playwright
import { chromium } from 'playwright';const { endpoint, token, id } = await fetch('https://api.browser.city/v1/sessions', { method: 'POST', headers: { Authorization: `Bearer ${process.env.BROWSERCITY_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ browser: 'chromium' }),}).then((r) => r.json());try { const browser = await chromium.connect(endpoint, { headers: { Authorization: `Bearer ${token}` }, }); const page = browser.contexts().at(0)!.pages().at(0)!; await page.goto('https://example.com'); console.log(await page.title());} finally { const { summary } = await fetch(`https://api.browser.city/v1/sessions/${id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.BROWSERCITY_API_KEY}` }, }).then((r) => r.json()); console.log('Runtime seconds:', summary?.stats?.runtimeSeconds ?? 0); console.log('Cookies stored:', summary?.storage?.cookies?.length ?? 0);}using Microsoft.Playwright;using System.Collections.Generic;using System.Net.Http.Headers;using System.Net.Http.Json;var http = new HttpClient();http.DefaultRequestHeaders.Authorization = new( "Bearer", Environment.GetEnvironmentVariable("BROWSERCITY_API_KEY"));var session = await (await http.PostAsJsonAsync( "https://api.browser.city/v1/sessions", new { browser = "chromium" })) .Content.ReadFromJsonAsync<Session>();var (id, endpoint, token) = session!;IPlaywright? pw = null;try { pw = await Playwright.CreateAsync(); var browser = await pw.Chromium.ConnectAsync(endpoint, new() { Headers = new() { ["Authorization"] = $"Bearer {token}" } }); var page = browser.Contexts[0].Pages[0]; await page.GotoAsync("https://example.com");} finally { try { if (session is not null) { var deleted = await (await http.DeleteAsync($"https://api.browser.city/v1/sessions/{id}")) .Content.ReadFromJsonAsync<DeleteSessionResponse>(); Console.WriteLine($"Runtime seconds: {deleted?.Summary?.Stats?.RuntimeSeconds ?? 0}"); Console.WriteLine($"Cookies stored: {deleted?.Summary?.Storage?.Cookies?.Count ?? 0}"); } } finally { pw?.Dispose(); }}record Session(string Id, string Endpoint, string Token);record DeleteSessionResponse(DeleteSessionSummary? Summary);record DeleteSessionSummary(DeleteSessionStats? Stats, DeleteSessionStorage? Storage);record DeleteSessionStats(double? RuntimeSeconds);record DeleteSessionStorage(List<object>? Cookies);import com.fasterxml.jackson.databind.ObjectMapper;import com.microsoft.playwright.*;import java.net.URI;import java.net.http.*;import java.util.List;import java.util.Map;public class Session { public static void main(String[] args) throws Exception { var key = System.getenv("BROWSERCITY_API_KEY"); var http = HttpClient.newHttpClient(); var json = new ObjectMapper(); var sessionRequestBody = json.writeValueAsString(Map.of("browser", "chromium")); var req = HttpRequest.newBuilder(URI.create("https://api.browser.city/v1/sessions")) .header("Authorization", "Bearer %s".formatted(key)) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(sessionRequestBody)) .build(); var created = json.readValue( http.send(req, HttpResponse.BodyHandlers.ofString()).body(), CreatedSession.class); var id = created.id(); var endpoint = created.endpoint(); var token = created.token(); Playwright pw = null; try { pw = Playwright.create(); var browser = pw.chromium().connect(endpoint, new BrowserType.ConnectOptions().setHeaders(Map.of("Authorization", "Bearer %s".formatted(token)))); var page = browser.contexts().get(0).pages().get(0); page.navigate("https://example.com"); } finally { try { var deleteResponse = http.send(HttpRequest.newBuilder(URI.create("https://api.browser.city/v1/sessions/" + id)) .header("Authorization", "Bearer %s".formatted(key)).DELETE().build(), HttpResponse.BodyHandlers.ofString()); if (deleteResponse.statusCode() / 100 != 2) { throw new RuntimeException("Delete failed: " + deleteResponse.statusCode()); } var deleteBody = deleteResponse.body(); if (deleteBody.strip().startsWith("{")) { var deleted = json.readValue(deleteBody, Map.class); Object runtimeSeconds = 0; int cookieCount = 0; if (deleted.get("summary") instanceof Map<?, ?> summary) { if (summary.get("stats") instanceof Map<?, ?> stats && stats.get("runtimeSeconds") != null) { runtimeSeconds = stats.get("runtimeSeconds"); } if (summary.get("storage") instanceof Map<?, ?> storage && storage.get("cookies") instanceof List<?> cookies) { cookieCount = cookies.size(); } } System.out.println("Runtime seconds: " + runtimeSeconds); System.out.println("Cookies stored: " + cookieCount); } } finally { if (pw != null) pw.close(); } } } record CreatedSession(String id, String endpoint, String token) {}}
From here, everything is normal Playwright: create contexts/pages, click, navigate, etc.
Option 3: Humanized REST tools (/v1/do/*)
Humanized REST is “interactive browsing over HTTP”. You open a remote session, then perform simple actions via REST calls.
This is ideal when:
- you want deterministic steps without running Playwright
- you’re integrating into systems that are “HTTP only” (workflows, serverless jobs, internal tools)
const doUrl = (action: string) => `https://api.browser.city/v1/do/${action}`;const doAction = (action: string, body: unknown) => fetch(doUrl(action), { method: 'POST', headers: { Authorization: `Bearer ${process.env.BROWSERCITY_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body), }).then((r) => r.json());const opened = await doAction('open', { browser: 'chromium' });try { await doAction('navigate', { sessionId: opened.sessionId, url: 'https://example.com' }); const page = await doAction('markdown', { sessionId: opened.sessionId }); console.log(page.result);} finally { await doAction('close', { sessionId: opened.sessionId });}using System.Net.Http.Headers;using System.Net.Http.Json;using System.Text.Json.Nodes;var http = new HttpClient();http.DefaultRequestHeaders.Authorization = new( "Bearer", Environment.GetEnvironmentVariable("BROWSERCITY_API_KEY"));async Task<JsonNode?> Do(string action, object body) => await (await http.PostAsJsonAsync( new Uri($"https://api.browser.city/v1/do/{action}"), body)).Content.ReadFromJsonAsync<JsonNode>();var opened = await Do("open", new { browser = "chromium" });try { await Do("navigate", new { sessionId = (string?)opened!["sessionId"], url = "https://example.com" }); var page = await Do("markdown", new { sessionId = (string?)opened!["sessionId"] }); Console.WriteLine((string?)page!["result"]);} finally { await Do("close", new { sessionId = (string?)opened!["sessionId"] });}import com.fasterxml.jackson.databind.*;import java.net.URI;import java.net.http.*;import java.util.Map;public class Do { static final String DO_API_BASE = "https://api.browser.city/v1/do"; static final String API_KEY = System.getenv("BROWSERCITY_API_KEY"); static final HttpClient HTTP = HttpClient.newHttpClient(); static final ObjectMapper JSON = new ObjectMapper(); public static void main(String[] args) throws Exception { var opened = callAction("open", Map.of("browser", "chromium")); var sessionId = opened.get("sessionId").asText(); try { callAction("navigate", Map.of("sessionId", sessionId, "url", "https://example.com")); var page = callAction("markdown", Map.of("sessionId", sessionId)); System.out.println(page.get("result").asText()); } finally { callAction("close", Map.of("sessionId", sessionId)); } } static JsonNode callAction(String action, Map<String, ?> body) throws Exception { var req = HttpRequest.newBuilder(URI.create("%s/%s".formatted(DO_API_BASE, action))) .header("Authorization", "Bearer %s".formatted(API_KEY)) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(JSON.writeValueAsString(body))).build(); return JSON.readTree(HTTP.send(req, HttpResponse.BodyHandlers.ofString()).body()); }}
Option 4: MCP server (best for agents and tooling)
MCP is the easiest way to connect browser.city to agent clients (for example: coding agents, editor assistants, and MCP-compatible frameworks).
Your MCP client discovers tools like:
- open browser session
- navigate
- snapshot / markdown extraction
- click / type / fill forms
Use MCP when:
- you want the agent to choose which pages to open
- you want to keep integration code minimal
- you want “tools” instead of writing a custom Playwright harness
Common patterns that work well
- Extract first, interact only when needed: Try Request API for 90% of URLs, fall back to Sessions only when a site requires login or interaction.
- Keep long workflows in Sessions: If you have to log in, keep the work in the same session and avoid switching back and forth between APIs.
- Use MCP for exploration, then productionize: Let an agent explore an unfamiliar UI with MCP tools, then codify the reliable flow in Sessions API.
MCP exposes the same Humanized primitives as tools, with tool discovery and schemas for agent clients.