Proxies REST API

Proxies

A library of the exits you keep: named, checked on a schedule, shared between profiles, and edited in one place — edit one and every profile using it follows. It is not the only way to give a profile a proxy, and for most automation it is not the one you want: pass a proxy string in the proxy field of POST /profiles or PATCH /profiles/:id and it is attached to that profile alone, with nothing added here. Save a proxy when you will reuse it and want one place to change it; paste one when it is a session that will be gone by lunchtime.

List saved proxies

GET/api/v1/proxies

Every saved proxy, each with a usedBy count and its last check and reputation results.

Query parameters

FieldTypeWhat it is
projectIdstringRestrict to one project.

Returns

{ total, proxies: SavedProxy[] } — each with usedBy.

Save a proxy

POST/api/v1/proxies

Stores a proxy for reuse. Credentials never reach the browser’s command line — Chromium cannot accept them there — so at launch the proxy is fronted by a loopback relay that authenticates upstream on the profile’s behalf.

Body

FieldTypeWhat it is
hostrequiredstringHostname or IP.
portrequirednumberPort.
type"http" | "https" | "socks4" | "socks5"Default http.
namestringDefaults to host:port.
usernamestringUpstream username.
passwordstringUpstream password.
providerstringKnown provider id, which selects the sticky-session format.
sessionIdstringSticky-session token injected into the username.
rotationUrlstringURL hit before launch to force a new exit IP.
rotateOnLaunchbooleanMint a fresh sticky-session token at every launch: one exit for the whole run, a different one next time. Different from a rotating gateway, which changes exit on every connection — and on a rotating port this does nothing, because the gateway answers 400 Can not use -session- on rotating ports. Applied in memory and never written back. Leave it off for an identity: a logged-in profile arriving from a new city each launch is telling the site so.
countryCodestringTwo-letter code the seller states. Shown as a flag until a check measures the real exit, which then wins.
citystringStated city. Equally unverified.
projectIdstring | nullnull makes it available to every project.

Returns

The created SavedProxy.

Which proxies are being checked

GET/api/v1/proxies/checking

The ids of every proxy with a check in flight this second. A check is owned by the app rather than by the screen that started it, so it keeps running when you navigate away and when a caller disconnects — which means a caller that arrives mid-batch would otherwise have no way to tell forty running checks from none, and would start them all again. Poll this before POST /proxies/:id/check on anything you did not start yourself. An empty list means nothing is in flight, not that nothing has ever been checked: the last result lives on the proxy.

Returns

{ total, checking: string[] } — proxy ids.

Read one proxy

GET/api/v1/proxies/:id

One saved proxy, including its last check and reputation.

Path parameters

FieldTypeWhat it is
idrequiredstringThe record’s id.

Returns

A SavedProxy.

Update a proxy

PATCH/api/v1/proxies/:id

Merges the fields you send. Every profile referencing it picks up the change on next launch.

Path parameters

FieldTypeWhat it is
idrequiredstringThe record’s id.

Body

Partial<SavedProxy>. Any subset.

Returns

The updated SavedProxy.

Delete a proxy

DELETE/api/v1/proxies/:id

Removes it. Profiles using it fall back to a direct connection, so check usedBy first.

Path parameters

FieldTypeWhat it is
idrequiredstringThe record’s id.

Returns

{ deleted: true }

Check a saved proxy

POST/api/v1/proxies/:id/check

Establishes three things that fail independently, because a check that collapses them into one boolean leaves you guessing which one broke. Where it exits — IP, country, city, ISP, ASN and whether the range looks like a datacentre, measured rather than taken from what the proxy was sold as. Whether it tunnels — a second probe over CONNECT, which is the path effectively all browsing takes; a gateway can answer plain HTTP perfectly and refuse to tunnel, and that proxy passes a naive check and then loads nothing in a browser. Whether it holds still — the two probes are separate connections, so an endpoint that hands out a different exit each time is reported as rotating, which is not a fault but does mean no profile behind it can hold a timezone. Where it resolves — one more request through the same relay asks which recursive resolver served a one-time hostname, which is the exit node’s resolver rather than yours, and dnsMismatch is true when its country is not the exit’s. That is not a browser setting and no profile can change it: the gateway resolves the names we hand it. It is reported because consistency scanners score it and the deduction is large — Whoer charges −30% for “Your DNS server country differs from the IP country”, its single biggest. Measured on an IP Oasis US session with plain curl and no browser in the path: exit Verizon in the United States, resolver Google LLC in Germany and then the Netherlands. A resolver that could not be read leaves all three fields absent rather than reporting agreement, and never fails the check. A transport failure is retried up to three times before it is called a failure, because a residential pool hands out a dead exit some of the time and one observation is not a verdict; attempts says how many it took. Scoring the exit against blocklists happens here too rather than being a separate errand. The result is cached on the proxy, and a successful check corrects the stored country — unless the endpoint rotates, in which case there is no country to correct it to.

Path parameters

FieldTypeWhat it is
idrequiredstringThe record’s id.

Returns

{ ok, ip, country, countryCode, city, region, postalCode, isp, asn, hosting, timezone, latitude, longitude, latencyMs, attempts, tunnel: "ok"|"failed", tunnelError, tunnelIp, tunnelCountryCode, dnsCountryCode, dnsIsp, dnsMismatch, rotating, reputation, error }

Scan a proxy’s reputation

POST/api/v1/proxies/:id/reputation

Connecting is not the same as being usable. This resolves the exit IP against public DNS blocklists, looks for a hosting or datacentre PTR record, and scores the result. The lookups go out over your own resolver rather than through the proxy, because a proxy is not a witness to its own reputation. POST /proxies/:id/check already does this, so reach for it only to re-score an exit without re-checking the connection. It refuses a proxy that has never answered — there is no exit address to score, and running a connectivity check first would be an errand the caller did not ask for and cannot see. On a rotating endpoint the report says so: the address scored is one draw from the pool, not the exit a profile will browse from.

Path parameters

FieldTypeWhat it is
idrequiredstringThe record’s id.

Returns

{ ip, risk (0–100), verdict: "clean"|"caution"|"risky", hosting, reverseDns, isp, asn, listings[], listsChecked, notes[], checkedAt }

Errors

  • 400 — the proxy has never answered, so it has no exit address to score. Check it first.

Read a proxy list without saving

POST/api/v1/proxies/parse

Reads a pasted list — or the contents of a .txt or .csv file — and says what every line came out as. Nothing is saved; use it to preview an import.

Formats are detected per line, in any mix: host:port, host:port:user:pass, user:pass:host:port, user:pass@host:port, host:port@user:pass, scheme://user:pass@host:port with http, https, socks4 or socks5, IPv6 in brackets ([2001:db8::1]:8080, with or without :user:pass), and host port user pass separated by spaces or tabs. A trailing |US on a line is the country the seller says it is in. A table with a header row — separated by commas, tabs or semicolons, what every other anti-detect browser exports — is read by column name: host/ip/server, port, username/login, password, type/protocol, country, name, rotation_url/change_ip_url, tags, notes, or one proxy column holding a whole line. Columns it does not use are listed in ignoredColumns rather than silently dropped.

Every line comes back with a status: ok, duplicate (the same proxy earlier in this list), exists (already saved — compared against what projectId can see, or every proxy when it is absent) or error. Every one that will not be imported carries a reason that names the part it could not read — the port that is not a number, the scheme that is not a proxy type, the IPv6 address without brackets. For a provider whose gateway ports name their protocol — CatProxies — a line with no scheme takes its type from the port; an explicit socks5:// always wins.

Body

FieldTypeWhat it is
textrequiredstringThe list, up to 5 MB.
typestringhttp, https, socks4 or socks5 — assumed when a line does not say. Default http.
projectIdstringCompare against the proxies this project can see when deciding exists.

Returns

{ format: "lines"|"csv"|"tsv"|"semicolon", columns, ignoredColumns, lines: [{ line, text, status, reason, proxy, existingId }], counts: { ok, duplicate, exists, error }, proxies, errors } — proxies and errors are the ok and error lines on their own, as this route always returned.

Import a proxy list

POST/api/v1/proxies/import

The same reading as POST /proxies/parse, and every ok line is saved, in one write. A proxy that is already saved is skipped, not added twice, and named in skipped with the row it matches; a line that could not be read is in errors with its reason. Nothing is dropped without a line saying so.

namePrefix names the new rows Shop 1, Shop 2, … and continues after the highest number that prefix already has, so a second import does not make a second Shop 1; a table’s name column wins over it. With check: true every new proxy is checked after it is saved, concurrency at a time, and checked names which answered and which did not — the import itself has already succeeded either way.

Body

FieldTypeWhat it is
textrequiredstringThe list, in any format POST /proxies/parse reads.
typestringDefault type for lines that do not say.
projectIdstring | nullProject to file them under; null or absent for every project.
namePrefixstringUp to 60 characters. Names the rows <prefix> <n>.
tagsstring[]Added to every imported proxy, beside any tags a table row carried.
checkbooleanCheck every new proxy after import. Default false.
concurrencynumberChecks in flight at once, 1–16. Default 6.

Returns

{ added, proxies, skipped: [{ line, text, reason }], errors: [{ line, text, reason }], counts, checked: { checked, ok: [{ id, name, ip, countryCode }], failed: [{ id, name, reason }] } | null }

curl -X POST -H "Authorization: Bearer $KEY" -H 'content-type: application/json' \
  -d '{"text":"socks5://u:p@1.2.3.4:1080\n5.6.7.8:8080:u:p|DE","namePrefix":"Shop","check":true}' \
  "$BASE/proxies/import"

Export proxies as text

GET/api/v1/proxies/export

The saved proxies as a list another tool reads — including this one, so an export imports back unchanged. url is scheme://user:pass@host:port with the credentials percent-encoded, the one form every importer understands; colon is host:port:user:pass, what most sellers print, which cannot carry a type; csv has a header row with name,type,host,port,username,password,country,rotation_url,tags,notes. Passwords are in the output, because a list without them imports nothing.

Query parameters

FieldTypeWhat it is
formatstringurl (default), colon or csv.
idsstringComma-separated proxy ids. An unknown one is a 400 naming it.
projectIdstringOnly the proxies this project can see.

Returns

{ format, count, text }

Check several proxies

POST/api/v1/proxies/check

Checks a batch of saved proxies, a few in flight at a time — a hundred at once would exhaust the socket table before the seller rate-limits you — and records each result on its proxy, as the single check does. The answer names every proxy that failed and why, because “checked 40” over eight that did not answer is a count rather than an answer. By id: there is no form of this that means all of them.

Body

FieldTypeWhat it is
idsrequiredstring[]Saved proxy ids.
concurrencynumberChecks in flight at once, 1–16. Default 6.

Returns

{ checked, ok: [{ id, name, ip, countryCode }], failed: [{ id, name, reason }] }

Rotate a proxy through its change-IP link

POST/api/v1/proxies/:id/rotate

For a mobile or residential port whose seller gives it a change-IP URL (rotationUrl). Mosaic fetches the link — from this machine, never through a browser carrying your cookies — waits a few seconds for the modem to re-dial, and checks the proxy. The answer says what the exit was, what it is now and whether they differ: a link answering 200 is not the same as the IP changing, and only the second is what you wanted. A link that answers anything but 2xx is a 502 naming the status, and nothing was rotated; a proxy with no link is a 400. The check is recorded on the proxy, as POST /proxies/:id/check does.

Path parameters

FieldTypeWhat it is
idrequiredstringThe record’s id.

Returns

{ id, name, status, before, after, changed, check } — changed is null when either exit is unknown.

Errors

  • 400 — the proxy has no change-IP link.
  • 502 — the link answered with an error or could not be reached.

Rotate several proxies

POST/api/v1/proxies/rotate

The route above over a list, one after another — several rotations of one seller’s ports at once is a pattern sellers rate-limit. One proxy failing does not stop the rest; every failure is named. By id: there is no form of this that means all of them.

Body

FieldTypeWhat it is
idsrequiredstring[]Saved proxy ids.

Returns

{ rotated: [{ id, name, status, before, after, changed, check }], failures: [{ id, name, reason }] }

Attach proxies to profiles

POST/api/v1/proxies/assign

Four modes. one points every listed profile at proxyId, and null detaches. roundRobin deals proxyIds out in order, one per profile, wrapping when there are more profiles than proxies. unique gives each profile a proxy no other profile holds, and is refused — naming how many are free — when there are not enough, rather than quietly wrapping: the point of owning forty proxies is forty exits. country picks proxies that come out in country — the measured country where a check has run, the declared one otherwise — unused ones first, then the least shared. For unique and country the pool is proxyIds when given, otherwise every saved proxy the profile’s project can see; a proxy scoped to another project is never handed out. Every choice is made before any profile is written, so a refusal changes nothing. reused says whether any proxy is now shared by two of these profiles — sometimes deliberate, sometimes the bug that links two accounts — and when it is, warnings carries a shared-exit entry naming the most profiles on any one proxy. Sharing is allowed; it is never silent.

Body

FieldTypeWhat it is
profileIdsrequiredstring[]Profiles to change, in order.
modestringone, roundRobin, unique or country. Default roundRobin when proxyIds is given, one otherwise.
proxyIdstring | nullMode one: one proxy for all of them, or null to detach.
proxyIdsstring[]The pool. Required for roundRobin.
countrystringMode country: a two-letter code, like US.

Returns

{ assigned, mode, reused, perProxy, assignments: [{ profileId, profileName, proxyId, proxyName }], warnings: [{ code: "shared-exit", profiles, message }] }

Move proxies between projects

POST/api/v1/proxies/scope

Sets which project a batch of proxies belongs to. null makes them available in every project; a project id pins them to that one. Profiles keep working across the change because they reference a proxy by id, not by scope. Proxies are created inside the project that added them precisely so that one customer’s list does not fill every other project’s dropdown.

Body

FieldTypeWhat it is
idsrequiredstring[]The proxies to move.
projectIdstring | nullnull for every project. Default null.

Returns

{ changed: n }

Errors

  • 400 — ids is missing or empty.

List known providers

GET/api/v1/proxies/providers/list

Providers Mosaic recognises, with their gateway conventions and how each encodes a sticky session in the username. offered marks the ones the app lists in its provider picker; the rest are still matched by hostname and still name themselves on a proxy already saved under them, they are simply not offered for a new one.

Returns

{ total, providers: ProviderPreset[] }

Check a proxy without saving it

POST/api/v1/proxy/check

Same check as a saved proxy, against a config you pass inline.

Body

ProxyConfig. type, host, port, username, password.

Returns

A ProxyCheckResult.

Match the identity to the proxy exit

POST/api/v1/profiles/:id/match-exit

Resolves where this profile actually comes out — through its proxy, or the direct connection when it has none — and brings the fingerprint into line with it. This is the same thing the launcher does on every start when matchExitOnStart is true; the endpoint exists so it can be done in bulk without opening a browser, which is what you want after attaching a proxy to profiles that already exist. It is also what the profile editor does by itself when you open a profile whose exit is known — there is no button for it on any surface, because a profile whose timezone contradicts its address should never be waiting on somebody to notice. Timezone and geolocation are moved whenever they disagree with the exit: neither is visible to a site that already knows the profile, because a session reads what the browser reports now rather than what it reported when it was created. The language is moved only for a profile that has never run. Accept-Language rides every single request, so switching a profile that has been speaking de-AT to a site for a month over to en-US tells that site something true about itself; for a profile with history the mismatch is reported in skipped instead of being applied. language: "move" overrides that, and it exists because the default, left in place for ever, is a dead end rather than a policy: a profile drawn on a European address and later put behind a US exit will send de-AT on every request it ever makes, and “report it and do nothing” leaves no way to fix it. Send move when the profile is not signed in anywhere; when it is, the change is one those sites see. Refused while the profile is running, because the identity was baked into the process at launch.

Path parameters

FieldTypeWhat it is
idstringProfile id.

Body

FieldTypeWhat it is
language"auto" | "move"What to do with Accept-Language on a profile that has already run. auto (the default) leaves it and reports the mismatch in skipped; move changes it to the exit country’s language. Any other value is a 400 naming the field.

Returns

{ changed, fields, skipped, from, to, reason, exitIp, profile } — fields names what moved, skipped names what deliberately did not and why.

Errors

  • 400 — the profile is running, or the proxy did not resolve so there is no exit to match against.
curl -X POST -H "Authorization: Bearer $KEY" "$BASE/profiles/$ID/match-exit"

Match many identities to their exits

POST/api/v1/profiles/bulk/match-exit

The route above, over a list, and it is the one the problem actually has the shape of. A proxy gets edited — a different country flag in the username, a new session token, a swapped gateway — and every profile behind it is now claiming a timezone, a set of coordinates and a keyboard layout that disagree with where it comes out. That mismatch is the first comparison every consistency scanner makes. Calling the singular route forty times means sequencing forty live proxy checks yourself, and the first dead proxy ends the loop with a 400. Here no single profile can fail the batch: the checks run one after another, deliberately rather than in parallel, because forty simultaneous requests to a residential pool is the traffic shape that gets an account rate-limited — which would then come back as forty proxies that "did not resolve". Three outcomes are reported separately and on purpose. changed counts the ones that moved. unresolved counts the ones whose exit could not be read at all, and those were not changed — it is not the same as agreeing, and reporting it as a success would be the 200-that-did-nothing this API refuses. Everything left over already agreed, which is the good case. A running profile counts as unresolved with a reason naming it, because its identity was baked into the process at launch.

Body

FieldTypeWhat it is
idsstring[]Profile ids. Required.
language"auto" | "move"As on the singular route. auto is the default and leaves Accept-Language alone on any profile that has run; move applies it to every profile in the list, which is visible to every site each of them is signed in to.

Returns

{ total, changed, unresolved, results } — one entry per profile carrying changed, fields, skipped, exitIp and, when it applies, unresolved.

Errors

  • 400 — ids missing, not an array, or containing something that is not a string.
curl -X POST -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"ids":["'$ID'"]}' "$BASE/profiles/bulk/match-exit"

Move the hardware onto a real machine

POST/api/v1/profiles/:id/repair-hardware

Rewrites the hardware half of the fingerprint so it describes a machine somebody can actually buy. The device catalogue makes an incoherent one unconstructible when a profile is generated; it does nothing for one that already exists, and profiles created before it drew their GPU, screen and thread count from three tables that knew nothing about each other — which is how an Apple M4 Max ends up reporting a 1680x1050 display, a resolution no Apple Silicon Mac has. Only the hardware moves: deviceId, gpu, screen, hardwareConcurrency, deviceMemory, the heap limit and the camera. The seed, the canvas and audio noise, the fonts, the locale, the timezone, the User-Agent, the proxy and the cookie jar are untouched, so this repairs a contradiction rather than issuing a new identity. It is deterministic from the profile’s own seed — calling it twice is a no-op, and calling it on three hundred profiles produces three hundred different machines rather than three hundred copies of the most popular row. A profile that already describes a real machine gets only its deviceId recorded, which nothing outside can observe. Refused while the profile is running, because the hardware was baked into the process at launch.

Path parameters

FieldTypeWhat it is
idstringProfile id.

Returns

{ changed, device: { id, model }, profile } — changed lists the fields that moved and is empty when the profile was already coherent.

Errors

  • 400 — the profile is running.
curl -X POST -H "Authorization: Bearer $KEY" "$BASE/profiles/$ID/repair-hardware"

Repair hardware across many profiles

POST/api/v1/profiles/bulk/repair-hardware

The route above, over a list. This is the one that matters: the problem it fixes is never one profile, and a repair that has to be invoked a row at a time on a library of three hundred is one nobody invokes. A running profile is skipped rather than refused — failing the whole batch over one open browser is the wrong answer for a caller who asked about two hundred — and skipped names them so they can be done later.

Body

FieldTypeWhat it is
idsstring[]Profile ids. Required.

Returns

{ total, repaired: [{ id, device, changed }], alreadyCoherent, skipped: [{ id, reason }] }. alreadyCoherent is counted rather than listed, because it is the answer for most of a healthy selection.

Errors

  • 400 — ids is missing or empty.
curl -X POST -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
  -d '{"ids":["'$ID'"]}' "$BASE/profiles/bulk/repair-hardware"

Check a profile’s own proxy

POST/api/v1/profiles/:id/proxy/check

Checks whichever proxy the profile would actually use, and caches the result on the profile.

Path parameters

FieldTypeWhat it is
idrequiredstringThe record’s id.

Returns

A ProxyCheckResult.

Run a leak test

POST/api/v1/profiles/:id/leak-test

Probes from inside a real page in the profile: the exit IP against the configured proxy, WebRTC ICE candidates for public address exposure, LAN address exposure, timezone against both the profile and the exit location, language, and geolocation. Starts the profile headlessly if needed. The exit-ip finding has three outcomes, not two — no proxy attached is a pass naming this machine, a proxy whose exit is known is compared against it, and a proxy that has never been checked is a warn asking for one. Read webrtc with that in mind: on a profile with no proxy the ICE candidates carry this machine’s own address, which is correct and not a leak, and the report says so rather than calling it a proxy exit.

Path parameters

FieldTypeWhat it is
idrequiredstringThe record’s id.

Returns

{ profileId, findings: [{ id, label, status, detail }], observedIp, observedCountry, observedTimezone, webrtcCandidates }

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.