Use this when a workflow needs readable page content rather than screenshots or raw HTML. The template uses one session per job so cookies, navigation state, and egress stay consistent while you process the URL list.
Executable starters
extract-markdown.ts
const API_BASE = 'https://api.browser.city/v1';const apiKey = process.env.BROWSERCITY_API_KEY;if (!apiKey) { throw new Error('Set BROWSERCITY_API_KEY before running this script.');}type OpenResponse = { sessionId: string };type MarkdownResponse = { result: string };async function callAction<T>(action: string, body: unknown): Promise<T> { const response = await fetch(`${API_BASE}/do/${action}`, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!response.ok) { throw new Error(`${action} failed: ${response.status} ${await response.text()}`); } return (await response.json()) as T;}const opened = await callAction<OpenResponse>('open', { browser: 'chromium', labels: ['template:page-to-markdown'],});try { for (const url of ['https://example.com', 'https://example.org']) { await callAction('navigate', { sessionId: opened.sessionId, url }); const page = await callAction<MarkdownResponse>('markdown', { sessionId: opened.sessionId }); console.log(JSON.stringify({ url, markdown: page.result })); }} finally { await callAction('close', { sessionId: opened.sessionId });}import jsonimport osimport urllib.errorimport urllib.requestAPI_BASE = "https://api.browser.city/v1"API_KEY = os.environ["BROWSERCITY_API_KEY"]URLS = ["https://example.com", "https://example.org"]def call_action(action, body): request = urllib.request.Request( f"{API_BASE}/do/{action}", data=json.dumps(body).encode("utf-8"), headers={ "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", }, method="POST", ) try: with urllib.request.urlopen(request, timeout=60) as response: return json.loads(response.read().decode("utf-8")) except urllib.error.HTTPError as error: detail = error.read().decode("utf-8", errors="replace") raise RuntimeError(f"{action} failed: {error.code} {detail}") from erroropened = call_action("open", {"browser": "chromium", "labels": ["template:page-to-markdown"]})try: for url in URLS: call_action("navigate", {"sessionId": opened["sessionId"], "url": url}) page = call_action("markdown", {"sessionId": opened["sessionId"]}) print(json.dumps({"url": url, "markdown": page["result"]}))finally: call_action("close", {"sessionId": opened["sessionId"]})using System.Net.Http.Headers;using System.Net.Http.Json;var apiKey = Environment.GetEnvironmentVariable("BROWSERCITY_API_KEY") ?? throw new InvalidOperationException("Set BROWSERCITY_API_KEY before running this script.");var urls = new[] { "https://example.com", "https://example.org" };using var http = new HttpClient { BaseAddress = new Uri("https://api.browser.city/v1/") };http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);async Task<T> CallAction<T>(string action, object body) { using var response = await http.PostAsJsonAsync($"do/{action}", body); var payload = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { throw new InvalidOperationException($"{action} failed: {(int)response.StatusCode} {payload}"); } return System.Text.Json.JsonSerializer.Deserialize<T>(payload, new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;}var opened = await CallAction<OpenResponse>("open", new { browser = "chromium", labels = new[] { "template:page-to-markdown" }});try { foreach (var url in urls) { await CallAction<object>("navigate", new { sessionId = opened.SessionId, url }); var page = await CallAction<MarkdownResponse>("markdown", new { sessionId = opened.SessionId }); Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(new { url, markdown = page.Result })); }} finally { await CallAction<object>("close", new { sessionId = opened.SessionId });}record OpenResponse(string SessionId);record MarkdownResponse(string Result);import com.fasterxml.jackson.databind.JsonNode;import com.fasterxml.jackson.databind.ObjectMapper;import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;import java.util.List;import java.util.Map;public class ExtractMarkdown { static final URI API_BASE = URI.create("https://api.browser.city/v1/"); 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 { if (API_KEY == null || API_KEY.isBlank()) { throw new IllegalStateException("Set BROWSERCITY_API_KEY before running this script."); } var opened = callAction("open", Map.of( "browser", "chromium", "labels", List.of("template:page-to-markdown"))); var sessionId = opened.get("sessionId").asText(); try { for (var url : List.of("https://example.com", "https://example.org")) { callAction("navigate", Map.of("sessionId", sessionId, "url", url)); var page = callAction("markdown", Map.of("sessionId", sessionId)); System.out.println(JSON.writeValueAsString(Map.of( "url", url, "markdown", page.get("result").asText()))); } } finally { callAction("close", Map.of("sessionId", sessionId)); } } static JsonNode callAction(String action, Map<String, ?> body) throws Exception { var request = HttpRequest.newBuilder(API_BASE.resolve("do/%s".formatted(action))) .header("Authorization", "Bearer %s".formatted(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("%s failed: %d %s".formatted(action, response.statusCode(), response.body())); } return JSON.readTree(response.body()); }}
Production hardening checklist
- Persist each URL result as soon as it is extracted.
- Retry failed URLs with your own queue policy rather than hiding errors.
- Use labels to group jobs in BrowserCity usage views.
- Keep secrets in environment variables or your secret manager, not in prompts.
Cost and plan notes
Start with a single worker and managed datacenter egress. If pages are large or you add residential/mobile egress requirements, update the traffic assumption in /pricing-calculator before scaling the batch size.