Profiles REST API

Running a profile

Start returns the DevTools socket. That socket is the whole automation story.

Lay the running windows out on screen

POST/api/v1/running/arrange

Positions every running profile in ids on the primary display's work area. grid gives each window an equal cell, cascade overlaps them with their title bars clickable, stack returns them all to full size on top of each other. Bounds go through the browser rather than the window manager, because Chromium owns its own geometry and undoes anything moved from outside. A profile that is no longer running is counted in skipped, not an error — the rest still move.

Body

FieldTypeWhat it is
idsrequiredstring[]Profiles to arrange. Ones that are not running are skipped.
layout"grid" | "cascade" | "stack"Default grid.

Returns

{ ok: n, skipped: n, failures: [{ profileId, reason }] }

curl -X POST http://127.0.0.1:PORT/api/v1/running/arrange \
  -H "Authorization: Bearer $MOSAIC_KEY" -H "Content-Type: application/json" \
  -d '{"ids":["p1","p2","p3"],"layout":"grid"}'

Type the same text into every running profile

POST/api/v1/running/type

Delivers text as real keystrokes into whatever is focused in each profile, in parallel. Each window samples its own timing rather than mirroring one keystroke stream: forty sessions typing the same password with millisecond-identical intervals is itself a correlation signal, which is the flaw in mirroring input verbatim. Click into the right field in each window first — the keystrokes go wherever focus already is.

Body

FieldTypeWhat it is
idsrequiredstring[]Running profiles to type into.
textrequiredstringNewlines are sent as Enter.
optionsobjectcharactersPerMinute, typoRate, thinkingPauses.

Returns

{ ok: n, skipped: n, failures: [{ profileId, reason }] }

Send every running profile to a URL

POST/api/v1/running/navigate

Navigates the focused page of each running profile to url. A profile with no page open gets a new tab rather than being reported as a failure, since that is what was asked for.

Body

FieldTypeWhat it is
idsrequiredstring[]Running profiles to navigate.
urlrequiredstringAbsolute URL.

Returns

{ ok: n, skipped: n, failures: [{ profileId, reason }] }

Start a profile

POST/api/v1/profiles/:id/start

Launches the browser with the profile’s fingerprint, proxy and cookies already applied, and returns the DevTools endpoint to attach to. A profile with a proxy is not started until that proxy has answered — see the 502 below. Starting an already-running profile returns the existing session rather than launching a second one. On macOS, and only when the perProfileDockIcon setting is on — it is off by default — a visible launch also gets a Dock tile of its own, drawn for the project: its colour, its glyph and its name, which is what identifies a window at Dock size. A launch through this route wears an API ribbon on that tile — a blue band across the top-left corner — so a browser a script opened is tellable from one somebody opened by hand. The tiles come from a small pool of window identities rather than one per profile, because macOS greets every bundle identifier it has not seen with a notification banner, for ever; the pool bounds that at the most browsers you have ever had open at once. Re-dressing a slot for its next occupant costs about 0.7s at launch, and nothing when the slot has not changed hands. Pass offscreen for any run nobody is going to watch — it parks the window off the edge of the screen and skips the Dock identity, which is what you want for scraping, warm-up and checks. It is not the same as headless: the browser is still headed, so it keeps the real User-Agent and the real GPU.

Path parameters

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat it is
headlessbooleanOverride the profile’s own launch mode for this run. Changes the User-Agent to HeadlessChrome unless the profile sets one; prefer offscreen.
offscreenbooleanRun headed but off-screen, and skip the macOS Dock identity. For work the user should not have to watch.
urlstringOpen this URL instead of the profile’s start pages.

Returns

{ profileId, status, pid, debugPort, wsEndpoint, cdpUrl, startedAt, error }

Errors

  • 400 — an option this route does not take, or one of the wrong type, named.
  • 409 — the concurrency limit in settings is already reached; or the profile was stopped (or trashed) before it finished starting, and no browser was left open.
  • 502 — the profile has a proxy and that proxy did not answer, so nothing was started. The proxy is checked before the browser is opened, because Chromium falls back to a direct connection for anything its proxy refuses: launching anyway gives you a window that works, a page that loads, and an identity browsing from this computer’s own address, reported by nothing. A gateway that answers plain HTTP and refuses CONNECT is refused too — it looks healthy and every HTTPS page behind it fails. The message names the proxy and quotes what it said. A successful check is reused for a minute, so starting forty profiles on one gateway costs one round trip. The same 502 covers traffic inspection: with inspect.enabled on, Mosaic proves the inspecting proxy forwards through it — by asking the inspector for the exit address and comparing it with the one just measured for this profile — and refuses when the two differ, naming the upstream to set. An inspector going out on its own is the same failure as a dead gateway, reached by a different route.
  • 409 — the profile is already open in a browser this app did not start. Chromium leaves a SingletonLock behind, and a second process finding a live one hands over its command line and exits; the message names the process id to close. A lock whose process is dead is cleared automatically, so this only ever means a real browser is holding the profile.
curl -X POST -H "Authorization: Bearer $KEY" \
  "$BASE/profiles/$ID/start" | jq -r .wsEndpoint

Stop a profile

POST/api/v1/profiles/:id/stop

Shuts the browser down properly rather than killing it: the request goes over DevTools so Chromium runs beforeunload handlers, tears down tabs, serialises the session and flushes its cookie and storage databases before exiting. It escalates to a signal only if that does not finish. This is why a clean stop is not instantaneous — and why the tabs come back next time.

Path parameters

FieldTypeWhat it is
idrequiredstringThe record’s id.

Returns

{ stopped: true }

Read live status

GET/api/v1/profiles/:id/status

The current session for one profile. Returns an idle record rather than a 404 when the profile is not running, so callers can poll without special-casing.

Path parameters

FieldTypeWhat it is
idrequiredstringThe record’s id.

Returns

{ profileId, status, pid, debugPort, wsEndpoint, startedAt, error }

List every running profile

GET/api/v1/running

Everything currently open, across all projects. Each row carries inspectedBy — the inspecting proxy that browser was launched through, or null. It answers a different question from GET /settings: the setting says what the next launch will do, this says what an open browser is doing, and they disagree for as long as a profile started before the switch is still open. Chromium takes its proxy from the command line and there is no way in from outside, so turning traffic inspection on does not reach a browser that is already running — restarting it is what moves it.

Returns

{ running: RunningProfile[] }

Stop everything

POST/api/v1/running/stop-all

Cleanly stops every running profile, in parallel. stopped is true only when every browser closed; one that would not is named in failures with the reason, never folded into a success.

Returns

{ stopped: boolean, profiles: [{ id, name }], failures: [{ id, name, reason }] }

Next Try it on your own accounts

Run the API on your own profiles

The REST API and the MCP server are part of Pro: $49 a month, with the first 14 days free. Free has every browser feature and no API key.