Profiles REST API
Profiles
The unit of work. A profile is an identity, a proxy, a cookie jar and a browser data directory.
- GETList profiles
- POSTCreate a profile
- GETRead one profile
- PATCHUpdate a profile
- DELETEMove a profile to the trash
- POSTDuplicate a profile
- POSTSave a profile’s proxy to the library
- POSTPut a profile on the desktop
- POSTMove profiles between projects
- POSTAdd or remove tags in bulk
- POSTApply one change to many profiles
- POSTClear a profile’s cache, keep its logins
- POSTClear several profiles’ caches
- POSTMove many profiles to the trash
- POSTStart several profiles
- POSTStop several profiles
List profiles
GET/api/v1/profiles
Returns a page of profile summaries for a project, ordered by sort — alphabetically by name unless you say otherwise. Each row carries its live status, so a separate status call per profile is never needed. Note that the app's own list defaults to most-recently-used instead, so an unsorted call here and the Profiles screen will not be in the same order; pass sort=lastRunAt to match the screen.
Query parameters
| Field | Type | What it is |
|---|---|---|
os | string | Comma-separated platforms to keep — windows, macos, linux, android. The app uses android for its Mobile section and the other three for Profiles, because a phone and a desktop are different populations rather than one list with a toggle. Filtering happens before paging, so total counts what matched; an unrecognised value is a 400 naming it rather than an empty page that looks like an empty project. |
projectId | string | Restrict to one project. Omit to list every project. |
search | string | Case-insensitive match against name, note and tags. |
tag | string | Exact tag match. |
limit | number | How many profiles to return. Omit for all of them; the cap is 1000. |
offset | number | How many matching profiles to skip, for paging through a large collection. |
sort | string | One of name (default), createdAt, lastRunAt, os or proxy — the same five columns the app's table sorts by. Anything else is a 400 rather than a silently ignored parameter. |
order | string | asc or desc. Defaults to asc for the three text columns and desc for the two dates, which is what each is usually wanted in. A profile that has never run sorts last in both directions — null is not a date at the beginning of time. Ties break on profile id, so paging over a sorted list can never show a row twice or skip one. |
Returns
{ total, limit, offset, profiles: ProfileSummary[] } — each with status of idle | starting | running | stopping | error. total counts every row that matched the filters, not the number returned, so a caller can tell whether another page exists.
curl -H "Authorization: Bearer $KEY" \
"$BASE/profiles?projectId=$PROJECT&tag=ads"
Create a profile
POST/api/v1/profiles
Creates a profile with a freshly generated, coherence-checked fingerprint. Anything you leave out is generated or defaulted, so an empty body is valid — POST /profiles with {} is a working call. Passing count creates a batch in one call and numbers the names, because five hundred profiles sharing one name is unusable.
Two things exist so an automation needs no setup calls. projectId is optional and falls back to the system project, so you do not have to look one up. And proxy takes a plain proxy string, attached to this profile alone and saved nowhere — so a script does not have to POST /proxies first, and the Proxies page does not fill up with one row per rotating session. The shortest useful call is {"proxy":"user:pass@gw.example.com:9000"}.
A profile created with a proxy is drawn for that proxy’s exit. The proxy is measured before the identity is generated, and the country, timezone, languages and geolocation come from the address it really leaves by — so you do not pass countryCode, and you should not guess one. Until this existed, the same pasted European gateway produced America/New_York on one call and America/Chicago on the next: two draws from a share-weighted table, which is what no match looks like from outside. The lookup is cached per exact exit for five minutes, so count: 500 costs one request.
Every response carries exitMatch, always. matched: true means a measured exit decided the locale. matched: false names the reason — a gateway that did not answer, matchExit: false, or a countryCode you passed yourself, which still wins. The profiles are created either way: a dead proxy is not a reason to hand back nothing. It is the one field that separates a batch that is right from a batch that is silently wrong, and they were byte-identical before it.
A batch on one proxy is allowed, and said. With count > 1, a pasted line, a ProxyConfig or a proxyId puts every profile in the batch behind one address. That is the operator’s choice and is not refused — but sites can link accounts that share an address, so the response carries a warnings entry { code: "shared-exit", profiles, message } naming how many now share it. A provider ({"providerId": …}) mints one session, and so one exit, per profile, and is the recommended way to put a batch behind proxies. warnings is always present, empty when nothing is worth saying.
Body
| Field | Type | What it is |
|---|---|---|
projectId | string | Target project. Optional — omitting it puts the profile in the system project, the one Mosaic created at first launch and the one that cannot be deleted. That is a fixed destination rather than “wherever the app window happens to be”, so a script that never names a project gets the same answer every run. GET /projects lists them; the system one carries system: true. |
name | string | Display name. With count > 1 it is suffixed “ 1”, “ 2”, … continuing after the highest number that name already has, so a second batch of “Shop” starts at “Shop 21” rather than making a second “Shop 1”. Put {n} in the name to place the number yourself, or {n:3} to pad it to three digits — Shop-{n:3} gives Shop-001, Shop-002; a token works for a single profile too. A token that is almost one — {num}, {n:x} — is a 400 naming name, never a batch of profiles all called “Shop {num}”. |
startAt | number | The first number the names take, 0–1000000. Omit to continue after the highest in use. Create-only, like count. |
count | number | How many to create, 1–500. Default 1. Refused rather than clamped: 9000 and "abc" are both a 400 naming the field, because a batch that silently became 500 — or, on "abc", silently became none — answers 200 either way and cannot be told from the one you asked for. |
note | string | Free text shown in the list and searchable. |
tags | string[] | Tag names; unknown tags are created. |
proxyId | string | null | Attach a proxy from the saved library. Use this when the exit is one you keep and share across profiles; use proxy for everything else. |
proxy | string | ProxyConfig | ProviderPick | null | A proxy for this profile only, and the field most automations want. Pass a plain string in any format the importer reads — scheme://user:pass@host:port, user:pass@host:port, host:port:user:pass, host:port@user:pass, or a bare host:port (scheme defaults to http). Nothing is added to the Proxies page: rotating sessions are not a library, and a run that saved one row per session would leave a list nobody can read of exits that stopped existing hours ago. A ProxyConfig object is still accepted. If the proxy you pass is already saved — same type, host, port and credentials — the profile links to that entry instead, so one exit is never described twice. Send null to remove the proxy entirely, which clears proxyId as well. A line that cannot be read is a 400 quoting it. Or an object naming a provider: {"providerId": "…", "country": "US", "state": "California", "city": "Los Angeles", "minutes": 30}. That mints a proxy from that seller’s template with a session id nothing else holds — which is how two profiles are kept off one exit address — and records where it came from on the profile. Every field but providerId is optional, and one the seller’s gateway cannot spell is a 400 naming it. See Proxy providers. |
fingerprintOptions | object | Passed to the generator — deviceId, os, brand, countryCode, timezone, languages, seed. deviceId pins which machine from the catalogue the profile claims; see GET /fingerprints/devices. Naming countryCode or timezone here turns the exit lookup off: you have said where the profile is from, and a measurement whose answer would be discarded is not worth a request. |
matchExit | boolean | Measure the attached proxy and draw the identity for the address it actually exits from. On by default, which is what you want — a profile whose timezone contradicts its exit fails the first comparison every consistency scanner makes. Pass false to skip the lookup: faster, and correct only when you do not care where the profile appears to be. It is a create-only instruction and not a field a profile has, so PATCH refuses it by name. |
startOptions | object | { mode: "headful" | "headless", restoreSession, windowWidth, windowHeight }. |
startPages | string[] | URLs opened as tabs on launch. |
extensions | string[] | Absolute paths to unpacked extensions. |
templateId | string | Start from a preset. It fills in whatever you did not pass and never overrides what you did — including its bookmarks, its default note and the provider it mints from. The fingerprint is still drawn fresh, from the preset’s own options, and the proxy is still minted per profile, so a batch of forty from one preset is forty exits rather than one. |
oneTime | boolean | Make a throwaway: it is marked in the list and moves itself to the trash the moment its browser closes. Default false. |
Returns
The created Profile, plus exitMatch and warnings. With count > 1, { created, profiles: Profile[], exitMatch, warnings }. exitMatch is { matched, source: "proxy" | "direct" | "none", countryCode, timezone, ip, reason }; warnings is [{ code: "shared-exit", profiles, message }] when the batch shares one address, [] otherwise. Both are present on both shapes.
# No project, no saved proxy, no setup calls.
curl -X POST -H "Authorization: Bearer $KEY" -H 'content-type: application/json' \
-d '{"name":"Scrape","proxy":"socks5://user:pass@gw.provider.com:1080"}' \
"$BASE/profiles"
# A batch in a named project, with the identity drawn for Germany.
curl -X POST -H "Authorization: Bearer $KEY" -H 'content-type: application/json' \
-d '{"name":"Ads","projectId":"'$PROJECT'","count":3,
"fingerprintOptions":{"countryCode":"DE"}}' \
"$BASE/profiles"
# Did the identity actually get paired with the exit? Never assume it did.
curl -X POST -H "Authorization: Bearer $KEY" -H 'content-type: application/json' \
-d '{"proxy":"user:pass@gw.provider.com:9000"}' "$BASE/profiles" \
| jq '{matched: .exitMatch.matched, tz: .fingerprint.locale.timezone, why: .exitMatch.reason}'
Read one profile
GET/api/v1/profiles/:id
The complete profile, including the full fingerprint, privacy flags, companion settings, start options, cookies and extension list. This is what you would edit and send back to PATCH.
Path parameters
| Field | Type | What it is |
|---|---|---|
idrequired | string | The record’s id. |
Returns
A Profile with status attached.
Errors
- 404 — no such profile.
Update a profile
PATCH/api/v1/profiles/:id
Merges the fields you send. Anything touching how the browser launches — fingerprint, proxy, privacy, start options, extensions, extra arguments, cookies — is refused while the profile is running, because those values were baked into the process at launch and changing them would either be ignored or corrupt the session. Stop the profile first. companion is the exception: it is read by the built-in extension, which is rebuilt on every launch, so a change is accepted at any time and applies from the next start. Its fields are autoRetry (default true — reload a page that failed to load), autoRetrySeconds (default 10) and autoRetryLimit (default 3, per address, reset by a successful load).
Path parameters
| Field | Type | What it is |
|---|---|---|
idrequired | string | The record’s id. |
Body
| Field | Type | What it is |
|---|---|---|
… | Partial<Profile> | Any subset of the profile’s own fields. |
proxy | string | ProxyConfig | ProviderPick | null | The same field as on create, and the quickest way to move a profile onto a new exit: pass the line. It replaces whatever the profile was using, saved proxy included, unless you send proxyId in the same body. null removes the proxy entirely and clears proxyId; omitting the field leaves the proxy alone. |
pinned | boolean | Keep the profile at the top of GET /profiles in every order, an explicit sort included — a pin is set by hand to keep a row in reach. Not launch state, so it is accepted while the profile runs. |
Returns
The updated Profile.
Errors
- 400 — the profile is running and the patch touches launch state.
- 404 — no such profile.
Move a profile to the trash
DELETE/api/v1/profiles/:id
Stops the profile if it is running, then moves it to the trash with its browser data intact. Nothing is destroyed: it stays restorable for 30 days and is purged on the first start after that. Use the trash endpoints to restore or purge immediately. A browser that will not close is a 400 naming the profile, and the profile stays on the list — a trashed row has no Stop button, so hiding a running window behind one is the worse outcome.
Path parameters
| Field | Type | What it is |
|---|---|---|
idrequired | string | The record’s id. |
Returns
{ deleted: true, stopped } — stopped says whether a browser was closed on the way.
Errors
400— its browser would not close.
Duplicate a profile
POST/api/v1/profiles/:id/duplicate
Copies settings, extensions and start pages. By default the copy gets a brand-new identity, because a duplicate is a different machine — two profiles sharing one fingerprint is exactly the correlation an anti-detect browser exists to prevent. Pass newFingerprint: false only when you deliberately want a clone.
Path parameters
| Field | Type | What it is |
|---|---|---|
idrequired | string | The record’s id. |
Body
| Field | Type | What it is |
|---|---|---|
name | string | Defaults to “<name> copy”. |
newFingerprint | boolean | Default true. False copies the identity verbatim. |
Returns
The new Profile.
Save a profile’s proxy to the library
POST/api/v1/profiles/:id/proxy/save
Promotes a profile’s pasted, not-yet-saved proxy into the Proxies library and links the profile to it, so the same address can be reused and edited in one place. Refused by name when there is nothing to save: a profile already on a saved proxy, a profile with no proxy, or a provider-minted session — that last one is one launch’s session id, not a reusable address, and saving it would pin many profiles behind one exit.
Path parameters
| Field | Type | What it is |
|---|---|---|
idrequired | string | The record’s id. |
Body
| Field | Type | What it is |
|---|---|---|
name | string | Name for the saved proxy. Defaults to “host:port”. |
Returns
The new saved Proxy.
Errors
400— the profile has no custom proxy, already uses a saved one, or its proxy is provider-minted.404— no profile has that id.
Put a profile on the desktop
POST/api/v1/profiles/:id/shortcut
Writes a shortcut that opens this one profile: a small app bundle on macOS, a .lnk on Windows, a .desktop file on Linux, wearing the project’s colour and glyph. Opening it starts the profile’s browser without the Mosaic window — Mosaic runs in the background for as long as the browser is open (the proxy relay, the cookie key and the clean shutdown live there) and quits when it closes. If Mosaic is already running, the shortcut hands the profile to it instead. Making a shortcut again for the same profile in the same place replaces the earlier one, including after a rename. A file of somebody else’s at the same name is left alone and the new one takes a suffix. The shortcut is removed when the profile is purged — not when it is trashed, because a trashed profile can come back.
Path parameters
| Field | Type | What it is |
|---|---|---|
idrequired | string | The record’s id. |
Body
| Field | Type | What it is |
|---|---|---|
place | "desktop" | "applications" | Default desktop. applications is ~/Applications on macOS, the Start menu on Windows, ~/.local/share/applications on Linux. |
Returns
{ path, place, platform, profileId, profileName } — path is the file or bundle written.
Errors
400—placeis not one of the two, an unknown field was sent, or the profile is in the trash.404— no profile has that id.
Move profiles between projects
POST/api/v1/profiles/bulk/move
Reassigns profiles to one project. Nothing about a profile changes except which project it belongs to — the fingerprint, proxy, cookies, logins and history are untouched, and a running browser keeps running. Tag names travel and are registered in the destination project, because a tag’s colour is a row owned by a project: without that a moved profile keeps its tag words and silently loses their colours. A profile already in the target is not counted as moved, and one whose file cannot be read comes back in missing rather than being silently skipped — this used to answer { moved: ids.length } whatever happened.
Body
| Field | Type | What it is |
|---|---|---|
idsrequired | string[] | Profiles to move. An absent or non-array value is a 400; an empty array moves nothing. |
projectIdrequired | string | Destination project. One that does not exist is a 400 naming the id, and nothing is moved. |
Returns
{ count, moved: Array<{ id, name, from }>, missing: string[] } — count is the number and moved is the list.
Errors
400—idsis missing or not an array,projectIdis missing, or no project has that id.
Add or remove tags in bulk
POST/api/v1/profiles/bulk/tags
Applies tag changes across many profiles. Adds and removals are applied in that order, so a tag in both lists ends up removed.
Body
| Field | Type | What it is |
|---|---|---|
idsrequired | string[] | Profiles to change. |
add | string[] | Tags to add. |
remove | string[] | Tags to remove. |
Returns
{ updated: n }
Apply one change to many profiles
POST/api/v1/profiles/bulk/edit
The general-purpose bulk operation: one change set, applied to every profile in ids. Every field is optional and every list is additive or subtractive against what each profile already has, so this never silently replaces a collection — adding a bookmark that a profile already has is a no-op rather than a duplicate, and omitting a field leaves it untouched. The common mistake is expecting startPages to append: it replaces, because start pages are a short deliberate set. Use addStartPages to add to what is there. A profile that cannot be written is reported in failures and the rest of the batch still runs.
Body
| Field | Type | What it is |
|---|---|---|
idsrequired | string[] | Profiles to change. |
addTags | string[] | Tags to add to each profile. |
removeTags | string[] | Tags to take off each profile. |
addBookmarks | { title, url }[] | Bookmarks to add, skipping URLs a profile already has. |
removeBookmarkUrls | string[] | Bookmarks to remove, matched on exact URL. |
addStartPages | string[] | URLs to append to each profile's start pages. |
startPages | string[] | Replaces the start pages outright. |
proxyId | string | null | Assign a saved proxy, or null to detach. Omit to leave alone. |
proxy | { providerId, country?, state?, city?, minutes? } | string | ProxyConfig | null | Recommended: mint from a proxy provider. Each profile mints its own — its own session id, and so its own exit address, which is what makes a proxy source meaningful across a selection. Sets proxyId to null, since an inline proxy and a library row are two answers to one question. Also accepted: one address for the whole selection, as a pasted line in any format the importer reads or a ProxyConfig, read once and refused as a 400 quoting it if it cannot be read; a line that matches a saved proxy links to it. That puts every selected profile behind one IP, which sites can use to link the accounts, so it is the operator’s choice and the response carries a shared-exit warning. null detaches. A value naming no proxy — {}, a disabled config — is a 400, never a quiet detach. |
note | string | Replaces each profile's note. |
oneTime | boolean | Whether each profile trashes itself when its browser closes. |
pinned | boolean | Pin each profile to the top of the list, or unpin it. |
Returns
{ changed: n, skipped: n, failures: [{ id, name, reason }], warnings: [{ code: "shared-exit", profiles, message }] } — warnings names how many profiles this edit put on one address (a saved proxyId or a pasted proxy), and is [] otherwise.
Errors
- 400 — the
proxyIdgiven does not exist. - 400 — a
proxyline that cannot be read (quoted), or aproxyvalue that names no proxy, such as{}. - 400 — an unknown field. The fields go at the top level beside
ids, never inside apatchobject. - 400 — the body names no field to change.
{"ids":[…]}alone used to answer{"changed":0,"skipped":N}, which reads like N profiles were passed over.
curl -X POST http://127.0.0.1:PORT/api/v1/profiles/bulk/edit \
-H "Authorization: Bearer $MOSAIC_KEY" -H "Content-Type: application/json" \
-d '{"ids":["p1","p2"],"addTags":["warm"],"addBookmarks":[{"title":"Docs","url":"https://example.com"}]}'
Clear a profile’s cache, keep its logins
POST/api/v1/profiles/:id/clear-cache
Empties the browser caches — the HTTP cache, code cache, GPU and shader caches, the same directories an export leaves out — and nothing else. Cookies, logins, local storage, IndexedDB, history and extensions stay, so the profile is still signed in everywhere it was. For shrinking a profile and for clearing a stale cached script without logging out. Refused while the profile runs: Chromium holds those files open, and the profile is never stopped on your behalf.
Path parameters
| Field | Type | What it is |
|---|---|---|
idrequired | string | The record’s id. |
Returns
{ id, name, files, bytes } — what was removed.
Errors
- 400 — the profile is running.
- 404 — no such profile.
Clear several profiles’ caches
POST/api/v1/profiles/bulk/clear-cache
The route above over a list. A running profile is skipped, by name, rather than failing the batch or being stopped; anything that could not be cleared is in failures with its reason. By id: there is no form of this that means all of them.
Body
| Field | Type | What it is |
|---|---|---|
idsrequired | string[] | Profile ids. |
Returns
{ cleared: [{ id, name, files, bytes }], skipped: [{ id, name, reason }], failures: [{ id, name, reason }], files, bytes }
Move many profiles to the trash
POST/api/v1/profiles/bulk/delete
Stops each profile if running, then moves it to the trash. Reversible, like every other delete. A browser that will not close keeps its profile on the list — a trashed row has no Stop button, so hiding a running window behind one is the worse outcome — and it is named in failures rather than counted away.
Body
| Field | Type | What it is |
|---|---|---|
idsrequired | string[] | Profiles to trash. |
Returns
{ deleted: n, trashed, stopped, failures } — stopped lists the ones whose browser was closed, failures names the ones that were left alone and why.
Start several profiles
POST/api/v1/profiles/bulk/start
Launches each profile in turn with a pause between them. Sequential on purpose, and that is a correctness point rather than politeness. POST /profiles/:id/start refuses when maxConcurrentProfiles is already reached, and it counts sessions that are still starting as well as running — so forty parallel requests would every one read the count before any had registered, all pass the check, and quietly exceed the cap the operator set. Awaiting each launch is what makes that setting mean what it says. The pause matters on its own too: every launch spawns a browser with its own renderer, GPU process and network service, and twenty at once is how a laptop stops responding. A profile that is already up is skipped, not a failure; one that cannot start costs that row and nothing else.
Body
| Field | Type | What it is |
|---|---|---|
idsrequired | string[] | The profiles to start. |
staggerMs | number | Gap between launches, 0–60000. Default 1000. |
Returns
{ ok, skipped, failures: [{ profileId, reason }] }
Errors
400—idsmissing or empty, orstaggerMsout of range.
Stop several profiles
POST/api/v1/profiles/bulk/stop
Closes each through Browser.close, so Chromium flushes its cookie jar rather than being killed mid-write. Concurrent, unlike starting: there is no cap on stopping and nothing to protect — every one of these is already holding the resources it is about to release. A profile that is not running is skipped.
Body
| Field | Type | What it is |
|---|---|---|
idsrequired | string[] | The profiles to stop. |
Returns
{ ok, skipped, failures: [{ profileId, reason }] }
Errors
400—idsmissing or empty.
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.