# Running a profile: Mosaic API reference

> Running a profile in Mosaic’s local REST API: 8 routes, each with its method, path, fields, what it returns and the errors it names.

Source: https://mosaicbrowser.com/docs/api/lifecycle

**Profiles** REST API

## Running a profile

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

- [POSTLay the running windows out on screen](#running-arrange)
- [POSTType the same text into every running profile](#running-type)
- [POSTSend every running profile to a URL](#running-navigate)
- [POSTStart a profile](#start-profile)
- [POSTStop a profile](#stop-profile)
- [GETRead live status](#profile-status)
- [GETList every running profile](#list-running)
- [POSTStop everything](#stop-all)

## [Lay the running windows out on screen](#running-arrange)

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

| Field | Type | What it is |
| --- | --- | --- |
| `ids`required | `string[]` | 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](#running-type)

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

| Field | Type | What it is |
| --- | --- | --- |
| `ids`required | `string[]` | Running profiles to type into. |
| `text`required | `string` | Newlines are sent as Enter. |
| `options` | `object` | `charactersPerMinute`, `typoRate`, `thinkingPauses`. |

#### Returns

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

## [Send every running profile to a URL](#running-navigate)

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

| Field | Type | What it is |
| --- | --- | --- |
| `ids`required | `string[]` | Running profiles to navigate. |
| `url`required | `string` | Absolute URL. |

#### Returns

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

## [Start a profile](#start-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

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Body

| Field | Type | What it is |
| --- | --- | --- |
| `headless` | `boolean` | Override the profile’s own launch mode for this run. Changes the User-Agent to HeadlessChrome unless the profile sets one; prefer `offscreen`. |
| `offscreen` | `boolean` | Run headed but off-screen, and skip the macOS Dock identity. For work the user should not have to watch. |
| `url` | `string` | Open 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](#stop-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

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Returns

`{ stopped: true }`

## [Read live status](#profile-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

| Field | Type | What it is |
| --- | --- | --- |
| `id`required | `string` | The record’s id. |

#### Returns

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

## [List every running profile](#list-running)

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](#stop-all)

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 }] }`

## 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.

Mosaic is a desktop app

Email yourself the link and open it on your computer.
