Profiles REST API

Profiles

The unit of work. A profile is an identity, a proxy, a cookie jar and a browser data directory.

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

FieldTypeWhat it is
osstringComma-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.
projectIdstringRestrict to one project. Omit to list every project.
searchstringCase-insensitive match against name, note and tags.
tagstringExact tag match.
limitnumberHow many profiles to return. Omit for all of them; the cap is 1000.
offsetnumberHow many matching profiles to skip, for paging through a large collection.
sortstringOne 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.
orderstringasc 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

FieldTypeWhat it is
projectIdstringTarget 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.
namestringDisplay 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}”.
startAtnumberThe first number the names take, 0–1000000. Omit to continue after the highest in use. Create-only, like count.
countnumberHow 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.
notestringFree text shown in the list and searchable.
tagsstring[]Tag names; unknown tags are created.
proxyIdstring | nullAttach a proxy from the saved library. Use this when the exit is one you keep and share across profiles; use proxy for everything else.
proxystring | ProxyConfig | ProviderPick | nullA 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.
fingerprintOptionsobjectPassed 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.
matchExitbooleanMeasure 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.
startOptionsobject{ mode: "headful" | "headless", restoreSession, windowWidth, windowHeight }.
startPagesstring[]URLs opened as tabs on launch.
extensionsstring[]Absolute paths to unpacked extensions.
templateIdstringStart 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.
oneTimebooleanMake 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

FieldTypeWhat it is
idrequiredstringThe 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat it is
…Partial<Profile>Any subset of the profile’s own fields.
proxystring | ProxyConfig | ProviderPick | nullThe 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.
pinnedbooleanKeep 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

FieldTypeWhat it is
idrequiredstringThe 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat it is
namestringDefaults to “<name> copy”.
newFingerprintbooleanDefault 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat it is
namestringName 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

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

FieldTypeWhat 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 — place is 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

FieldTypeWhat it is
idsrequiredstring[]Profiles to move. An absent or non-array value is a 400; an empty array moves nothing.
projectIdrequiredstringDestination 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 — ids is missing or not an array, projectId is 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

FieldTypeWhat it is
idsrequiredstring[]Profiles to change.
addstring[]Tags to add.
removestring[]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

FieldTypeWhat it is
idsrequiredstring[]Profiles to change.
addTagsstring[]Tags to add to each profile.
removeTagsstring[]Tags to take off each profile.
addBookmarks{ title, url }[]Bookmarks to add, skipping URLs a profile already has.
removeBookmarkUrlsstring[]Bookmarks to remove, matched on exact URL.
addStartPagesstring[]URLs to append to each profile's start pages.
startPagesstring[]Replaces the start pages outright.
proxyIdstring | nullAssign a saved proxy, or null to detach. Omit to leave alone.
proxy{ providerId, country?, state?, city?, minutes? } | string | ProxyConfig | nullRecommended: 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.
notestringReplaces each profile's note.
oneTimebooleanWhether each profile trashes itself when its browser closes.
pinnedbooleanPin 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 proxyId given does not exist.
  • 400 — a proxy line that cannot be read (quoted), or a proxy value that names no proxy, such as {}.
  • 400 — an unknown field. The fields go at the top level beside ids, never inside a patch object.
  • 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

FieldTypeWhat it is
idrequiredstringThe 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

FieldTypeWhat it is
idsrequiredstring[]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

FieldTypeWhat it is
idsrequiredstring[]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

FieldTypeWhat it is
idsrequiredstring[]The profiles to start.
staggerMsnumberGap between launches, 0–60000. Default 1000.

Returns

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

Errors

  • 400 — ids missing or empty, or staggerMs out 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

FieldTypeWhat it is
idsrequiredstring[]The profiles to stop.

Returns

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

Errors

  • 400 — ids missing or empty.
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.