Use this when a workflow ends at a web form that has no API: claims portals, supplier onboarding, government filings, support consoles. The session runs in the cloud with a coherent browser identity, and your existing Playwright knowledge transfers unchanged.
Executable starters
import { chromium } from 'playwright';const apiKey = process.env.BROWSERCITY_API_KEY;if (!apiKey) { throw new Error('Set BROWSERCITY_API_KEY before running this script.');}const { endpoint, token, id } = await fetch('https://api.browser.city/v1/sessions', { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, '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/portal/submit'); await page.fill('#applicant-name', 'Ada Lovelace'); await page.fill('#reference', 'CASE-2026-0713'); await page.setInputFiles('#attachment', './report.pdf'); await page.click('button[type="submit"]'); await page.waitForSelector('.confirmation'); console.log(await page.textContent('.confirmation'));} finally { await fetch(`https://api.browser.city/v1/sessions/${id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${apiKey}` }, });}import osimport requestsfrom playwright.sync_api import sync_playwrightAPI_KEY = os.environ["BROWSERCITY_API_KEY"]auth = {"Authorization": f"Bearer {API_KEY}"}session = requests.post( "https://api.browser.city/v1/sessions", headers=auth, json={"browser": "chromium"}).json()try: with sync_playwright() as playwright: browser = playwright.chromium.connect( session["endpoint"], headers={"Authorization": f"Bearer {session['token']}"}, ) page = browser.contexts[0].pages[0] page.goto("https://example.com/portal/submit") page.fill("#applicant-name", "Ada Lovelace") page.fill("#reference", "CASE-2026-0713") page.set_input_files("#attachment", "./report.pdf") page.click('button[type="submit"]') page.wait_for_selector(".confirmation") print(page.text_content(".confirmation"))finally: requests.delete(f"https://api.browser.city/v1/sessions/{session['id']}", headers=auth)
Humanized REST upload alternative
When an HTTP-first workflow already uses /v1/do/*, open the file chooser with browser_click, then call POST /v1/do/file_upload. The public contract accepts caller-provided bytes or public HTTP(S) URLs; it never accepts a path on BrowserCity’s service filesystem.
{
"sessionId": "<session id>",
"files": [
{
"filename": "report.pdf",
"mediaType": "application/pdf",
"source": { "type": "base64", "data": "<base64 bytes>" }
},
{
"filename": "terms.txt",
"source": { "type": "url", "url": "https://files.example/terms.txt" }
}
]
}
One request may contain 1–10 files, up to 8 MiB each and 20 MiB combined. BrowserCity validates and pins every public-URL redirect hop, stages private temporary files only for the transfer, and removes them afterward. The response reports safe filename, media type, and byte-count metadata without returning temporary paths.
Production hardening checklist
- Wait for an explicit confirmation element before treating the submission as done.
- Capture the confirmation text or reference number as your audit record.
- Make the job idempotent: check whether the record already exists before resubmitting after a retry.
- Keep credentials and uploaded files out of logs; only persist the outcome.
Cost and plan notes
A single form pass typically runs well under a minute of session time. For recurring workflows, multiply run frequency by observed session length in /pricing-calculator — traffic is usually negligible next to compute.