Cloud browser

Cloud browser

A session is a real Chrome window running in the cloud. You drive it over HTTP with an API key, a person can watch it or take it over in the live viewer, and both can happen in the same session. This guide walks through a session from creation to release.

Create a session

Every browser endpoint accepts Authorization: Bearer sk_.... Read endpoints need the browser:read scope on the key and anything that acts needs browser:write.

curl -X POST https://scrappy.gg/api/browser/sessions \
-H "Authorization: Bearer sk_..." \
-H "Content-Type: application/json" \
-d '{ "screenWidth": 1280, "screenHeight": 720 }'

The response carries session.id, which every later call uses, and a viewerUrl. screenWidth and screenHeight set the real window size and the screen the browser reports. Other optional fields are profileId (start already signed in, see Saved logins), clientRef, projectId and showCursor.

A session ends on its own after 30 minutes with no activity, with a 12-hour cap as a backstop. Every action the browser served resets the idle window. If a person is only looking at the viewer, call POST /api/browser/sessions/{id}/keepalive on the interval it returns.

Watch or take over in the live viewer

The viewer shows the session's page live and passes your mouse and keyboard through to it. In the dashboard it opens alongside the session. To embed it in your own app, mint a short-lived token from your backend and use the viewerUrl that comes back as an iframe src. The token lasts about five minutes, so mint it when the viewer is about to open, not when the session is created.

curl -X POST https://scrappy.gg/api/browser/sessions/$SESSION/viewer-token \
-H "Authorization: Bearer sk_..."
# -> { token, expiresAt, expiresInMs, viewerUrl } viewerUrl is absolute and carries the token

From a phone

The viewer always starts with the whole page in view. On a touch screen:

  • One finger is the mouse. Touch presses, moving while held drags, lifting releases. A press lasts as long as your finger stays down, and two quick taps are a double click.
  • Two fingers move the view. Pinch to zoom, drag to pan. A drag the view cannot absorb scrolls the page. Zooming in sharpens the picture.
  • A keyboard buttonraises your phone's keyboard and types into whatever has focus in the remote page.

The remote page stays a desktop page throughout. Your touches arrive there as mouse and key input, so the site sees the same browser before and after you step in.

Read the page, then act by ref

read-page returns Chrome's accessibility tree as a flat list. Each node has a role, an accessible name and a ref such as ref_31. find asks the same tree a question and returns the ranked matches. Pass the ref to click, type, hover, press, check, select, element-info, screenshot or fill-form.

# 1. Find the control by what it says
curl -X POST https://scrappy.gg/api/browser/sessions/$SESSION/find \
-H "Authorization: Bearer sk_..." -H "Content-Type: application/json" \
-d '{ "query": "Sign in", "role": "button" }'
# -> matches: [{ "ref": "ref_31", "role": "button", "name": "Sign in", ... }]
# 2. Act on the ref
curl -X POST https://scrappy.gg/api/browser/sessions/$SESSION/click \
-H "Authorization: Bearer sk_..." -H "Content-Type: application/json" \
-d '{ "ref": "ref_31" }'

A ref stays valid until the page re-renders that node. After that the action answers NOT_FOUND or ELEMENT_NOT_FOUND and you call read-page again. CSS selectors and x/y coordinates are accepted everywhere a ref is.

Single actions and batches

Each action is its own endpoint under /api/browser/sessions/{id}/: navigate, click, type, press, hover, scroll, select, check, fill-form, drag-drop, paste, screenshot, scrape, evaluate and more. Each takes an optional tab to target a specific tab. The API reference lists every parameter.

batch runs up to 25 steps in one request, in order. Each step is { "action": ..., ...that action's parameters } and runs the same handler its own endpoint runs. By default the batch stops at the first failed step and returns the rest under skipped. A whole batch is bounded at 90 seconds.

curl -X POST https://scrappy.gg/api/browser/sessions/$SESSION/batch \
-H "Authorization: Bearer sk_..." -H "Content-Type: application/json" \
-d '{
"steps": [
{ "action": "navigate", "url": "https://example.com/login", "waitUntil": "domcontentloaded" },
{ "action": "type", "selector": "#email", "text": "ada@example.com", "clear": true },
{ "action": "type", "selector": "#password", "text": "correct horse" },
{ "action": "click", "selector": "button[type=submit]" },
{ "action": "wait_for_function", "expression": "location.pathname === \"/home\"", "required": true }
]
}'

The response carries results (one per step, with the status its own endpoint would have returned), skipped, completed, failed and stoppedAt. A batch-only wait step pauses for up to 15 seconds. upload, cookies, storage and the session lifecycle calls are not batch steps.

Wait for the page to be ready

navigate returns as soon as the new document exists, before any script has run. A click sent at that moment can land on a button whose handler is not attached yet and report success anyway. Pick a wait that matches what your next step needs:

WaitReturns whenUse it for
waitUntil: "domcontentloaded"Deferred scripts have runAny page you are about to interact with
waitUntil: "load"Images, fonts and stylesheets have loaded tooScreenshots and PDFs
wait-for-selectorAn element is visible, present, hidden or detachedA form or confirmation appearing
wait-for-functionA synchronous JavaScript expression is truthyThe app itself being ready
wait-for-networkThe page has stopped issuing requestsSingle-page apps that render before their data arrives

The waits answer 200 on timeout with timedOut: true, so you decide whether that is fatal. Inside a batch, set required: true on wait_for_function to turn a timeout into a failed step, so stopOnError stops before the next click.

Upload and download files

From the viewer: when a page asks for a file, the viewer opens the file picker on your own device and attaches what you choose to the remote page.

From the API: upload attaches a file to a file input. Send the bytes one way (multipart file up to 9MB, a sourceUrl the browser service fetches itself up to 100MB, or base64 content) and name the input by selector. The response lists what the input holds afterwards under attached.

# Multipart, up to 9MB
curl -X POST https://scrappy.gg/api/browser/sessions/$SESSION/upload \
-H "Authorization: Bearer sk_..." \
-F "selector=input[type=file]" -F "file=@./report.pdf"

Downloads are kept per session. Each file is stored under its own download id, so two exports with the same name never overwrite each other and no other session can touch them. List them, then fetch a completed one by id:

curl "https://scrappy.gg/api/browser/sessions/$SESSION/downloads?since=0" \
-H "Authorization: Bearer sk_..."
# -> downloads: [{ id, filename, state: "completed", receivedBytes, ... }], now
curl -OJ https://scrappy.gg/api/browser/sessions/$SESSION/downloads/$DOWNLOAD_ID \
-H "Authorization: Bearer sk_..."

Each record has a state of in_progress, completed or canceled. To poll for new events, pass the now value from the last response as since. Downloads are tracked from the session's first API action onward, and files must be fetched before the session is released.

Record a session to MP4

Recording captures what the session does as video without anything the page can observe. The defaults are 5 frames per second at 1280x720 for up to two minutes. start also accepts quality, maxWidth, maxHeight, maxDurationMs (up to 15 minutes) and maxBytes. record is a batch step too, so start, drive and stop can be one request.

curl -X POST https://scrappy.gg/api/browser/sessions/$SESSION/recording \
-H "Authorization: Bearer sk_..." -H "Content-Type: application/json" \
-d '{ "action": "start", "fps": 5, "maxDurationMs": 120000 }'
# ... drive the session, then post { "action": "stop" } to the same path ...
curl -o session.mp4 https://scrappy.gg/api/browser/sessions/$SESSION/recording \
-H "Authorization: Bearer sk_..."

When a tab stops answering

A native alert, confirm, prompt or beforeunload dialog blocks its tab. Every page command against that tab times out until the dialog is answered, which looks the same as a hung page. When a dialog is known to be open, the failing action says so: the error gains error.dialogOpen (type, message, url, tab) and error.recovery pointing at /dialog. The status, code and message are unchanged.

curl -X POST https://scrappy.gg/api/browser/sessions/$SESSION/dialog \
-H "Authorization: Bearer sk_..." -H "Content-Type: application/json" \
-d '{ "action": "dismiss" }'
# -> { answered: true, dialog: {...}, nextDialog: {...} | null }
  • status reports whether a dialog is open without asking anything of the page.
  • accept and dismiss answer it. The page and anything typed into it survive. Answering one dialog can open another, returned as nextDialog. The tab stays blocked until that one is answered too, so loop until nextDialog is null.
  • suppress (the default action) replaces the dialog functions before they are called. Apply it before a click that might open one, and again after each navigation. It covers the top frame only.

If no dialog is found and commands keep timing out, later errors carry error.pageUnresponsive: true. POST /api/browser/sessions/{id}/recover-tab is the last resort: it replaces the tab with a fresh one in the same session and navigates back to the same URL. Cookies, storage and sign-in are kept. The hung document and anything typed into it are lost, and refs and dialog suppression need to be re-read and re-applied.

What it costs

Starting a session costs 5 credits, refunded if the session fails to start. Runtime costs 1 credit for each elapsed minute while the session is live. Actions are not charged individually.

Creating a session without enough credits answers 402. If the balance runs out mid-run the session is ended. See Credits and billing for buying credits.

Release the session

DELETE /api/browser/sessions/{id} stops the meter and waives the final partial minute. If the session was created from a profile, its current login is written back to the profile first. Pass ?capture=false to skip that write when the session ended on a page you do not want saved.

POST /api/browser/sessions/{id}/release does the same and is meant for navigator.sendBeacon, so a session is freed when its page closes.