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 apiKey = process.env.BROWSERCITY_API_KEY!;const opts = { method: "POST", headers: { Authorization: `Bearer ${apiKey}` } };const res = await fetch("https://api.browser.city/v1/requests", { ...opts, 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 java.net.URI;import java.net.http.*;var apiKey = System.getenv("BROWSERCITY_API_KEY");var http = HttpClient.newHttpClient();var req = HttpRequest.newBuilder() .uri(URI.create("https://api.browser.city/v1/requests")) .header("Authorization", "Bearer " + apiKey) .POST(HttpRequest.BodyPublishers.ofString( "{\"url\":\"https://example.com\",\"markdown\":true}")) .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 apiKey = process.env.BROWSERCITY_API_KEY!;const opts = { method: "POST", headers: { Authorization: `Bearer ${apiKey}` } };const res = await fetch("https://api.browser.city/v1/requests/batch", { ...opts, 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 java.net.URI;import java.net.http.*;var apiKey = System.getenv("BROWSERCITY_API_KEY");var http = HttpClient.newHttpClient();var body = "{\"requests\":[{\"url\":\"https://example.com\",\"markdown\":true},{\"url\":\"https://example.com/docs\",\"markdown\":true}]}";var req = HttpRequest.newBuilder() .uri(URI.create("https://api.browser.city/v1/requests/batch")) .header("Authorization", "Bearer " + apiKey) .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 apiKey = process.env.BROWSERCITY_API_KEY!;const opts = { method: "POST", headers: { Authorization: `Bearer ${apiKey}` } };const session = await fetch("https://api.browser.city/v1/sessions", { ...opts, body: JSON.stringify({ browser: "chromium", egress: { mode: "managed", proxyType: "residential", country: "US" }, }),}).then((r) => r.json());const browser = await chromium.connect(session.endpoint, { headers: { Authorization: `Bearer ${session.token}` },});import osimport requestsfrom playwright.sync_api import sync_playwrightapi_key = os.environ["BROWSERCITY_API_KEY"]session = requests.post( "https://api.browser.city/v1/sessions", headers={"Authorization": f"Bearer {api_key}"}, json={"browser": "chromium"},).json()with sync_playwright() as p: browser = p.chromium.connect( session["endpoint"], headers={"Authorization": f"Bearer {session['token']}"}, ) page = browser.new_page() page.goto("https://example.com")using Microsoft.Playwright;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/sessions", new { browser = "chromium" });var session = await res.Content.ReadFromJsonAsync<Session>() ?? throw new Exception("bad response");using var pw = await Playwright.CreateAsync();var browser = await pw.Chromium.ConnectAsync(session.endpoint, new() { Headers = new Dictionary<string, string> { ["Authorization"] = $"Bearer {session.token}", },});public record Session(string endpoint, string token);import com.microsoft.playwright.*;import java.net.URI;import java.net.http.*;// Create session via your preferred HTTP clientvar apiKey = System.getenv("BROWSERCITY_API_KEY");var http = HttpClient.newHttpClient();var res = http.send( HttpRequest.newBuilder() .uri(URI.create("https://api.browser.city/v1/sessions")) .header("Authorization", "Bearer " + apiKey) .POST(HttpRequest.BodyPublishers.ofString("{\"browser\":\"chromium\"}")) .build(), HttpResponse.BodyHandlers.ofString());var session = res.body();try (var pw = Playwright.create()) { var browser = pw.chromium().connect( extractEndpoint(session), new BrowserType.ConnectOptions().setHeaders( java.util.Map.of("Authorization", "Bearer " + extractToken(session))));}static String extractEndpoint(String json) { return ""; }static String extractToken(String json) { return ""; }
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 apiKey = process.env.BROWSERCITY_API_KEY!;const opts = { method: "POST", headers: { Authorization: `Bearer ${apiKey}` } };const open = await fetch("https://api.browser.city/v1/do/open", { ...opts, body: JSON.stringify({ browser: "chromium" }),}).then((r) => r.json());const sessionId = open.result;await fetch("https://api.browser.city/v1/do/navigate", { ...opts, body: JSON.stringify({ sessionId, url: "https://example.com" }),});const md = await fetch("https://api.browser.city/v1/do/markdown", { ...opts, body: JSON.stringify({ sessionId }),}).then((r) => r.json());console.log(md.result);import osimport requestsapi_key = os.environ["BROWSERCITY_API_KEY"]headers = {"Authorization": f"Bearer {api_key}"}open_res = requests.post( "https://api.browser.city/v1/do/open", headers=headers, json={"browser": "chromium"},).json()session_id = open_res["result"]requests.post( "https://api.browser.city/v1/do/navigate", headers=headers, json={"sessionId": session_id, "url": "https://example.com"},)md = requests.post( "https://api.browser.city/v1/do/markdown", headers=headers, json={"sessionId": session_id},).json()print(md["result"])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 open = await http.PostAsJsonAsync( "https://api.browser.city/v1/do/open", new { browser = "chromium" });var openJson = await open.Content.ReadFromJsonAsync<DoResponse>() ?? throw new Exception("bad response");var sessionId = openJson.result;await http.PostAsJsonAsync( "https://api.browser.city/v1/do/navigate", new { sessionId, url = "https://example.com" });var md = await http.PostAsJsonAsync( "https://api.browser.city/v1/do/markdown", new { sessionId });var mdJson = await md.Content.ReadFromJsonAsync<DoResponse>() ?? throw new Exception("bad response");Console.WriteLine(mdJson.result);public record DoResponse(string result);import java.net.URI;import java.net.http.*;var apiKey = System.getenv("BROWSERCITY_API_KEY");var http = HttpClient.newHttpClient();var openRes = http.send( HttpRequest.newBuilder() .uri(URI.create("https://api.browser.city/v1/do/open")) .header("Authorization", "Bearer " + apiKey) .POST(HttpRequest.BodyPublishers.ofString("{\"browser\":\"chromium\"}")) .build(), HttpResponse.BodyHandlers.ofString());var sessionId = extractResult(openRes.body());http.send( HttpRequest.newBuilder() .uri(URI.create("https://api.browser.city/v1/do/navigate")) .header("Authorization", "Bearer " + apiKey) .POST(HttpRequest.BodyPublishers.ofString( "{\"sessionId\":\"" + sessionId + "\",\"url\":\"https://example.com\"}")) .build(), HttpResponse.BodyHandlers.discarding());var mdRes = http.send( HttpRequest.newBuilder() .uri(URI.create("https://api.browser.city/v1/do/markdown")) .header("Authorization", "Bearer " + apiKey) .POST(HttpRequest.BodyPublishers.ofString("{\"sessionId\":\"" + sessionId + "\"}")) .build(), HttpResponse.BodyHandlers.ofString());System.out.println(extractResult(mdRes.body()));static String extractResult(String json) { return ""; }
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.