CrewAI agents work best when tools are:
- deterministic (same input -> same output)
- fast (low latency, low glue code)
- safe (minimal data retention; avoid logging page content by default)
browser.city gives you two good “tool shapes”:
- Request API: one call to turn a URL into markdown
- Humanized REST (
/v1/do/*): interactive steps over HTTP (open, navigate, click, type, markdown) without running Playwright
1) Tool: URL -> markdown (Request API)
Define a tool that returns markdown for any URL:
tools.ts
export async function browsercityMarkdown(url: string): Promise<string> { 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, markdown: true }), }).then((r) => r.json()); return res.content as string;}import osimport requestsapi_key = os.environ["BROWSERCITY_API_KEY"]def browsercity_markdown(url: str) -> str: res = requests.post( "https://api.browser.city/v1/requests", headers={"Authorization": f"Bearer {api_key}"}, json={"url": url, "markdown": True}, ).json() return 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);async Task<string> BrowsercityMarkdown(string url){ var res = await http.PostAsJsonAsync( "https://api.browser.city/v1/requests", new { url, markdown = true }); return await res.Content.ReadAsStringAsync();}import com.fasterxml.jackson.databind.ObjectMapper;import java.net.URI;import java.net.http.*;import java.util.Map;public class Tools { static final String API_KEY = System.getenv("BROWSERCITY_API_KEY"); static final HttpClient HTTP = HttpClient.newHttpClient(); static final ObjectMapper JSON = new ObjectMapper(); static String browsercityMarkdown(String url) throws Exception { var body = JSON.writeValueAsString(Map.of("url", url, "markdown", true)); var req = HttpRequest.newBuilder() .uri(URI.create("https://api.browser.city/v1/requests")) .header("Authorization", "Bearer %s".formatted(API_KEY)) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); var res = HTTP.send(req, HttpResponse.BodyHandlers.ofString()); return res.body(); }}
Wire this function into your CrewAI agent as a tool (exact wiring depends on your CrewAI version).
2) Tool: interactive browse -> markdown (Humanized REST)
When a site needs state (cookies) or interaction (click/type), use /v1/do/* as a tool.
browse.ts
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());export async function browse(url: string) { const opened = await doAction('open', { browser: 'chromium' }); try { await doAction('navigate', { sessionId: opened.sessionId, url }); const page = await doAction('markdown', { sessionId: opened.sessionId }); return 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>();async Task<string?> Browse(string url) { var opened = await Do("open", new { browser = "chromium" }); try { await Do("navigate", new { sessionId = (string?)opened!["sessionId"], url }); var page = await Do("markdown", new { sessionId = (string?)opened!["sessionId"] }); return (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 Browse { 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(); static String browse(String url) throws Exception { var opened = callAction("open", Map.of("browser", "chromium")); var sessionId = opened.get("sessionId").asText(); try { callAction("navigate", Map.of("sessionId", sessionId, "url", url)); return callAction("markdown", Map.of("sessionId", sessionId)).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()); }}
What to use when
- Default to Request API for reading pages at scale (cheap and simple).
- Use Humanized REST when you need click/type/navigation without running Playwright.
- Use Sessions API for long-running, production-grade workflows you want to own in Playwright.