System REST API
Application
Health, engines, settings and the MCP server. Three groups of the desktop app’s own actions are deliberately not here: minting and revoking API keys, because authenticating with a key to mint more keys defeats the point of having them; opening files and folders on the host, because a remote caller asking the machine to open a path is a request to run something, not to manage a profile; and native file dialogs, which have nobody in front of them to answer.
- GETIs there a newer Mosaic
- POSTFetch and verify a newer version
- POSTRestart into the downloaded version
- GETWhat is wrong with this library
- GETVersion, platform and where the data lives
- POSTRestart the MCP server
- GETHealth check
- GETStream profile status changes
- GETWhat the API is doing right now
- GETList browser engines
- GETWhat the engine manifest offers
- POSTInstall the engine
- GETThe plan this install is on
- DELETEDelete the account, for good
- GETRead settings
- PATCHUpdate settings
Is there a newer Mosaic
GET/api/v1/updates
Reads the release manifest at downloads.mosaicbrowser.com/<channel>/latest-mac.json (latest-win.json on Windows) — public, no credential — and returns the updater's whole state, the same object the window draws. phase is one of idle, checking, up-to-date, available, downloading, ready, installing, error. A check that could not run is reported as itself in error, never as available: false: error.quiet is true when it was only the network — offline, a timeout, a server that did not answer, a channel with nothing published — which is retried by itself and never shown as an alert. replaceable and blocked are a separate question from whether an update exists: a development build, one running outside /Applications, or a Windows copy unpacked from the portable zip rather than installed, is told the truth about what exists and why it cannot install it in place. With updateAutoDownload on, a found update starts downloading in the background. PATCH /settings sets updateCheck (auto, manual, off), updateAutoDownload and updateChannel.
Query parameters
| Field | Type | What it is |
|---|---|---|
cached | boolean | true reads the state without contacting anything. Default false. |
Returns
{ phase, current, channel, latest: { version, notes, publishedAt, size } | null, progress, error: { id, stage, title, detail, quiet } | null, lastCheckedAt, restartWhenIdle, runningProfiles, replaceable, blocked, autoDownload, checkMode, installerUrl }
Errors
400—cachedis nottrueorfalse.
Fetch and verify a newer version
POST/api/v1/updates/download
Downloads the build for this platform and architecture and installs nothing. The archive is hashed as it streams and compared with the SHA-256 in the manifest before it is unpacked or run; the download is asked its own version and has to agree (the bundle's Info.plist on macOS, the installer's version resource on Windows); and when the running copy is Developer-ID signed, the new one must be signed by the same team (GK97ADS394) and pass codesign --verify --deep --strict. Windows builds are not code-signed, so there the checksum is the proof. Allowed while profiles run — downloading touches nothing they use; restarting is where they are counted. Progress is in GET /updates.
Returns
{ version, bytes }
Errors
400— this build is not an installed application, there is no newer version, the server could not be reached, or the archive failed its checksum, version or signature check (nothing is installed).
Restart into the downloaded version
POST/api/v1/updates/apply
Swaps the downloaded bundle in and relaunches Mosaic. It never restarts under a running profile. With browsers open, when: "now" is refused with the count unless stopProfiles: true asks for them to be stopped first, each through the browser's own exit; when: "when-idle" records the wish and Mosaic restarts by itself once the last profile closes; when: "cancel" withdraws it. The response is sent before the process ends: a caller that got nothing back cannot tell a restart from a crash. On macOS the old application is moved aside rather than deleted and swept on the next start, and a swap that fails puts it back. On Windows Mosaic starts the verified installer silently (/S --updated --force-run) and exits; the installer replaces the install where it is and starts the new version. The data directory is never touched on either.
Body
| Field | Type | What it is |
|---|---|---|
when | "now" | "when-idle" | "cancel" | Default now. |
stopProfiles | boolean | With when: "now", stop running profiles first. Default false. |
Returns
{ version, restarting, waitingFor?, cancelled? }
Errors
400— nothing has been downloaded, profiles are running and neitherwhen-idlenorstopProfileswas sent, an unknown option, or this build is not an installed application.
What is wrong with this library
GET/api/v1/overview
One pass over every profile and proxy, returning the counts a dashboard needs and the ids behind each one, so a caller can act without going looking. The three findings worth acting on: profiles claiming a timezone their proxy's measured exit contradicts — which is the first comparison every consistency scanner makes and drifts on its own whenever a pool re-homes a subnet; profiles describing a machine nobody sells, which a detector holding a table of real devices reads as masking; and proxies that are unreachable, refusing CONNECT, blocklisted or rotating. no-proxy and never-run are marked info because neither is a fault — they are facts worth knowing, since direct profiles all share one address and an empty cookie jar is itself scored as risk. Auditing needs the whole fingerprint, so this reads every profile off disk: call it when a screen opens, not on a timer. Scope it to what you are looking at. projectId and os narrow the pass to one project and one set of platforms — the same two filters GET /profiles takes — and the response echoes them in scope. Unscoped it counts every project, which is right for “what is wrong anywhere” and wrong for anything rendered over one project’s list: the app’s own health bar used to do exactly that and named profiles the table under it could not show.
Query parameters
| Field | Type | What it is |
|---|---|---|
projectId | string | Count only this project. Absent counts every project. |
os | string | Comma-separated platforms to count: windows, macos, linux, android. An unknown one is a 400 naming the valid set. |
Returns
{ profiles: { total, running, neverRun, inTrash }, proxies: { total, live, warn, down, untested }, issues: [{ id, subject: "profile"|"proxy", severity: "high"|"medium"|"info", title, summary, detail, count, ids[], names[] }], scope: { projectId, os }, checkedAt } — summary is the fault in a few words with no count in it, and names[i] is ids[i].
Version, platform and where the data lives
GET/api/v1/app
The same facts the Settings page shows: version, platform, architecture, the resolved userData directory, and whether the MCP server is up. Useful for a script that needs to find the profile store on disk without guessing at platform conventions. hostLanguage is the tag this machine's ICU resolves to — on a stock Chromium engine it is the only value Intl will ever report, so a profile whose locale.languages does not start with it is claiming a language its own date and number formatting contradicts.
Returns
{ version, platform, arch, dataDir, apiRunning, mcpRunning, hostLanguage }
Restart the MCP server
POST/api/v1/mcp/restart
Stops the MCP server and starts it again on the port in settings, or leaves it stopped if mcpEnabled is false. Changing mcpPort through PATCH /settings does not move a running server — this is what applies it. Agents connected over MCP are disconnected and must reconnect.
Returns
{ running, port }
Health check
GET/api/v1/health
The one endpoint that does not require a key, so a supervisor can check the API is up without holding a credential. It is also the only one that answers while nobody is signed in to Mosaic: every profile, proxy, setting and API key belongs to an account, so signed out every other route — and every MCP request — is 401 with { error, signIn: true } and a sentence saying to sign in, before any key is looked at. signedIn says which of the two you will get.
Returns
{ ok: true, name: "mosaic", apiVersion: 1, signedIn }
Stream profile status changes
GET/api/v1/events
A server-sent event stream that stays open and reports profile status as it changes, so an automation client does not have to poll /running on a timer. The first message is always running, carrying everything running at the moment you connected — without it a client that attached mid-session would know nothing until the next change. After that: a profile event whenever any profile starts, becomes ready, stops or errors, and a retired event when a one-time profile deletes itself after its run. A : keep-alive comment every 25 seconds keeps intermediaries from reaping an idle connection; SSE ignores comment lines, and EventSource reconnects on its own if one is dropped anyway. The response never ends, so do not await it as an ordinary request.
Returns
A text/event-stream. Events: running { profiles: RunningProfile[] }, profile RunningProfile, retired { profileId }.
curl -N -H "Authorization: Bearer $KEY" "$BASE/events"
What the API is doing right now
GET/api/v1/activity
Everything happening from outside this window in the last five minutes, which is what the interface’s own API activity page renders. Four parts. callers is which keys have called, by the name their owner gave them, with what each last called and how many calls it has made inside the window — REST and MCP are reported separately, because the same key used from a script and from an assistant is two things doing two jobs. runningUnderApi is how many of the profiles those calls touched are running right now. profiles is every profile the API has touched, flat and newest first. Each entry is a whole ProfileSummary — the same row GET /profiles returns, so a client rendering a table needs no second request per profile — alongside what the API last did to it, which key did it (keyId, keyName) and which project it is in (projectId, projectName). Flat rather than grouped by project, because grouping is a rendering decision with more than one useful answer: the interface groups by project after filtering to a single key, and a client watching one project wants the opposite. Two keys working in the same project is the case any project-first shape cannot express without listing the project twice. A profile the API deleted still appears, rebuilt from the action that recorded it and marked gone; a project the API deleted keeps the name it had, because the name is recorded on the action rather than looked up afterwards. profiles and actions cover the same retained events — they used to use different windows, so the feed could name a profile the profile list had already dropped. actions is what was done, newest first, in words rather than routes — Created profile, Attached proxy — and reads are left out entirely, because a client polling /running once a second would otherwise bury the one line that says a profile was deleted. Actions are kept in memory and capped, so the list starts empty after a restart. A script can use this to check whether another key is already working on something before it starts fighting it. Never returns a key, only its id and name; the secret is hashed in the store and does not exist in the process in any readable form. Reading this route does not itself count as activity.
Returns
{ windowSeconds, callers: [{ keyId, keyName, surface, lastAt, lastCall, calls }], runningUnderApi, profiles: [{ profile: ProfileSummary, running, lastVerb, lastAt, gone, keyId, keyName, projectId, projectName, projectGone }], actions: [{ id, keyId, keyName, surface, at, verb, subject, profileId, projectId, projectName }] }
curl -H "Authorization: Bearer $KEY" "$BASE/activity"
List browser engines
GET/api/v1/engines
The browser on this machine: whether it is installed, its version, and source — bundled (the copy inside the app, which every release carries) or installed (a newer one downloaded as an engine update; the newer of the two is the one profiles run). install is the download in progress, if any, and the last failure — a launch that answered 503 because the browser was missing can watch it arrive here. install.update is whether a newer engine is published, as the last check found it — Mosaic looks shortly after starting and every four hours while updateCheck is auto, and every GET /engines/available refreshes it. An engine update is its own channel, published apart from app releases, and optional: it is offered (here, in Settings → Engine, and as one line in the sidebar) and never installed on its own.
Returns
{ total, engines: EngineInfo[], install: { installing, lastError, update: { current, latest, size, available, checkedAt, error } | null } }
What the engine manifest offers
GET/api/v1/engines/available
Reads and validates the published browser’s manifest and answers which of its builds this machine can take, and whether it is newer than the one in use (updateAvailable, against current). Nothing is downloaded. The source is engineManifestUrl when it is set, otherwise the Mosaic account server, which is sent your session: a signed-out account gets a 400 saying to sign in; every plan, Free included, gets the browser, and any other refusal from the server is passed through as it said it. A self-hosted or loopback manifest (your own build) is never gated. The manifest itself is refused, with the field named, if any value is missing, unknown, not https, or not a 64-character hex checksum.
Returns
{ source, manifest, forHost, note, current, updateAvailable }
Install the engine
POST/api/v1/engines/install
An engine update, independent of the app: downloads the build for this machine into the data directory, verifies its SHA-256 before unpacking anything, checks the unpacked binary reports the version the manifest claims, and only then swaps it into place. About a gigabyte. The copy bundled with the app is never touched; the download is used from the next launch because it is newer. Gated like GET /engines/available; the build URL the account server hands back is a short-lived signed link, so it cannot be shared. 409 while a profile is running on a previously downloaded copy, because that is the one being replaced. A second call while a download runs waits on that download rather than starting another. A download host that fails — unreachable, a non-200, a checksum that does not match — is a 502 carrying the sentence.
Returns
{ version, executablePath, bytes }
The plan this install is on
GET/api/v1/account
Free or Pro, what the plan allows and how much of it is in use. limits.profiles, limits.creations (profiles made in all, counted by the account server per account and per computer) and limits.projects are numbers on Free and null (unlimited) on Pro — on Free they are this account’s own: freeTier is current (3 profiles, 10 made in all) or legacy for an account made before the Free plan shrank on 2026-11-03, which keeps 5 and 20; limits.devices is how many computers the account may be signed in on; limits.api and limits.mcp say whether those surfaces answer an API key. usage.creations is the account server's count as of the last time it was asked, or null when it does not apply or has not been read. source says why: subscription (a verified, paid entitlement), free (signed out, or signed in without paying), expired (a paid plan that was not renewed in time), or unconfigured (a build with no account server, which limits nothing). billing says what pays for Pro: card (a Stripe subscription, periodEnd is when it renews or ends) or crypto (a period paid ahead — periodEnd is its paid-until, nothing renews, and it stays set after the period ends so the end can be named); null for neither. interval is month or year for a card subscription. status reads paused while a break the customer took is in effect — Pro is withheld, the profiles are kept — and resumesAt says when billing and Pro come back. Pausing, resuming, changing interval and cancelling happen on the account page at mosaicbrowser.com. trustedUntil is how long the current answer holds offline before Mosaic must ask the account server again. Never carries a session token or the signed entitlement. Read-only: signing in and paying happen in the Mosaic window. The REST API itself is part of Pro — on Free every request with an API key is answered 402 with { error, feature: "api" }, and the in-browser companion keeps working because it does not use an API key.
Returns
An AccountState: { configured, plan, limits, source, signedIn, email, status, periodEnd, cancelAtPeriodEnd, billing, freeTier, interval, resumesAt, checkedAt, trustedUntil, lastError, usage: { profiles, projects, creations } }.
Delete the account, for good
DELETE/api/v1/account
Deletes the Mosaic account this install is signed in to, at the account server, and signs this Mosaic out. Irreversible: the account, its sessions on every computer and its support history go, and a live subscription is cancelled first so the card is not charged again. Every browser is stopped first (one that will not close is named and nothing is deleted), and Mosaic then restarts on its sign-in screen. The account’s profiles can never be opened again: they stay on this disk, encrypted, but the key that opened them was the account’s, and it is deleted with it — profiles, cookies and proxies were never on the server, so there is no copy anywhere else. This computer keeps its count of profiles made, so a new account here starts where the old one stopped. The body is { "password": "…" }, every time: an absent password is a 400 naming it, a wrong one is a 400 that deletes nothing, any other field is refused by name. 400 when not signed in; the operator's root account cannot be deleted from the app.
Body
| Field | Type | What it is |
|---|---|---|
passwordrequired | string | The account's password, typed again: the confirmation. |
Returns
{ deleted: true, account } — account is the AccountState after signing out.
Errors
- 400 —
passwordmissing or wrong (nothing deleted), an unknown field, or not signed in
Read settings
GET/api/v1/settings
Current application settings.
Returns
An AppSettings.
Update settings
PATCH/api/v1/settings
Merges the fields you send, after checking every one of them. A value of the wrong type or outside its range is refused rather than stored — apiPort must be a whole number between 1 and 65535, theme must be system (follows the operating system and switches when it does; the default), light or dark, trashRetentionDays at least 1. defaultEngineId is mosaic — Mosaic builds Tesseract Chrome and profiles run it, and the interface offers no other choice, because every value a stock browser can only move in JavaScript is one a service worker reads the true answer for. An unbuilt engine is a refusal naming it rather than a quiet fall-through to whatever else is installed. perProfileDockIcon is macOS-only and off by default: with it on, every visible launch runs under a copy of the browser dressed as the profile’s project, so the Dock shows which window belongs to what instead of a row of identical browser tiles. The cost is a macOS one and cannot be prevented from inside the bundle — the system registers a bundle identifier it has never seen as a newly installed application and greets it with a notification banner, once per identifier, for ever. Eleven measured attempts to suppress that (the Alerts helper removed, --disable-notifications, NSUserNotificationAlertStyle set to none and deleted outright, and combinations) all still registered. So the identities are pooled by window slot, which bounds the banners at your peak number of open browsers rather than your number of profiles — and the setting is off unless you ask for it. Off-screen and headless launches skip it regardless, so checks and warm-up runs mint nothing. Setting it to false also deletes the copies already on disk. An unknown key is also refused, and the error lists the settings that do exist, so a misspelled field is a 400 instead of a silent no-op that reports success. downloadMode decides where a download lands and where a file picker opens. "shared" is one folder for every profile — downloadDir, or null for the operating system’s own Downloads folder — which is what every browser does and what this app did for its whole life: cookies and history are separated because a site must not see across them, and a download is a file the operator asked for. "perProfile" (the default) gives each profile its own folder inside Mosaic’s data directory, opens that profile’s file picker there, deletes the files when the profile is purged, and makes them reportable and wipeable through /downloads. It is not encryption — Chromium writes the bytes itself — but it keeps the files out of the home folder and out of Spotlight. An unknown value is a 400 naming the field rather than a quiet fall-through, because a caller told their downloads are isolated when they are not is the worst answer this API could give. A change to either takes effect at each profile’s next launch; a running browser keeps the folder it resolved at startup. profileColumns is which optional columns the profile table shows — any of tags, proxy, timezone, created, lastRun, stored in that order whatever order they are sent in; a name that is not a column is a 400 naming it. Changing the API bind address or port needs a server restart to take effect. inspect is { enabled, address } and routes every launch from now on through a proxy that can read its traffic — Proxyman, Charles, mitmproxy, Fiddler, Burp. Both fields are required together and an unknown field inside it is refused by name; address is host:port (an http:// prefix is accepted and dropped) and is stored normalised. It is a debugging switch and not a privacy one: whatever is listening sees every request in plaintext. A profile with a proxy of its own needs the inspector forwarding back to Mosaic — the chain has to be browser → inspector → Mosaic → the exit — and Mosaic arranges that itself: it holds a relay on 127.0.0.1:39219 for as long as inspection is on and points the inspector at it through the inspector’s own command line, putting it back when you switch off. That relay forwards direct whenever no profile is using it, so nothing else you are watching through the inspector breaks. The chain is still measured before each window opens — by counting whether the traffic actually passed through Mosaic, which holds for rotating exits where comparing addresses cannot — and a launch is refused with a 502 rather than sent out of this computer’s address. Automatic configuration is Proxyman-only today; elsewhere the address is typed in and the refusal names the upstream to set. The one thing left to a person either way is trusting the inspector’s CA certificate, without which HTTPS pages fail to load.
Body
Partial<AppSettings>. Any subset of settings. Unknown keys are rejected.
Returns
The updated AppSettings.
Errors
400— a value of the wrong type or out of range, or a key that is not a setting.
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.