# Octo Browser API > Octo Browser provides a comprehensive API for browser profile automation, enabling programmatic control of browser profiles, fingerprints, proxies, team members, and the local desktop client. The cloud API uses RESTful principles with JSON request/response bodies. All cloud requests authenticate via the `X-Octo-Api-Token` header against `https://app.octobrowser.net/api/v2/automation`. The local client API runs on `http://localhost:58888` inside the desktop app and is unauthenticated. Important notes: - Authentication: send `X-Octo-Api-Token: ` for every cloud request. - Standard envelope: `{ "success": bool, "msg": string, "data": ..., "code": string | null }`. Paginated listings (profiles, fingerprint renderers) add `total_count` and `page`; teams subaccounts/invites add only `total_count`; other listings return a bare `data` array. - Validation errors return HTTP 400 with `{"validation_error": {"body_params" | "query_params": [...]}}`. - Pagination: `page` is zero-based; `page_len` is one of `10, 25, 50, 100`. - Rate limits: 50–1000 RPM and 500–50000 RPH depending on subscription, shared with local-client profile operations. Honour `Retry-After` and `X-Ratelimit-*` headers. - Resource UUIDs are 32-character hex strings; team extension IDs use `@`; tags and folders are referenced by name in profile payloads. - Timestamps follow ISO 8601. - Machine-readable spec: [openapi.json](openapi.json) (OpenAPI 3.0, covers the cloud API including endpoints absent from the official spec). - Mirror hosts: `app.octobrowser-mirror1.com` / `.net` / `.org`. ## Getting Started - [Authentication](api/authentication.md): Obtain and use API tokens, mirror endpoints. - [Rate Limiting](api/rate-limiting.md): RPM/RPH budgets, headers, and 429 handling. - [Error Handling](api/errors.md): Business and validation error envelopes, error code reference. ## Core Resources - [Profiles](api/profiles.md): Create, update, list, force-stop, transfer, export, import, and password-protect browser profiles (15 endpoints). - [Tags](api/tags.md): Manage coloured profile tags (4 endpoints). - [Folders](api/folders.md): Group profiles into folders, introduced in Octo Browser 3.0 (4 endpoints). - [Proxies](api/proxies.md): Manage saved HTTP/HTTPS/SOCKS/SSH proxies and reference them from profiles (4 endpoints). - [Teams](api/teams.md): Manage subaccounts, invitations, and team-installed extensions (8 endpoints). - [Action Log](api/action-log.md): Real-time team audit stream over WebSocket (`wss://.../ws/action_log`) with watermark-based resume (1 WebSocket endpoint, upstream-documented). - [Fingerprint](api/fingerprint.md): Browser fingerprint object plus lookup endpoints for renderers, screens, and mobile device models (3 endpoints). ## Optional - [Local Client API](api/local-client.md): Drive the desktop app on `localhost:58888` — start, stop, force_stop, one-time profiles, password, auth, and update endpoints (12 endpoints). - [Docker & Kubernetes](api/docker.md): Run Octo Browser headless inside containers. - [Automation Libraries](api/automation.md): Selenium, Playwright, Puppeteer/Pyppeteer integration in Node.js, Python, Java, and VB.NET. --- # File: api/authentication.md # Authentication All cloud API requests require an API token. The local client API on `http://localhost:58888` is **unauthenticated** and intended for use by code running on the same machine. ## Getting Your API Token 1. Open the Octo Browser desktop app. 2. Navigate to **Settings → API**. 3. Copy your API token. ## Using the API Token Include the token in the `X-Octo-Api-Token` header on every request to `https://app.octobrowser.net/api/v2/automation/...`: ```bash curl -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ https://app.octobrowser.net/api/v2/automation/profiles ``` The token is shared across the team — every team member's token authenticates the same workspace, with permissions scoped per [subaccount](teams.md#permission-reference). ## Alternative Endpoints If your network blocks the canonical host, the API is also reachable on these mirrors: - `https://app.octobrowser-mirror1.com` - `https://app.octobrowser-mirror1.net` - `https://app.octobrowser-mirror1.org` The path, headers, and behaviour are identical. ## Environment Variable Avoid embedding the token in source code. Store it in an environment variable: ```bash export OCTO_API_TOKEN="your_token_here" ``` The desktop client also reads `OCTO_API_TOKEN` at startup and signs in automatically, which lets you script cold-start automation without `POST /api/auth/login` — see [local-client.md](local-client.md#signing-in-with-an-api-token) for per-OS launch commands. ```python import os import requests headers = {"X-Octo-Api-Token": os.environ["OCTO_API_TOKEN"]} response = requests.get( "https://app.octobrowser.net/api/v2/automation/profiles?page_len=10&page=0", headers=headers, ) ``` ## Security Notes - Treat the token like a password. Anyone holding it can act on the team's workspace. - Never commit tokens to version control. Add `.env`, secret files, and IDE run-configs to `.gitignore`. - Rotate tokens periodically from the **Settings → API** screen. - Server-side responses may include cleartext proxy credentials (see [proxies.md](proxies.md)) — log responses carefully. --- # File: api/rate-limiting.md # Rate Limiting The cloud API enforces a Requests-Per-Minute (RPM) and Requests-Per-Hour (RPH) budget. Limits are **shared across the whole team** — every subaccount's calls draw from the same bucket. Repeatedly ignoring `429` responses will cause us to enforce stricter limits on the team. ## Limits by Subscription | Plan | RPM | RPH | |-----------|------------------------------|------------------------------------| | Base | 50 | 500 | | Team | 100 | 1,500 | | Advanced | 200, expandable up to 1,000 | 3,000, expandable up to 50,000 | For limits beyond 1,000 RPM or 50,000 RPH, contact Octo Browser Technical Support. ## Point Costs of Local Client Operations Local client API calls (`localhost:58888`) are charged against the same RPM/RPH budget as cloud calls: | Local operation | Cost | |-----------------|------| | `POST /api/profiles/start` | 1 request | | `POST /api/profiles/stop` | 1 request | | `POST /api/profiles/password` (set) | 1 request | | `DELETE /api/profiles/password` | 1 request | | `POST /api/profiles/force_stop` | 2 requests | | `POST /api/profiles/one_time/start` | 4 requests | The following local requests are **not counted** toward the limits: List Active Profiles (`GET /api/profiles/active`), Login, Logout, Get Client Version, Update Client, and Username. ## Rate Limit Headers Every cloud API response carries: ``` Retry-After: 0 # seconds; 0 means you may send the next request now X-Ratelimit-Limit: 200 # current RPM limit X-Ratelimit-Limit-Hour: 3000 # current RPH limit X-Ratelimit-Remaining: 196 # remaining points this minute X-Ratelimit-Remaining-Hour: 2996 # remaining points this hour X-Ratelimit-Reset: 1671789217 # unix timestamp when the minute window resets ``` `X-Ratelimit-Remaining` is the authoritative signal — drop your request rate as it approaches `0`. ## Handling 429 Responses When the bucket is empty, the API returns `429 Too Many Requests` with a non-zero `Retry-After` header. Because the budget is team-wide, **stop all requests across the team** (not just the one that got the `429`) and pause for `Retry-After` seconds before retrying. Ignoring `429` responses may lead to stricter limits being enforced on the team. ```python import time import requests def request_with_retry(method, url, headers, **kwargs): while True: response = requests.request(method, url, headers=headers, **kwargs) if response.status_code != 429: return response retry_after = int(response.headers.get("Retry-After", "1")) time.sleep(max(retry_after, 1)) ``` ## Best Practices 1. **Watch the headers.** Slow down before you hit `0` rather than after. 2. **Linear pacing beats bursts.** Spread heavy jobs evenly across the minute. 3. **Cache lookups.** Don't call `GET /tags`, `GET /proxies`, or `GET /fingerprint/renderers` on every iteration. 4. **One-time profiles cost 4×.** When scraping at volume, batch with saved profiles where possible. 5. **Local client calls count too.** Profile start/stop/password operations on `localhost:58888` draw from the same team budget (see the point-cost table above); only the read-only client endpoints are free. --- # File: api/errors.md # Error Handling The cloud API uses two distinct error envelopes: a **business error envelope** (returned for most non-2xx responses, including auth failures, missing resources, and conflicts) and a **validation envelope** (returned when the request body or query string is malformed). ## Business Error Envelope ```json { "success": false, "msg": "Human-readable message", "code": "machine_readable_code", "data": "" } ``` - `success` is always `false` on errors. - `code` is a stable identifier you can branch on. The list below is not exhaustive. - `data` is `""` for most errors and an object for partial-failure responses (e.g. mass `force_stop`). Real samples: ```http HTTP/2 401 {"success":false,"msg":"Invalid or missing API token.","data":"","code":"api_token"} ``` ```http HTTP/2 404 {"success":false,"msg":"Cannot find profile by uuid","data":"","code":"not_found"} ``` ## Validation Envelope Bad query parameters or body fields return HTTP 400 with a different shape: ```json { "validation_error": { "query_params": [ { "type": "enum", "loc": ["page_len"], "msg": "Input should be 10, 25, 50 or 100", "input": "1" } ] } } ``` Body validation uses the same shape, replacing `query_params` with `body_params`: ```json { "validation_error": { "body_params": [ { "type": "string_too_long", "loc": ["name"], "msg": "String should have at most 20 characters", "input": "this-name-is-way-too-long" } ] } } ``` When you receive a 400, look for `validation_error` first. If it is missing, parse the response as a business error. ## HTTP Status Codes | Code | Meaning | |------|---------| | 200 | OK | | 201 | Created — returned by `POST /profiles`, `POST /tags`, and `POST /proxies` (`POST /folders` returns 200). | | 207 | Multi-Status — declared by the OpenAPI spec for `POST /profiles/import` partial results, and per the official Postman collection for a partially successful mass `POST /profiles/force_stop` (not live-verified). | | 400 | Bad request — body or query failed validation, or a domain rule rejected the call. | | 401 | Invalid or missing `X-Octo-Api-Token`. | | 403 | The token is valid but the subaccount lacks the required permission, or a quota was reached. | | 404 | Resource (profile, tag, proxy, subaccount, …) does not exist. | | 409 | Conflict — resource is locked, running, or in a state that forbids the operation. | | 422 | Declared as the validation response throughout the official OpenAPI spec, but in practice the API returns validation failures as `400` with the `validation_error` envelope above. Treat a 422 body of `{ "detail": [{ "loc": [...], "msg": "...", "type": "..." }] }` as a rare fallback. | | 429 | Rate limit exceeded; see [rate-limiting.md](rate-limiting.md). | | 5xx | Server-side issue. Retry with exponential backoff. | ## Error Code Reference The `code` field on the business error envelope. Codes prefixed with a domain (`profiles.`, `tags.`, …) are scoped to that resource. ### General | Code | Meaning | |------|---------| | `api_token` | Invalid or missing `X-Octo-Api-Token`. | | `not_found` | Resource UUID does not exist (also returned for an unknown or temporary proxy UUID in a profile's `proxy` field: `proxy not found:`). | | `no_permission` | Subaccount does not have permission for this action. | | `limit_reached` | Resource limit reached on the current subscription. | | `already_exists` | Conflict — a record with the same key already exists. | | `internal_error` | Server-side fault. Retry. | | `unknown_error` | Unclassified failure (from the OpenAPI spec's `ErrorCode` enum). | | `no_tokens` | No API token exists for the workspace (from the OpenAPI spec's `ErrorCode` enum). | | `rate_limit_exceeded` | RPM/RPH bucket empty. See [rate-limiting.md](rate-limiting.md). | ### Profiles | Code | Meaning | |------|---------| | `profiles.started` | Profile is already running. | | `profiles.not_started` | Profile is not running. | | `profiles.stop_error` | Bulk force-stop stopped none of the profiles (`400`). `data.failed` lists the offending UUIDs. | | `profiles.consistency_error` | `version` passed to `force_stop` does not match the server-side version. | | `profiles.update_error` | Update payload was rejected, e.g. `fingerprint.os` on `PATCH /profiles/{uuid}` (`You can't update os.`). | | `profiles.password_error` | Password set/clear failed (wrong `old_password`/`password`, or a listed profile is running on another machine); nothing is changed. | | `profiles.invalid_cookie` | Cookies field is not in JSON, Mozilla, or Netscape format. | | `profiles.transfer_error` | Generic transfer failure. | | `profiles.transfer_no_receiver` | `receiver_email` is not a member of the workspace. | | `profiles.transfer_no_profiles` | None of the supplied UUIDs are eligible for transfer. | | `profiles.export_error` | Export operation failed. | | `profiles.export_limit_exceeded` | Export rate or volume limit reached. | | `profiles.import_error` | Import operation failed. | | `profiles.import_limit_exceeded` | Import rate or volume limit reached. | | `profiles.import_no_valid_profiles` | None of the supplied export blobs decoded into valid profiles. | ### Other Resources | Code | Meaning | |------|---------| | `fingerprints.invalid` | Fingerprint object failed server-side validation. | | `fingerprints.invalid_mobile_model` | `device_model` is unknown, or the chosen `os_version` is not in that model's `os_versions` list. | | `subscriptions.inactive` | Workspace has no active subscription. | | `proxy_providers.empty_balance` | Built-in proxy provider has no balance. | | `proxy.maximum_saved_error` | Proxy quota reached on the subscription. | ## Handling Errors in Code ```python import time import requests def call(method, url, headers, max_retries=3, **kwargs): for attempt in range(max_retries): response = requests.request(method, url, headers=headers, **kwargs) if response.status_code == 429: time.sleep(int(response.headers.get("Retry-After", "1"))) continue if response.status_code >= 500: time.sleep(2 ** attempt) continue body = response.json() if response.ok and body.get("success"): return body # Validation error? if "validation_error" in body: raise ValueError(f"Validation failed: {body['validation_error']}") # Business error code = body.get("code") msg = body.get("msg") if code == "profiles.started": raise RuntimeError("Profile is running — stop it first.") if code == "limit_reached": raise RuntimeError("Subscription limit reached.") raise RuntimeError(f"{response.status_code} {code}: {msg}") raise RuntimeError("Exhausted retries") ``` ## Debugging Tips - Log `x-trace-id` from the response headers — Octo Browser support uses it to look up the call server-side. - For 400s, inspect `validation_error` first; it pinpoints the offending field with `loc`. - For 429s, slow your request rate before retrying; never tight-loop on `Retry-After: 0` if the body still says you are rate-limited. - For 5xx, retry with exponential backoff. If the failure persists, file a support ticket including the trace id. --- # File: api/profiles.md # Profiles API Browser profiles bundle a fingerprint, an optional proxy, browser storage settings and metadata. Profiles are identified by 32-character hex `uuid` strings. All endpoints return the standard envelope `{ "success": bool, "msg": string, "data": ..., "code": string | null }`. List endpoints additionally return `total_count` and `page`. ## Get Profiles **Endpoint**: `GET /profiles` **Query Parameters**: | Param | Type | Notes | |---------------|---------|-------| | `page_len` | int | Page size; one of `10, 25, 50, 100`. Default `10`. | | `page` | int | Zero-based page number. Default `0`. | | `fields` | string | Comma-separated list of fields to include in each item. Allowed: `title`, `description`, `proxy`, `start_pages`, `tags`, `status`, `last_active`, `version`, `storage_options`, `created_at`, `updated_at`, `has_user_password`, `pinned_tag`, `folder`, `launch_args`, `images_load_limit`, `local_cache`, `extra_info`. Without this parameter only `uuid` is returned. | | `ordering` | string | One of `created`, `-created`, `active`, `-active`, `title`, `-title`. | | `search` | string | Filter profiles whose title starts with the given string. | | `search_tags` | string | Comma-separated list of tag **names**. A profile is returned only when it has **all** of the listed tags. | | `status` | int | Filter by status code. | | `password` | bool | `true` returns only profiles with passwords; `false` returns only profiles without; omit for all. | | `proxies` | string | Comma-separated list of proxy UUIDs, or the special token `@no-proxies-filter` to return only profiles without a proxy. | ```bash curl -X GET "https://app.octobrowser.net/api/v2/automation/profiles?page_len=100&page=0&fields=title,description,folder,proxy,start_pages,tags,status,last_active,version,storage_options,created_at,updated_at&ordering=active" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" ``` ### Example Response ```json { "success": true, "msg": "", "data": [ { "uuid": "4bbdc824762342f485bed4968533b28a" }, { "uuid": "4565940d5b9b41f99341fae6f5f3d855" } ], "total_count": 25, "page": 0, "code": null } ``` ## Get Profile Fetch a single profile with the full nested fingerprint. The full object is always returned — a `fields` query parameter is ignored here. `folder` is `null` when the profile is not in a folder. The official Postman sample also shows a `user_agent_changed_by_user` field inside `fingerprint`; it was not returned by the live API on 2026-08-20. **Endpoint**: `GET /profiles/{uuid}` ```bash curl -X GET "https://app.octobrowser.net/api/v2/automation/profiles/cfd673b04de3433caca327836ae69d19" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" ``` ### Example Response ```json { "success": true, "msg": "", "data": { "uuid": "cfd673b04de3433caca327836ae69d19", "title": "Quick befitting-sick", "description": "", "start_pages": [], "bookmarks": [], "tags": [], "pinned_tag": null, "folder": null, "proxy": null, "status": 0, "version": "0", "storage_options": { "cookies": true, "passwords": true, "extensions": true, "localstorage": false, "history": false, "bookmarks": true, "serviceworkers": false }, "last_active": null, "fingerprint": { "os": "win", "os_version": "11", "os_arch": "x86", "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...", "screen": "1440x900", "renderer": "NVIDIA GeForce GT 710", "languages": { "type": "ip", "data": null }, "timezone": { "type": "ip", "data": null }, "geolocation": { "type": "ip", "data": null }, "webrtc": { "type": "ip", "data": null }, "noise": { "webgl": false, "canvas": false, "audio": false, "client_rects": false }, "media_devices": { "video_in": 1, "audio_in": 1, "audio_out": 1 }, "fonts": ["Arial", "Times New Roman", "..."], "cpu": 12, "ram": 16, "dns": null }, "image": "55e228c7227946b3889f370b54be26c1", "extensions": [], "launch_args": [], "images_load_limit": null, "local_cache": false, "extra_info": null, "has_user_password": true, "password_set_at": "2024-08-21T15:41:11", "created_at": "2024-08-21T15:40:15", "updated_at": "2024-08-21T15:41:11" }, "code": null } ``` ## Create Profile **Endpoint**: `POST /profiles` > If you omit a parameter, the server generates a sensible value for it. Only override what you specifically need to control. > Tip: if you are unsure what a working payload looks like, configure a profile in the browser UI, then fetch it with [`GET /profiles/{uuid}`](#get-profile) and reuse the returned fields. **Request Body** — top-level fields: | Field | Type | Required | Notes | |-------------------|----------|----------|-------| | `title` | string | yes | 1–90 characters. | | `fingerprint` | object | yes | See [fingerprint.md](fingerprint.md). Only `os` is mandatory inside it. With manual `timezone` data you currently have to account for daylight-saving time yourself (upstream caveat). | | `description` | string | no | Up to 1024 characters. | | `start_pages` | string[] | no | Up to 20 URLs (each ≤ 2048 chars). | | `bookmarks` | object[] | no | Up to 100 `{name, url}` items. | | `tags` | string[] | no | Tag **names** to attach (each 1–20 chars). Tags are referenced by name here, not by UUID. | | `pinned_tag` | string | no | Tag **name** rendered prominently in the UI. | | `folder` | string | no | Folder to place the profile in (see [folders.md](folders.md)). Octo 3.0+. | | `password` | string | no | Profile password (4–255 chars). | | `proxy` | object \| string \| null | no | Saved proxy by UUID, inline temporary proxy, or `null`. See [Proxy Field](#proxy-field). | | `storage_options` | object | no | Toggles for cookies, passwords, extensions, localstorage, history, bookmarks, serviceworkers. | | `cookies` | array \| string | no | JSON array, or a string in JSON, Mozilla, or Netscape format (see below). The string may be base64-encoded; string form is capped at 16 MiB. | | `image` | string | no | Profile avatar identifier. | | `extensions` | string[] | no | Extension UUIDs. | | `launch_args` | string[] | no | Extra Chromium command-line flags. | | `images_load_limit` | int | no | Max images cached per page (bytes). | | `local_cache` | bool | no | Persist HTTP cache between sessions. | | `extra_info` | object | no | Arbitrary JSON, accessible from extensions via `chrome.cookies.getOctoProfileExtraInfo`. | ```bash curl -X POST "https://app.octobrowser.net/api/v2/automation/profiles" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "title": "Test profile from api", "description": "test description", "start_pages": ["https://fb.com"], "tags": ["octo"], "pinned_tag": "octo", "launch_args": ["--start-maximized"], "proxy": { "type": "socks5", "host": "1.1.1.1", "port": 5555, "login": "", "password": "" }, "storage_options": { "cookies": true, "passwords": true, "extensions": true, "localstorage": false, "history": false, "bookmarks": true }, "fingerprint": { "os": "mac", "os_version": "11", "os_arch": "x86", "renderer": "AMD Radeon Pro 450", "screen": "1920x1080", "languages": { "type": "ip" }, "timezone": { "type": "ip" }, "geolocation":{ "type": "ip" }, "webrtc": { "type": "ip" }, "cpu": 4, "ram": 8 } }' ``` ### Example Response ```json { "success": true, "msg": "", "data": { "uuid": "21d471786f4e4038811e1e78371831d9" }, "code": null } ``` ### Create Mobile Profile Mobile profiles pick a `device_model` (see [fingerprint.md](fingerprint.md)); `os_version` must be one of that model's `os_versions`. Mobile profiles do not support extensions. ```bash curl -X POST "https://app.octobrowser.net/api/v2/automation/profiles" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "title": "Test mobile profile from api", "description": "Mobile profiles do not support extensions", "fingerprint": { "device_model": "SM-T870", "device_type": "tablet", "os": "android", "os_arch": "arm", "os_version": "13" } }' ``` ### Proxy Field The `proxy` field on create/update accepts three forms: - `{ "uuid": "" }` — attach a saved proxy from [proxies.md](proxies.md). A plain string, `"proxy": ""`, is accepted and means the same. - `{ "type": "...", "host": "...", "port": ..., "login": "...", "password": "..." }` — a temporary proxy bound to this profile only. Sending an inline object again edits the profile's existing temporary proxy in place. In `GET` responses a temporary proxy appears with `"uuid": ""` and no `title`/`profiles_count`. - `null` — detach the proxy. A temporary proxy is deleted in this case, as it is when the profile is switched from it to a saved proxy. Temporary proxies cannot be referenced by UUID. Passing the UUID of a temporary proxy, or an unknown UUID, fails with `404`: ```json { "success": false, "msg": "proxy not found:", "data": "", "code": "not_found" } ``` ### Cookie Formats The `cookies` field on create/update accepts either an array or a single string in any of these formats: **JSON** ```json [ { "domain": ".google.com", "expirationDate": 1639134293.313654, "hostOnly": false, "httpOnly": false, "name": "1P_JAR", "path": "/", "sameSite": "no_restriction", "secure": true, "value": "2021-11-10-11" } ] ``` **Mozilla** ```json [ { "Path raw": "/", "Samesite raw": "no_restriction", "Name raw": "NID", "Content raw": "2021-11-10-11", "Expires raw": "1639134293", "Host raw": "https://.google.com/", "This domain only raw": "false", "HTTP only raw": "false", "Send for raw": "true" } ] ``` **Netscape** — tab-separated text, one cookie per line (newline-separated for several cookies): ``` .google.com\tTRUE\t/\tTRUE\t1639134293\t1P_JAR\t2021-11-10-1\t544 ``` As a JSON request value: ```json { "title": "Test profile from api with netscape cookies", "fingerprint": { "os": "win" }, "cookies": "some.host\tTRUE\t/\tTRUE\t123\tname\tvalue\nsome2.host\tTRUE\t/\tTRUE\t111\tname2\tvalue2" } ``` ### `extra_info` from Extensions Add `"permissions": ["cookies"]` to the extension's `manifest.json` and call `chrome.cookies.getOctoProfileExtraInfo` from the extension's service-worker: ```js chrome.cookies.getOctoProfileExtraInfo((extraInfo) => { console.log(extraInfo); }); ``` ## Update Profile **Endpoint**: `PATCH /profiles/{uuid}` All fields are optional. The body shape matches `POST /profiles`, except that `password` is not accepted here — use [`POST /profiles/set_password`](#set-profiles-password) instead. A partial `fingerprint` object is accepted (only the listed sub-fields change), but `fingerprint.os` cannot be changed: any `fingerprint` containing `os` — even the current value — is rejected with `400` `profiles.update_error` (`You can't update os.`). Setting `proxy` to `null` detaches the proxy; `images_load_limit` is accepted here. > Updating running profiles works, but for synchronisation safety prefer to update stopped profiles. ```bash curl -X PATCH "https://app.octobrowser.net/api/v2/automation/profiles/d9623a2be9a0431784aacc4500d7963a" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "title": "new title", "description": "new description", "tags": ["octo"], "fingerprint": { "os_version": "11", "screen": "1920x1080" } }' ``` ### Example Response ```json { "success": true, "msg": "", "data": { "uuid": "d9623a2be9a0431784aacc4500d7963a" }, "code": null } ``` ## Delete Profiles **Endpoint**: `DELETE /profiles` **Request Body**: - `uuids` (string[], required) — profiles to delete, 1–100 per request. - `skip_trash_bin` (bool, optional, default `true`) — bypass the trash bin and delete immediately. ```bash curl -X DELETE "https://app.octobrowser.net/api/v2/automation/profiles" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"uuids":["a4708a63d55742a09b7f1a600c248484","d7226450b9fb4bac8526c24fc3669814"], "skip_trash_bin": true}' ``` ### Example Response ```json { "success": true, "msg": "Profiles deleted", "data": { "deleted_uuids": ["a4708a63d55742a09b7f1a600c248484"], "active_uuids": ["d7226450b9fb4bac8526c24fc3669814"] } } ``` `active_uuids` lists profiles that could not be deleted because they were running — stop them first and retry. ## Import Cookies **Endpoint**: `POST /profiles/{uuid}/import_cookies` **Request Body**: `{ "cookies": }` — accepts the same JSON, Mozilla and Netscape formats described under [Cookie Formats](#cookie-formats). Cookies can also be uploaded as `multipart/form-data` under the `cookies` field. The profile must not be running (`409` otherwise). ```bash curl -X POST "https://app.octobrowser.net/api/v2/automation/profiles/d9623a2be9a0431784aacc4500d7963a/import_cookies" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"cookies":[{"domain":".google.com","name":"1P_JAR","value":"2021-11-10-11","path":"/","secure":true,"httpOnly":false,"hostOnly":false,"sameSite":"no_restriction","expirationDate":1639134293.313654}]}' ``` ### Example Response ```json { "success": true, "msg": "Cookies imported", "data": "" } ``` ## Force Stop Profile Forcibly stops a running profile. **Endpoint**: `POST /profiles/{uuid}/force_stop` **Request Body**: - `version` (int | null, required) — profile version for an optimistic concurrency check. The field must be present; pass `null` when you do not track versions. Omitting it entirely returns a `400` validation error (`Field required`). ```bash curl -X POST "https://app.octobrowser.net/api/v2/automation/profiles/d9623a2be9a0431784aacc4500d7963a/force_stop" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"version": null}' ``` ### Example Response ```json { "success": true, "msg": "Profile stopped", "data": "" } ``` ## Mass Force Stop Profile Stops several profiles in one call. Unlike the single-profile endpoint, this one takes no `version` and performs no consistency check. **Endpoint**: `POST /profiles/force_stop` **Request Body**: - `uuids` (string[], required) — 1–100 per request. **Response**: `data.failed` lists the UUIDs that were not stopped (for example, profiles that were not running). Status codes: | Code | Meaning | |------|---------| | 200 | All requested profiles were stopped; `success` is `true` and `data.failed` is empty. | | 207 | Multi-Status — only some were stopped, per the official Postman collection (not live-verified). | | 400 | None were stopped; `code` is `profiles.stop_error`. | ```bash curl -X POST "https://app.octobrowser.net/api/v2/automation/profiles/force_stop" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"uuids":["4b8afed25a524f5aa1dc2922279622f8","21d471786f4e4038811e1e78371831d9"]}' ``` ### Example Response (400 — none stopped) ```json { "success": false, "msg": "Bulk force stop error", "code": "profiles.stop_error", "data": { "failed": ["4b8afed25a524f5aa1dc2922279622f8", "21d471786f4e4038811e1e78371831d9"] } } ``` ## Transfer Profiles Move profiles to another account on the same workspace. **Endpoint**: `POST /profiles/transfer` **Request Body**: - `uuids` (string[], required) — up to 100 entries per request. - `receiver_email` (string, required) — destination account email. - `transfer_proxy` (bool, required) — transfer the attached proxies along with the profiles. ```bash curl -X POST "https://app.octobrowser.net/api/v2/automation/profiles/transfer" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"uuids":["21d471786f4e4038811e1e78371831d9"], "receiver_email":"team-mate@example.com", "transfer_proxy": true}' ``` ### Example Response ```json { "success": true, "msg": "Profiles transferred successfully", "data": "" } ``` ## Export Profiles Encode profiles into transferable strings. **Endpoint**: `POST /profiles/export` > Paid action — costs 0.5 tokens per profile. Up to 100 profiles per request; duplicate UUIDs are silently ignored. **Request Body**: - `uuids` (string[], required) — up to 100 per request. - `export_proxy` (bool, required) — include the attached proxy data in the export blob. - `app_version` (string, optional) — client version to target the export format at. ```bash curl -X POST "https://app.octobrowser.net/api/v2/automation/profiles/export" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"uuids":["21d471786f4e4038811e1e78371831d9"], "export_proxy": false}' ``` ### Example Response ```json { "success": true, "msg": "Profiles exported successfully", "data": { "exported": [ { "uuid": "21d471786f4e4038811e1e78371831d9", "title": "Test profile from api", "data": "MEYCIQDcEhji9E69bcOC1853v0zlXIP8kq6ecKwdcejBYkagrwIhAPUU/65bZqoT74AiqKT4IVuKK26zkBN2M9HTFJoZC4rS" } ], "failed": [] } } ``` ## List Exports Paginated list of previously generated exports for the account. **Endpoint**: `GET /profiles/export` **Query Parameters**: - `page` (int, optional) - `page_len` (int, optional) ```bash curl -X GET "https://app.octobrowser.net/api/v2/automation/profiles/export?page=0&page_len=10" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" ``` ### Example Response ```json { "success": true, "msg": "", "data": { "data": [ { "uuid": "21d471786f4e4038811e1e78371831d9", "title": "Test profile from api", "data": "JGFlc19nY20kJFpkbkZ6WWlOTVROejNCRnhwVDBuanZJcWp6a0lLemNvV2VpWXEvLkdETC40WFF1QmdwYnlNampRcTRzZ1JSUmJqWHhKMjNWbnVITUxuL1ZTMTBCT1BmaVZZS0E=" } ], "total": 1, "page": 0 } } ``` > Note: pagination keys here are nested under `data` and use `total` (not `total_count`). ## Get Export Fetch a single previously generated export. **Endpoint**: `GET /profiles/export/{uuid}` ```bash curl -X GET "https://app.octobrowser.net/api/v2/automation/profiles/export/21d471786f4e4038811e1e78371831d9" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" ``` ### Example Response ```json { "success": true, "msg": "", "data": { "uuid": "21d471786f4e4038811e1e78371831d9", "title": "Test profile from api", "data": "JGFlc19nY20kJDMvMzBKcW12d2tXYi91UlRNL1ZHNFNTZEROYUlKSC5wbDRwWTdkaFlac0pBRUE4dkpVOXVRclhnWnpPT3N5eUtUOUlKOGMveTZkekpmemRPMEFMd3Z0YnBsenM=" } } ``` ## Import Profiles Restore profiles from export blobs. **Endpoint**: `POST /profiles/import` > Up to 100 profiles per request. **Request Body**: - `data` (array, required) — up to 100 items, each in one of three forms: - a string — the `data` blob from [`POST /profiles/export`](#export-profiles); - an object `{ "data", "title", "uuid" }` — an entry from a new-format export file, passed verbatim; - an object `{ "data": "", "signature" }` — an entry from an old (V1) export file. - `folder` (string, optional) — folder to place the imported profiles in (see [folders.md](folders.md)). Octo 3.0+. ```bash curl -X POST "https://app.octobrowser.net/api/v2/automation/profiles/import" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"data":["MEYCIQDcEhji9E69bcOC1853v0zlXIP8kq6ecKwdcejBYkagrwIhAPUU/65bZqoT74AiqKT4IVuKK26zkBN2M9HTFJoZC4rS"]}' ``` Import from a new-format export file: ```json { "data": [ { "data": "JGFlc19nY20kJE1mUk9tVS5KUGhrZmZyeGVCcGpzQ1VDa2xOUWRtcGdtVXc2WGdnMldqNWU5bFdHcGFnME5LSzZQa1BZLndsdnZEMWRZdHRudG5GdW5xakk5aUxyRWIycHhWdw==", "title": "[Imported] Quick bouncy-reception", "uuid": "761c6c15b2e44545851061555c665319" } ] } ``` Import from an old-format (V1) export file: ```json { "data": [ { "data": "{\"title\": \"[Imported] Quick rosy-chairlift\", \"exported_at\": \"2023-12-06 08:58:38\", \"profile\": \"gAAAAABlcDhTKzTeRIj2pGHBg-zJggNGMsekvt2DT47uM_iZYua9yoEmov0x3ZQKOu-XSWjgMGLL3CAC8rN92zyj-qOBVw3bnA==\"}", "signature": "MEUCIQD4ynaphY6R0TjQPz6hb5cmPaN5W+5pguRT65a6KB4r/gIgDOOcLfRvhrmeYHSpRIKSnjaziXM6lrS1OvVm86vxKVk=" } ] } ``` ### Example Response ```json { "success": true, "msg": "Profiles imported successfully", "data": { "failed": [] } } ``` ## Set Profiles Password Set or change the password protecting one or more profiles. The same password is applied to every listed profile. The request is all-or-nothing: if `old_password` does not match for one of the profiles, or one of them is currently running on another machine, the whole request fails with `400` and no profile is changed. **Endpoint**: `POST /profiles/set_password` **Request Body**: - `profiles` (string[], required) — profile UUIDs. - `password` (string, required) — new password, 4–255 characters. - `old_password` (string, required when any listed profile already has a password) — a single value that must match the current password of every listed profile that has one. Omit when none of them has a password yet. ```bash curl -X POST "https://app.octobrowser.net/api/v2/automation/profiles/set_password" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"profiles":["21d471786f4e4038811e1e78371831d9"], "password":"new-pw", "old_password":"old-pw"}' ``` ### Example Response ```json { "success": true, "msg": "Password has been set for selected profiles", "data": "" } ``` ## Clear Profile Password Remove the password from a single profile. The current password must be provided; if it does not match, the request fails with `400` `profiles.password_error`. **Endpoint**: `POST /profiles/{uuid}/clear_password` **Request Body**: - `password` (string, required) — current profile password. ```bash curl -X POST "https://app.octobrowser.net/api/v2/automation/profiles/21d471786f4e4038811e1e78371831d9/clear_password" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"password":"current-pw"}' ``` ### Example Response ```json { "success": true, "msg": "Password has been cleared", "data": "" } ``` ## Common Errors - **400 Bad Request** — body fails validation (see the `validation_error` envelope in [errors.md](errors.md)), `fingerprint.os` sent on `PATCH` (`profiles.update_error`), a wrong password (`profiles.password_error`), or a mass `force_stop` that stopped nothing (`profiles.stop_error`). - **404 Not Found** — profile UUID does not exist, or the `proxy` UUID is unknown or belongs to a temporary proxy (`proxy not found:`). - **409 Conflict** — profile is locked, running (e.g. on `import_cookies`), or its `version` does not match. - Unknown tag **names** (≤ 20 chars) in `tags[]` are not an error: the request succeeds and the tag is silently dropped (no tag is auto-created). Names longer than 20 characters return `400` `string_too_long`. --- # File: api/tags.md # Tags API Tags are coloured labels attached to browser profiles. They are managed independently and referenced **by name** (1–20 characters) from profile payloads; tag UUIDs are used only by `PATCH /tags/{uuid}` and `DELETE /tags/{uuid}`. All endpoints return the standard envelope `{ "success": bool, "msg": string, "data": ..., "code": string | null }`. ## Get Tags List every tag on the account. **Endpoint**: `GET /tags` ```bash curl -X GET "https://app.octobrowser.net/api/v2/automation/tags" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" ``` ### Example Response ```json { "success": true, "msg": "", "data": [ { "uuid": "3524ff6e3f0245ffbbcb5ea3d0446a8e", "name": "aaaa", "color": "grey" }, { "uuid": "07fd5038fc0743cd955d963a19edb3d9", "name": "facebook", "color": "blue" }, { "uuid": "ab1391ce01aa46fcbdd9d02a4569bec4", "name": "google", "color": "yellow" }, { "uuid": "7891009afee84952a926b03e7bc0af52", "name": "octo", "color": "orange" } ] } ``` ## Create Tag **Endpoint**: `POST /tags` Returns HTTP `201 Created`. **Request Body**: - `name` (string, required) — tag name, 1–20 characters. - `color` (string, optional) — one of: `grey`, `blue`, `cyan`, `orange`, `green`, `purple`, `red`, `yellow`. Defaults to `grey`. Hex colours are not accepted. ```bash curl -X POST "https://app.octobrowser.net/api/v2/automation/tags" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"supertag","color":"blue"}' ``` ### Example Response ```json { "success": true, "msg": "", "data": { "uuid": "ab003313e690425cb1f01e67b9b3a5da", "name": "supertag", "color": "blue" } } ``` ## Update Tag **Endpoint**: `PATCH /tags/{uuid}` Both `name` and `color` are optional; supply only the fields you want to change (e.g. `{"color":"red"}` alone is accepted). `name` is still limited to 1–20 characters. Returns HTTP `200 OK`. ```bash curl -X PATCH "https://app.octobrowser.net/api/v2/automation/tags/ab003313e690425cb1f01e67b9b3a5da" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"supertag1","color":"orange"}' ``` ### Example Response ```json { "success": true, "msg": "", "data": { "uuid": "ab003313e690425cb1f01e67b9b3a5da", "name": "supertag1", "color": "orange" } } ``` ## Remove Tag **Endpoint**: `DELETE /tags/{uuid}` ```bash curl -X DELETE "https://app.octobrowser.net/api/v2/automation/tags/ab003313e690425cb1f01e67b9b3a5da" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" ``` ### Example Response ```json { "success": true, "msg": "", "data": null } ``` ## Using Tags with Profiles Profile payloads reference tags **by name** (not by UUID — the UUIDs above are only used for `PATCH /tags/{uuid}` and `DELETE /tags/{uuid}`). The `tags` field on a profile is an array of tag names, and `pinned_tag` is a single tag name: ```json { "title": "My Profile", "tags": ["facebook", "google"], "fingerprint": { "os": "win" } } ``` ### Search Profiles by Tags `GET /profiles` accepts `search_tags`, a comma-separated list of tag **names**. A profile is returned only when it has **all** listed tags (logical AND, not OR). ```bash curl -X GET "https://app.octobrowser.net/api/v2/automation/profiles?page_len=10&page=0&search_tags=facebook,google" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" ``` ## Common Errors - **400 Bad Request** — invalid colour, missing `name` on create, `name` longer than 20 characters (`string_too_long`), or malformed UUID. - **404 Not Found** — tag UUID does not exist. - **409 Conflict** — tag name already exists on the account. See [errors.md](errors.md) for the standard error envelope and validation-error payload. --- # File: api/folders.md # Folders API Folders group browser profiles for organisation and access control. They were introduced in Octo Browser 3.0 (July 2026) and are managed independently, then referenced from profile payloads via the `folder` field. All endpoints return the standard envelope `{ "success": bool, "msg": string, "data": ..., "code": string | null }`. ## Get Folders List every folder in the workspace. The response is not paginated. **Endpoint**: `GET /folders` ```bash curl -X GET "https://app.octobrowser.net/api/v2/automation/folders" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" ``` ### Example Response ```json { "success": true, "msg": "", "data": [ { "uuid": "4d867ccd37224526ac2f6a65761c8614", "name": "clients", "assignable_wo_permission": false } ], "code": null } ``` ## Create Folders Creates one or more folders in a single request. **Endpoint**: `POST /folders` Returns HTTP `200 OK` (not `201`, unlike `POST /profiles`, `POST /tags`, and `POST /proxies`). **Request Body**: - `folders` (array, required) — 1 to 100 folder objects, each with: - `name` (string, required) — folder name, 1–20 characters. - `assignable_wo_permission` (bool, optional) — whether profiles can be assigned to this folder without an explicit folder permission. Defaults to `false`. ```bash curl -X POST "https://app.octobrowser.net/api/v2/automation/folders" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"folders":[{"name":"clients"}]}' ``` ### Example Response `data` is an array of the created folders: ```json { "success": true, "msg": "", "data": [ { "uuid": "4d867ccd37224526ac2f6a65761c8614", "name": "clients", "assignable_wo_permission": false } ], "code": null } ``` ## Update Folder **Endpoint**: `PATCH /folders/{folder}` The `{folder}` path parameter accepts either the folder UUID or the folder name. Both body fields are optional; supply only what you want to change: - `name` (string, optional) — new name, 1–20 characters. - `assignable_wo_permission` (bool, optional). ```bash curl -X PATCH "https://app.octobrowser.net/api/v2/automation/folders/4d867ccd37224526ac2f6a65761c8614" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"clients-eu","assignable_wo_permission":true}' ``` ### Example Response `data` is the updated folder object: ```json { "success": true, "msg": "", "data": { "uuid": "4d867ccd37224526ac2f6a65761c8614", "name": "clients-eu", "assignable_wo_permission": true }, "code": null } ``` ## Remove Folder **Endpoint**: `DELETE /folders/{folder}` The `{folder}` path parameter accepts either the folder UUID or the folder name. ```bash curl -X DELETE "https://app.octobrowser.net/api/v2/automation/folders/clients-eu" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" ``` ### Example Response ```json { "success": true, "msg": "", "data": null, "code": null } ``` ## Using Folders with Profiles Profile payloads accept an optional `folder` field (string) on `POST /profiles`, `PATCH /profiles/{uuid}`, and `POST /profiles/import` to place the profile into a folder. ## Common Errors - **400 Bad Request** — validation failure, e.g. a name longer than 20 characters. Note that this payload has no `success`/`msg`/`data`/`code` envelope, only `validation_error`: ```json { "validation_error": { "body_params": [ { "type": "string_too_long", "loc": ["name"], "msg": "String should have at most 20 characters", "input": "a-folder-name-that-is-too-long" } ] } } ``` - **404 Not Found** — folder UUID or name does not exist: ```json { "success": false, "msg": "Folder not found", "data": "", "code": "not_found" } ``` See [errors.md](errors.md) for the standard error envelope. --- # File: api/proxies.md # Proxies API Manage saved proxies. A proxy is a reusable record that can be referenced by UUID from a profile, or supplied inline on a profile payload. All endpoints return the standard envelope `{ "success": bool, "msg": string, "data": ..., "code": string | null }`. ## Get Proxies **Endpoint**: `GET /proxies` ```bash curl -X GET "https://app.octobrowser.net/api/v2/automation/proxies" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" ``` ### Example Response ```json { "success": true, "msg": "", "data": [ { "uuid": "963d30cb2d7247c89da222c8a9dcab29", "type": "socks5", "port": 29801, "host": "localhost", "login": "some_login", "password": "some_password", "change_ip_url": "https://localhost/api/v1/change-ip?uuid=c26dc4d6-0de7-4aeb-bbef-de3d98362f4e", "external_id": null, "profiles_count": 0, "title": "example proxy" } ] } ``` > **Security note**: proxy `login` and `password` are returned in clear text. Avoid logging full responses. ## Create Proxy **Endpoint**: `POST /proxies` Returns HTTP `201 Created`. Unknown body fields are rejected. **Request Body**: - `type` (string, required) — one of `http`, `https`, `socks`, `socks5`, `ssh`. - `host` (string, required) — an IPv4/IPv6 address or a hostname of 4–253 characters. - `port` (integer, required) — 0–65535. - `title` (string, required) — 1–255 characters. - `login` (string, optional) — up to 255 characters. - `password` (string, optional) — up to 255 characters. - `change_ip_url` (string, optional) — URL hit by the rotate-IP action; a valid URI, 1–65536 characters. - `external_id` (string, optional) — your own identifier, up to 255 characters; preserved in responses. **Response**: the created proxy object, including `uuid` and `profiles_count` (number of profiles using the proxy, `0` for a new proxy). ```bash curl -X POST "https://app.octobrowser.net/api/v2/automation/proxies" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "socks", "host": "localhost", "port": 1081, "login": "user", "password": "secret111", "title": "super proxy", "change_ip_url": "http://example.com", "external_id": "12345" }' ``` ### Example Response ```json { "success": true, "msg": "", "data": { "uuid": "789f4d3f898d4e10acf2bebd249fcf95", "type": "socks", "port": 1081, "host": "localhost", "login": "user", "password": "secret111", "change_ip_url": "http://localhost:1082/change_ip", "external_id": "my_custom_id=1", "profiles_count": 0, "title": "super proxy" } } ``` ## Update Proxy **Endpoint**: `PATCH /proxies/{uuid}` All body fields are optional; supply only the fields you want to change. ```bash curl -X PATCH "https://app.octobrowser.net/api/v2/automation/proxies/5246b94778f549859e2e6577d98d90aa" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"title":"renew title","port":40000}' ``` ### Example Response ```json { "success": true, "msg": "", "data": { "uuid": "5246b94778f549859e2e6577d98d90aa", "type": "socks5", "port": 40000, "host": "127.0.0.1", "login": "", "password": "", "change_ip_url": null, "external_id": null, "profiles_count": 0, "title": "renew title" } } ``` ## Remove Proxy **Endpoint**: `DELETE /proxies/{uuid}` ```bash curl -X DELETE "https://app.octobrowser.net/api/v2/automation/proxies/789f4d3f898d4e10acf2bebd249fcf95" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" ``` ### Example Response ```json { "success": true, "msg": "", "data": "" } ``` ## Using Proxies with Profiles The `proxy` field of a profile payload (`POST /profiles`, `PATCH /profiles/{uuid}`) accepts three forms: a saved-proxy UUID, an inline proxy object, or `null`. ### Reference a Saved Proxy by UUID Pass the UUID either as a plain string or as an object with a `uuid` key: ```json { "title": "Profile with saved proxy", "fingerprint": { "os": "win" }, "proxy": "789f4d3f898d4e10acf2bebd249fcf95" } ``` ```json { "title": "Profile with saved proxy", "fingerprint": { "os": "win" }, "proxy": { "uuid": "789f4d3f898d4e10acf2bebd249fcf95" } } ``` An unknown UUID is rejected with `404`: ```json { "success": false, "msg": "proxy not found:", "data": "", "code": "not_found" } ``` ### Inline Proxy on a Profile The same proxy fields accepted by `POST /proxies` may be embedded directly inside a profile payload. The proxy is then attached to that profile only and is not added to `GET /proxies`. In `GET /profiles/{uuid}` such a temporary proxy is returned with `"uuid": ""` and without `title` or `profiles_count`. ```json { "title": "Profile with inline proxy", "fingerprint": { "os": "win" }, "proxy": { "type": "http", "host": "proxy.example.com", "port": 8080, "login": "user", "password": "pass" } } ``` ### Detach a Proxy Send `"proxy": null` on `PATCH /profiles/{uuid}`; the profile's `proxy` then reads `null`. ```json { "proxy": null } ``` ### Filter Profiles by Proxy `GET /profiles` accepts the `proxies` query parameter — a comma-separated list of proxy UUIDs. The special token `@no-proxies-filter` returns profiles that have no proxy attached. ```bash # Profiles attached to specific proxies curl -X GET "https://app.octobrowser.net/api/v2/automation/profiles?page_len=10&page=0&proxies=789f4d3f898d4e10acf2bebd249fcf95,963d30cb2d7247c89da222c8a9dcab29" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" # Profiles with no proxy curl -X GET "https://app.octobrowser.net/api/v2/automation/profiles?page_len=10&page=0&proxies=@no-proxies-filter" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" ``` ## Proxy Types | Value | Description | |-----------|-------------| | `http` | HTTP proxy | | `https` | HTTPS proxy | | `socks` | SOCKS4 proxy | | `socks5` | SOCKS5 proxy | | `ssh` | SSH-tunnel proxy | ## Bulk Import Tip Upstream example scripts import saved proxies from a `.txt` file, one proxy per line, fields in this exact order and separated by `;`: ```text protocol;host;port;login;password;title;change_ip_url ``` `change_ip_url` is optional; the scripts accept the protocols `socks5`, `http`, and `ssh`. Each line maps directly onto a `POST /proxies` body. ## Common Errors - **400 Bad Request** — invalid `type`, missing `host`/`port`/`title`, out-of-range `port`, unknown field, or malformed body. - **404 Not Found** — proxy UUID does not exist. - **409 Conflict** — proxy is in use by a running profile and cannot be modified or deleted. See [errors.md](errors.md) for the standard error envelope. --- # File: api/teams.md # Teams API Manage team-wide resources: shared browser extensions, team members (subaccounts), and pending invitations. All endpoints return the standard envelope `{ "success": bool, "msg": string, "data": ..., "code": string | null }`. The subaccounts and invites listings additionally return `total_count`; the extensions listing does not. > Team features require a team or enterprise subscription. ## Get Extensions Returns extensions used by the team, installed in any profile. **Endpoint**: `GET /teams/extensions` **Query Parameters**: - `start` (int, optional) — offset (zero-based). - `limit` (int, optional) — page size, maximum `100`. ```bash curl -X GET "https://app.octobrowser.net/api/v2/automation/teams/extensions?start=0&limit=25" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" ``` ### Example Response ```json { "success": true, "msg": "", "data": [ { "uuid": "54d6ff5042c545b990349a7a7e653e81@2.0.12", "name": "Google Translate", "version": "2.0.12" } ] } ``` The extension `uuid` is the canonical extension id appended with `@` (e.g. `54d6ff5042c545b990349a7a7e653e81@2.0.12`). Always use the value as reported by `GET /teams/extensions`, not the bare store id. ## Delete Extensions Remove team-installed extensions by UUID. **Endpoint**: `DELETE /teams/extensions` **Request Body**: - `uuids` (string[], required) — up to 100 extension UUIDs per request, in the `@` form returned by `GET /teams/extensions`. > If a profile is running while the extension is deleted, the running session keeps using the extension. Once that profile stops, the extension reappears in the team's extension list. Make sure no profile is using the extensions you are about to remove. ```bash curl -X DELETE "https://app.octobrowser.net/api/v2/automation/teams/extensions" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"uuids":["54d6ff5042c545b990349a7a7e653e81@2.0.12"]}' ``` ### Example Response ```json { "success": true, "msg": "Extensions deleted successfully", "data": "" } ``` ## Get Subaccounts List the team's members. **Endpoint**: `GET /teams/subaccounts` ```bash curl -X GET "https://app.octobrowser.net/api/v2/automation/teams/subaccounts" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" ``` ### Example Response ```json { "success": true, "msg": "", "total_count": 1, "data": [ { "uuid": "54d6ff5042c545b990349a7a7e653e81", "email": "test@octo.com", "master": false, "created_at": "2023-11-27T13:07:28", "permissions": { "manage_team": false, "edit_tags": false, "view_all_tags": false, "manage_action_log": false, "proxies": { "create": false, "edit": false, "delete": false }, "paid_proxies": { "create": false }, "profiles": { "transfer": false, "clone": false, "create": false, "edit": false, "delete": false, "passwords": false }, "templates": { "create": false, "edit": false, "delete": false }, "extensions": { "delete": false }, "tasks": { "view": false, "manage": false }, "visible_tags": ["tag_name"], "view_all_folders": false, "visible_folders": [], "edit_folders": false, "view_profiles_wo_folders": false } } ] } ``` `master: true` marks the workspace owner. `created_at` is an ISO 8601 timestamp without a timezone suffix (`YYYY-MM-DDTHH:MM:SS`). ## Create Subaccount Invite a new member to the team. The address receives an email invitation; the subaccount becomes active once the invite is accepted. **Endpoint**: `POST /teams/subaccounts` **Request Body**: - `email` (string, required) — invitee's email. - `permissions` (object, optional) — see [Permission Reference](#permission-reference). Any permission you do not list defaults to `false`. ```bash curl -X POST "https://app.octobrowser.net/api/v2/automation/teams/subaccounts" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "email": "test@octo.net", "permissions": { "manage_team": false, "edit_tags": false, "view_all_tags": false, "manage_action_log": false, "proxies": { "create": false, "edit": false, "delete": false }, "paid_proxies": { "create": false }, "profiles": { "transfer": false, "clone": false, "create": false, "edit": false, "delete": false, "passwords": false }, "templates": { "create": false, "edit": false, "delete": false }, "extensions": { "delete": false }, "tasks": { "view": false, "manage": false }, "visible_tags": ["tag_name"], "view_all_folders": false, "visible_folders": [], "edit_folders": false, "view_profiles_wo_folders": false } }' ``` ### Example Response ```json { "success": true, "msg": "Invite sent", "data": "" } ``` ## Update Subaccount Change a subaccount's permissions. The subaccount is identified by `email`. **Endpoint**: `PATCH /teams/subaccounts` **Request Body**: same shape as [Create Subaccount](#create-subaccount). Permissions you omit default to `false`, so always send the full permissions block when patching. ```bash curl -X PATCH "https://app.octobrowser.net/api/v2/automation/teams/subaccounts" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "email": "test@octo.net", "permissions": { "manage_team": true, "edit_tags": true, "view_all_tags": true, "manage_action_log": false, "proxies": { "create": true, "edit": true, "delete": true }, "paid_proxies": { "create": true }, "profiles": { "transfer": true, "clone": true, "create": true, "edit": true, "delete": true, "passwords": true }, "templates": { "create": true, "edit": true, "delete": true }, "extensions": { "delete": true }, "tasks": { "view": true, "manage": true }, "visible_tags": [], "view_all_folders": true, "visible_folders": [], "edit_folders": true, "view_profiles_wo_folders": true } }' ``` ### Example Response ```json { "success": true, "msg": "Team member updated", "data": { "uuid": "d2874db6b3344506946c1bb91bd17bf8" } } ``` ## Delete Subaccount Remove a team member by email. **Endpoint**: `DELETE /teams/subaccounts` **Request Body**: - `email` (string, required) ```bash curl -X DELETE "https://app.octobrowser.net/api/v2/automation/teams/subaccounts" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"email":"test@octo.net"}' ``` ### Example Response ```json { "success": true, "msg": "Team member deleted", "data": "" } ``` ## Get Invites List pending invitations created via `POST /teams/subaccounts` that have not yet been accepted. **Endpoint**: `GET /teams/invites` ```bash curl -X GET "https://app.octobrowser.net/api/v2/automation/teams/invites" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" ``` ### Example Response ```json { "success": true, "msg": "", "total_count": 1, "data": [ { "receiver": "test_receiver@octo.net", "created_at": "2023-11-27T13:07:28" } ] } ``` ## Delete Invite Cancel a pending invitation by recipient email. **Endpoint**: `DELETE /teams/invites` **Request Body**: - `receiver` (string, required) — email address the invite was sent to. ```bash curl -X DELETE "https://app.octobrowser.net/api/v2/automation/teams/invites" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"receiver":"test@octo.net"}' ``` ### Example Response ```json { "success": true, "msg": "Invite deleted", "data": "" } ``` ## Permission Reference Top-level boolean flags: | Field | Description | |---------------------|-------------| | `manage_team` | Add, edit, and remove subaccounts. | | `edit_tags` | Create, rename, recolour, and delete tags. | | `view_all_tags` | See every tag on the workspace; otherwise the subaccount only sees tags listed in `visible_tags`. | | `manage_action_log` | Access and manage the team's action log (see [action-log.md](action-log.md)). | | `edit_folders` | Create, rename, and delete folders (Octo 3.0+). | | `view_all_folders` | See every folder on the workspace; otherwise the subaccount only sees folders listed in `visible_folders`. | | `view_profiles_wo_folders` | See profiles that are not assigned to any folder. | Nested permission groups: | Group | Sub-permissions | |-----------------|------------------| | `proxies` | `create`, `edit`, `delete` | | `paid_proxies` | `create` | | `profiles` | `create`, `edit`, `delete`, `transfer`, `clone`, `passwords` | | `templates` | `create`, `edit`, `delete` | | `extensions` | `delete` | | `tasks` | `view`, `manage` — see and manage tasks. | `visible_tags` is an array of tag names — when `view_all_tags` is `false`, the subaccount can see only profiles tagged with at least one of these tag names. `visible_folders` works the same way for folders when `view_all_folders` is `false` (Octo 3.0+). ## Common Errors - **400 Bad Request** — invalid email, malformed permissions, or missing required field. - **403 Forbidden** — the calling token does not have `manage_team` permission. - **404 Not Found** — subaccount, invite, or extension UUID does not exist. - **409 Conflict** — subaccount already exists with that email, or an invite has already been issued. --- # File: api/action-log.md # Action Log API The action log is a real-time audit stream of team events (profile starts, stops, proxy changes, transfers, deletions) delivered over a WebSocket. Unlike the rest of the cloud API, it is not a REST endpoint: you open one long-lived connection and receive JSON pages as events happen. > **Live check on 2026-08-20:** an HTTP upgrade request to this path returned 404 `not_found`; the endpoint is documented in the official Postman collection and may require the API option to be enabled on the subscription — treat the details below as upstream-documented, not mirror-verified. ## Endpoint - **Type**: WebSocket - **URL**: `wss://app.octobrowser.net/api/v2/automation/ws/action_log` - **Authentication**: API token in the `X-Octo-Api-Token` header, sent with the upgrade request (same token as the REST API, see [authentication.md](authentication.md)). Example handshake with [`websocat`](https://github.com/vi/websocat): ```bash websocat -H "X-Octo-Api-Token: YOUR_API_TOKEN" \ "wss://app.octobrowser.net/api/v2/automation/ws/action_log" ``` ## Query Parameters - `after_uuid` (string, UUID, optional) — UUID of the last received entry. Use this to resume streaming from a specific point after a disconnect. - `from_timestamp` (integer, Unix timestamp, optional) — start streaming from this time. Events are only available for 24 hours. > Parts of the upstream docs call this parameter `from_time`; `from_timestamp` is the name used in the upstream parameter table and examples, and the one documented here. ```text wss://app.octobrowser.net/api/v2/automation/ws/action_log wss://app.octobrowser.net/api/v2/automation/ws/action_log?after_uuid=550e8400-e29b-41d4-a716-446655440000 wss://app.octobrowser.net/api/v2/automation/ws/action_log?from_timestamp=1707053400 ``` ## Message Format Each WebSocket message is a **StreamPage**: an `items` array of up to 100 events plus a `watermark` cursor. There is no `{ success, msg, data }` envelope. **Response** ```json { "items": [ { "uuid": "550e8400-e29b-41d4-a716-446655440000", "action": "profiles.started", "time": 1707053400, "user_email": "user@example.com", "object_type": "profile", "object_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "object_title": "Octo Browser Profile", "data": { "connection_data": { "ip": "203.0.113.42", "countryCode": "FR", "countryName": "France", "subdivisions": ["Île-de-France", "Paris"], "postalCode": "75008", "cityName": "Paris", "timezone": "Europe/Paris", "lat": 48.8566, "lon": 2.3522, "languages": ["fr", "en-US", "en"], "isp": "Example Networks Ltd", "connectionType": "Cable/DSL" } } } ], "watermark": { "uuid": "550e8400-e29b-41d4-a716-446655440000", "time": 1707053400 } } ``` ### Item Fields - `uuid` (string) — unique id of the log entry; feed it back as `after_uuid` to resume. - `action` (string) — event type, see [Event Types](#event-types). - `time` (integer) — Unix timestamp of the event. - `user_email` (string) — team member who triggered the event. - `object_type` (string) — type of the affected object, e.g. `profile`. - `object_id` (string) — UUID of the affected object. - `object_title` (string) — human-readable title of the affected object. - `data` (object) — event-specific payload; for profile start events it carries `connection_data`. ### `connection_data` Fields - `ip` (string) — egress IP address of the profile's connection. - `countryCode` / `countryName` (string) — geolocation of the IP. - `subdivisions` (array of string) — region / city hierarchy. - `postalCode`, `cityName` (string) — postal code and city name. - `timezone` (string) — IANA timezone, e.g. `Europe/Paris`. - `lat`, `lon` (number) — approximate coordinates. - `languages` (array of string) — languages derived from the location. - `isp` (string) — provider name. - `connectionType` (string) — e.g. `Cable/DSL`. ### Watermark The `watermark` object acts as a cursor: - contains `uuid` and `time` of the last record in the page; - guarantees stable ordering even when several records share the same timestamp; - store it after every message and pass `after_uuid` (or `from_timestamp`) on reconnect to continue where you left off. ## Event Types - `profiles.started` — profile browser was started - `profiles.stopped` — profile browser was stopped - `profiles.force_stopped` — profile browser was force stopped - `profiles.transferred` — profile was transferred - `profiles.trashed` — profile was moved to the trash bin - `profiles.updated` — profile was updated - `profiles.deleted` — profile was deleted - `profiles.proxy_assigned` — proxy was assigned to a profile - `profiles.proxy_unassigned` — proxy was unassigned from a profile - `profiles.proxy_changed` — profile's proxy was changed to another one ## How It Works 1. Connect to the WebSocket endpoint with the token header. 2. Receive pages of events as JSON messages. 3. Persist the `watermark` from each page and use it on reconnect. ## Constraints - **Event retention**: 24 hours; entries older than 24 h are no longer available. - **Batch size**: up to 100 records per page. - **Subscription**: the team must have the API option in its subscription. - **Permissions**: subaccounts presumably need the `manage_action_log` permission (see [teams.md](teams.md)); this was not verified. ## Common Errors - **401 Unauthorized** — missing or invalid `X-Octo-Api-Token` header. - **403 Forbidden** — likely when the API option is not enabled on the subscription or the subaccount lacks `manage_action_log` (not verified). - **404 Not Found** (`not_found`) — returned to a plain HTTP/upgrade request when the stream is unavailable for the account (observed on 2026-08-20; likely the API option is missing from the subscription). See [errors.md](errors.md) for the standard error envelope. --- # File: api/fingerprint.md # Fingerprint API The `fingerprint` object on a profile controls every detectable browser characteristic. Three lookup endpoints expose the values accepted by that object. All endpoints return the standard envelope `{ "success": bool, "msg": string, "data": ..., "code": string | null }`. The renderers endpoint also returns `total_count` and `page` for pagination; screens and device models return a bare `data` array. ## Fingerprint Object The `fingerprint` field is required on profile creation. Only `os` is mandatory inside it; the other fields are optional and the server fills sensible defaults when omitted. ### Common Fields | Field | Type | Notes | |---------------|---------|-------| | `os` | enum | `win`, `mac`, `android`. Required. | | `os_arch` | string | `x86` or `arm`. Defaults depend on `os`; use `arm` for Android and Apple silicon Macs. | | `os_version` | string | E.g. `10`, `11` for Windows; `12`–`15` for Android (model-dependent — use only versions listed in `os_versions` for the chosen `device_model`, otherwise the API returns `fingerprints.invalid_mobile_model`). | | `user_agent` | string | Up to 255 chars. | | `screen` | string | E.g. `1920x1080`. Pick from `GET /fingerprint/screens`. | | `cpu` | int | One of `2, 4, 6, 8, 10, 11, 12, 14, 16, 20, 24`. | | `ram` | int | One of `2, 4, 8, 12, 16, 24, 32, 64` (GB). | | `renderer` | string | WebGL renderer; pick from `GET /fingerprint/renderers`. | | `fonts` | array | List of system font names. | | `dns` | string | Custom DNS server. | | `noise` | object | Anti-fingerprinting noise toggles, see below. | | `media_devices` | object | `video_in`, `audio_in`, `audio_out` — integers 0–5. | | `languages` | object | See [Languages](#languages-configuration). | | `timezone` | object | See [Timezone](#timezone-configuration). | | `geolocation` | object | See [Geolocation](#geolocation-configuration). | | `webrtc` | object | See [WebRTC](#webrtc-configuration). | ### Mobile Fields When `os` is `android`, the following extra fields apply: | Field | Type | Notes | |-----------------|--------|-------| | `device_type` | enum | `phone` or `tablet`. | | `device_model` | string | Model identifier from `GET /fingerprint/device_models`. | Mobile (Android) profiles do not support browser extensions. ## Get Renderers List WebGL renderer strings filtered by OS and architecture. Paginated. **Endpoint**: `GET /fingerprint/renderers` **Query Parameters**: - `os` (string, optional) — `win`, `mac`, or `android`. Defaults to `win`. Any other value (e.g. `lin`) is rejected with `400` (`Input should be 'win', 'mac' or 'android'`). - `os_arch` (string, optional) — `x86` or `arm`. - `page_len` (int, optional) — page size; must be one of `10, 25, 50, 100`, any other value is rejected with `400`. - `page` (int, optional) — zero-based page number. `os=android` currently returns an empty list — mobile renderers are derived from the `device_model`. ```bash curl -X GET "https://app.octobrowser.net/api/v2/automation/fingerprint/renderers?os=win&os_arch=x86&page_len=100&page=0" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" ``` ### Example Response ```json { "success": true, "msg": "", "data": [ { "value": "ATI Radeon HD 3200 Graphics (Microsoft Corporation WDDM 1.1)", "platform": "win", "archs": ["x86"] }, { "value": "NVIDIA GeForce GTX 660", "platform": "win", "archs": ["x86"] }, { "value": "AMD Radeon HD 6800 Series", "platform": "win", "archs": ["x86"] }, { "value": "Apple M2 Pro", "platform": "mac", "archs": ["arm"] } ], "total_count": 250, "page": 0 } ``` ## Get Screens List screen resolutions for the requested platform. **Endpoint**: `GET /fingerprint/screens` **Query Parameters**: - `os` (string, optional) — `win`, `mac`, or `android`. Defaults to `win`; other values are rejected with `400`. - `os_arch` (string, optional) — `x86` or `arm`. `os=android` currently returns an empty list — mobile screens are derived from the `device_model`. ```bash curl -X GET "https://app.octobrowser.net/api/v2/automation/fingerprint/screens?os=win&os_arch=x86" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" ``` ### Example Response ```json { "success": true, "msg": "", "data": [ { "value": "1440x900", "platform": "win", "archs": ["x86"] }, { "value": "1920x1080", "platform": "win", "archs": ["x86"] }, { "value": "2560x1440 (2K)", "platform": "win", "archs": ["x86"] } ] } ``` ## Get Mobile Device Models List Android device models that can be used for mobile fingerprints. **Endpoint**: `GET /fingerprint/device_models` **Query Parameters**: - `device_type` (string, optional) — `phone` or `tablet`. The response is not paginated — all models (800+) are returned in one call, so cache it client-side. ```bash curl -X GET "https://app.octobrowser.net/api/v2/automation/fingerprint/device_models?device_type=phone" \ -H "X-Octo-Api-Token: YOUR_API_TOKEN" ``` ### Example Response ```json { "success": true, "msg": "", "data": [ { "value": "M2102J20SG", "os": "android", "os_versions": ["12"], "archs": ["arm"], "device_type": "phone" }, { "value": "SM-M526B", "os": "android", "os_versions": ["13"], "archs": ["arm"], "device_type": "phone" }, { "value": "moto g32", "os": "android", "os_versions": ["13"], "archs": ["arm"], "device_type": "phone" } ] } ``` > The endpoint returns both phones and tablets regardless of the requested `device_type` (`device_type=phone` still includes tablets such as `SM-T870`). Filter the response client-side on the `device_type` field if you need a strict match. ## Configuration Types ### Languages Configuration ```json { "type": "manual", "data": ["en-US", "en"] } ``` - `type` — `ip` (auto-detect from proxy IP), `real` (system languages), or `manual`. - `data` — array of language tags. Required only when `type=manual`. The API stores any strings verbatim without validation or normalisation (both `["ru-RU","en-US"]` and `["[ru-RU] Russian (Russia)"]` are accepted), so use well-formed BCP-47 tags such as `en-US` to keep the browser's `navigator.languages` realistic. ### Timezone Configuration ```json { "type": "manual", "data": "America/New_York" } ``` - `type` — `ip`, `real`, or `manual`. - `data` — IANA timezone string. Required only when `type=manual`. ### Geolocation Configuration ```json { "type": "manual", "data": { "latitude": 40.7128, "longitude": -74.0060, "accuracy": 100 } } ``` - `type` — `ip`, `real`, or `manual`. - `data` — `{latitude, longitude, accuracy}`. Required only when `type=manual`. ### WebRTC Configuration ```json { "type": "ip" } ``` - `type` — `ip` (use proxy IP), `real`, or `disable_non_proxied_udp`. - `data` — optional explicit IP override. Not accepted when `type=real` or `type=disable_non_proxied_udp`. ### Noise Configuration ```json { "webgl": true, "canvas": false, "audio": true, "client_rects": false } ``` All four flags are independent booleans controlling fingerprint noise injection. ### Media Devices ```json { "video_in": 1, "audio_in": 1, "audio_out": 2 } ``` Each counter accepts integers 0–5. ## Fingerprint Examples ### Basic Windows Fingerprint ```json { "os": "win", "os_version": "10", "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "screen": "1920x1080", "cpu": 8, "ram": 16 } ``` ### Android Mobile Fingerprint ```json { "os": "android", "os_arch": "arm", "os_version": "13", "device_type": "phone", "device_model": "M2102J20SG", "user_agent": "Mozilla/5.0 (Linux; Android 13; M2102J20SG) AppleWebKit/537.36 Mobile Safari/537.36", "languages": { "type": "ip", "data": null }, "timezone": { "type": "ip", "data": null }, "geolocation": { "type": "ip", "data": null }, "webrtc": { "type": "ip" } } ``` ### Android Tablet Fingerprint ```json { "os": "android", "os_arch": "arm", "os_version": "13", "device_type": "tablet", "device_model": "SM-T870" } ``` ### Fully Specified Desktop Fingerprint ```json { "os": "win", "os_version": "11", "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "screen": "1920x1080", "cpu": 8, "ram": 16, "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 620 Direct3D11 vs_5_0 ps_5_0)", "languages": { "type": "manual", "data": ["en-US", "en"] }, "timezone": { "type": "manual", "data": "America/New_York" }, "geolocation": { "type": "manual", "data": { "latitude": 40.7128, "longitude": -74.0060, "accuracy": 100 } }, "webrtc": { "type": "ip" }, "noise": { "webgl": true, "audio": true, "canvas": false, "client_rects": false }, "fonts": ["Arial", "Verdana", "Times New Roman"], "media_devices": { "video_in": 1, "audio_in": 1, "audio_out": 2 }, "dns": "8.8.8.8" } ``` ## Best Practices - Use values from `/fingerprint/renderers`, `/fingerprint/screens`, and `/fingerprint/device_models` rather than guessing — invalid values are rejected. - Keep `user_agent` consistent with `os` and `os_version`. - Do not attach `extensions` to Android profiles — mobile profiles do not support them. - Match `timezone`/`geolocation` to your proxy's location when realism matters. - For mobile profiles, always pair `device_type` with a `device_model` from the lookup endpoint, and pick `os_version` from that model's `os_versions` list — a version the model does not support is rejected with `fingerprints.invalid_mobile_model`. --- # File: api/local-client.md # Local Client API The Local Client API runs inside the Octo Browser desktop app on `http://localhost:58888`. It does not require an `X-Octo-Api-Token` header — authentication is implicit via the running session of the desktop app. Each path below is rooted at `http://localhost:58888`. Responses are bare JSON objects and do **not** use the `{success, msg, data}` envelope of the cloud API. Although the local API is unauthenticated, profile start/stop/password operations are charged against the team's cloud RPM/RPH budget — see the point-cost table in [rate-limiting.md](rate-limiting.md). ## Prerequisites - Octo Browser desktop app installed and running. - The user is signed in (use [Login](#login), or the `OCTO_API_TOKEN` environment variable described below, if you are scripting cold-start automation). - The local API server listens on `127.0.0.1:58888` only — no remote access. ## Error Responses The local client uses two error shapes: - **Business errors** — `400` (or another 4xx/5xx noted per endpoint) with a string `code`, a human-readable `error`, and a legacy numeric `error_code`: ```json { "code": "profiles.not_found", "error": "Profile not found", "error_code": 6 } ``` `error_code` is deprecated; for start errors it maps to the numeric table in [Start Error Codes](#start-error-codes). Prefer the string `code`. - **Validation errors** — `422` with a `detail` array describing the offending field: ```json { "detail": [{ "loc": ["body", "uuid"], "msg": "field required", "type": "value_error.missing" }] } ``` Unknown body fields are silently ignored. ## Client Setup ### Silent Installation ```powershell & '.\Octo_Browser_latest_win.exe' /qn ``` ```bash # macOS hdiutil attach ./Octo_Browser_latest_mac_arm64.dmg cp -R /Volumes/Octo\ Browser/Octo\ Browser.app /Applications hdiutil unmount /Volumes/Octo\ Browser ``` ### Signing In With an API Token The desktop client reads the `OCTO_API_TOKEN` environment variable at startup and logs in automatically, so no interactive login (or [Login](#login) call) is needed. ```powershell $env:OCTO_API_TOKEN = "YOUR_API_TOKEN"; start 'C:\Program Files\Octo Browser\Octo Browser.exe' ``` ```bash # macOS OCTO_API_TOKEN="YOUR_API_TOKEN" open -a "Octo Browser" # Linux (from the folder containing the AppImage) OCTO_API_TOKEN=YOUR_API_TOKEN ./OctoBrowser.AppImage ``` ### Running the Client Headless Set `OCTO_HEADLESS=1` to start the desktop client itself without a window (for servers and containers — see [docker.md](docker.md) for a complete Docker setup). This is independent of the per-profile `headless` flag below. ```powershell $env:OCTO_HEADLESS = "1"; start 'C:\Program Files\Octo Browser\Octo Browser.exe' ``` ```bash # macOS OCTO_HEADLESS=1 open -a "Octo Browser" # Linux (from the folder containing the AppImage) OCTO_HEADLESS=1 ./OctoBrowser.AppImage ``` ## Profile Control ### List Active Profiles Show the profiles currently launched on the device. `ws_endpoint` is populated only for profiles started in headless mode. **Endpoint**: `GET http://localhost:58888/api/profiles/active` ```bash curl -X GET http://localhost:58888/api/profiles/active ``` #### Example Response ```json [ { "uuid": "2bbfd1dbaf3349cf979787f15a9e413d", "state": "STARTED", "headless": true, "start_time": 1724173918, "ws_endpoint": "ws://127.0.0.1:55834/devtools/browser/a26c9612-6479-43f1-87ef-34590321a99a", "debug_port": "55834", "one_time": false, "browser_pid": 26616 } ] ``` ### Start Profile **Endpoint**: `POST http://localhost:58888/api/profiles/start` **Request Body**: - `uuid` (string, required) — profile UUID. - `headless` (bool, optional, default `false`). - `debug_port` (bool | int, optional, default `false`) — `true` allocates a random free port; an integer `1024–65534` pins a specific port. - `only_local` (bool, optional, default `true`) — bind the debug port to `127.0.0.1` only. - `flags` (string[], optional, default `[]`) — extra Chromium switches. **Use sparingly.** Recommended values include `--disk-cache-dir=` to keep cache between sessions (by default the cache is cleared every time the profile closes; the path must be outside the profile folder, e.g. `["--disk-cache-dir=C:/Cache"]`) and `--disable-backgrounding-occluded-windows` for parallel automation. Use `--remote-debugging-address=0.0.0.0` to expose the debug port to the LAN. - `timeout` (int, optional, seconds, default `60`) — overrides the default start timeout; raise it for slow proxies. - `password` (string, optional) — profile password if one is set. - `profile_data` (object, optional) — overrides for selected profile fields, e.g. `{"images_load_limit": null}` (size in bytes). ```bash curl -X POST http://localhost:58888/api/profiles/start \ -H "Content-Type: application/json" \ -d '{ "uuid": "2bbfd1dbaf3349cf979787f15a9e413d", "headless": false, "debug_port": true, "only_local": true, "flags": [], "timeout": 120 }' ``` #### Example Response ```json { "uuid": "2bbfd1dbaf3349cf979787f15a9e413d", "state": "STARTED", "headless": true, "start_time": 1724172886, "ws_endpoint": "ws://127.0.0.1:54739/devtools/browser/4d05ab38-20bf-45e6-b463-6bc643028107", "debug_port": "54739", "one_time": false, "browser_pid": 10108, "connection_data": { "ip": "188.188.188.88", "country": "Germany" } } ``` #### Error Responses ```json { "code": "profiles.not_found", "error": "Profile not found", "error_code": 6 } ``` ```json { "code": "profiles.already_started", "error": "Profile is already started", "error_code": 2 } ``` ```json { "code": "profiles.proxy_error", "error": "Failed to get proxy data", "error_code": 4 } ``` A missing `uuid` returns `422` with a `detail` array (see [Error Responses](#error-responses)). #### Start Error Codes Legacy numeric `error_code` values: ```text 1 ComponentNotFoundException 2 ProfileAlreadyRunningException 3 ProfileStartFailedException 4 GetProxyDataFailedException 5 InvalidProxyDataException 6 ProfileNotFoundException 7 NoSubscriptionException 8 OutdatedVersionException 9 ProfileVersionConsistencyError ``` ### Stop Profile **Endpoint**: `POST http://localhost:58888/api/profiles/stop` **Request Body**: - `uuid` (string, required) ```bash curl -X POST http://localhost:58888/api/profiles/stop \ -H "Content-Type: application/json" \ -d '{"uuid":"2bbfd1dbaf3349cf979787f15a9e413d"}' ``` #### Example Response ```json { "msg": "Profile stopped" } ``` ### Force Stop Profile Forcibly terminate a profile. Requires Octo Browser **1.7 or later**. **Endpoint**: `POST http://localhost:58888/api/profiles/force_stop` **Request Body**: - `uuid` (string, required) ```bash curl -X POST http://localhost:58888/api/profiles/force_stop \ -H "Content-Type: application/json" \ -d '{"uuid":"2bbfd1dbaf3349cf979787f15a9e413d"}' ``` #### Example Response ```json { "msg": "Profile stopped successfully" } ``` ### Start a One-Time Profile Spin up a temporary profile that is not saved to the account. The full fingerprint/proxy/cookies payload is provided inline. One-time profiles are faster to create and start, are not synchronised to the cloud, and are removed when stopped, so they do not clutter the profile list — well suited to scraping-style workloads. Each call costs **4 requests** of the RPM/RPH budget (see [rate-limiting.md](rate-limiting.md)). **Endpoint**: `POST http://localhost:58888/api/profiles/one_time/start` **Request Body**: - `profile_data` (object, required) — same shape as `POST /profiles` body in [profiles.md](profiles.md), minus saved-only fields like `tags` and `pinned_tag`. Any field you omit is auto-generated. In addition to `fingerprint` (including `noise`, `dns`, `media_devices`), `proxy` and `start_pages`, it may carry `extensions` (`"@"` strings), `local_cache` (bool) and inline `cookies[]` in Chrome-extension export shape (`domain`, `expirationDate`, `hostOnly`, `httpOnly`, `name`, `path`, `sameSite`, `secure`, `value`). - `headless` (bool, optional, default `true`) — note the default differs from [Start Profile](#start-profile). - `debug_port` (bool, optional, default `true`) - `flags` (string[], optional, default `[]`) - `timeout` (int, optional, seconds, default `60`) ```bash curl -X POST http://localhost:58888/api/profiles/one_time/start \ -H "Content-Type: application/json" \ -d '{ "profile_data": { "fingerprint": { "os": "win", "os_version": "11", "os_arch": "x86", "screen": "1920x1080", "languages": { "type": "ip" }, "timezone": { "type": "ip" }, "geolocation":{ "type": "ip" }, "webrtc": { "type": "ip" }, "cpu": 4, "ram": 8, "noise": { "webgl": true, "canvas": false, "audio": true, "client_rects": false }, "dns": "1.1.1.1", "media_devices": { "video_in": 1, "audio_in": 1, "audio_out": 1 } }, "extensions": ["ewbjmajocgfcbeboaewbfgobmjsjcoja@1.0"], "proxy": { "type": "socks5", "host": "1.1.1.1", "port": 5555, "login": "", "password": "" }, "start_pages": ["https://fb.com"], "local_cache": true, "cookies": [ { "domain": ".google.com", "expirationDate": 1639134293.313654, "hostOnly": false, "httpOnly": false, "name": "1P_JAR", "path": "/", "sameSite": "no_restriction", "secure": true, "value": "2021-11-10-11" } ] }, "headless": false, "debug_port": true, "timeout": 60 }' ``` #### Example Response ```json { "uuid": "9a906e7d45124e6fb37388633277c22f", "state": "STARTED", "headless": false, "start_time": 1702904780, "ws_endpoint": "ws://127.0.0.1:63269/devtools/browser/f7aa4e97-c300-404f-b9c7-2633db0c1515", "debug_port": "63269", "one_time": true, "browser_pid": 4684, "connection_data": { "ip": "188.188.188.88", "country": "Germany" } } ``` ### Set Profile Password (Local) Set or update the password protecting a single profile. Length must be 4–255 characters. **Endpoint**: `POST http://localhost:58888/api/profiles/password` **Request Body**: - `uuid` (string, required) - `password` (string, required) ```bash curl -X POST http://localhost:58888/api/profiles/password \ -H "Content-Type: application/json" \ -d '{"uuid":"9585dc0cdc1e497896afe81ba1fbcdb6","password":"password"}' ``` #### Example Response ```json { "msg": "Profile password has been set" } ``` #### Error Responses `503`: ```json { "code": "profiles.password_change_failed", "error": "Failed to change profile password", "error_code": 1 } ``` ### Delete Profile Password (Local) Remove the password from a profile. The current password must be supplied. **Endpoint**: `DELETE http://localhost:58888/api/profiles/password` **Request Body**: - `uuid` (string, required) - `password` (string, required) — current password. ```bash curl -X DELETE http://localhost:58888/api/profiles/password \ -H "Content-Type: application/json" \ -d '{"uuid":"9585dc0cdc1e497896afe81ba1fbcdb6","password":"password"}' ``` #### Example Response ```json { "msg": "Profile password has been cleared" } ``` #### Error Responses `400`: ```json { "code": "profiles.invalid_password", "error": "Profile password incorrect", "error_code": 1 } ``` ```json { "error": "Another profile's operation in progress", "error_code": 3 } ``` ```json { "error": "Failed to clear profile password", "error_code": 1 } ``` ## Auth Requires Octo Browser **1.8.0 or later**. ### Login Sign in the desktop app from a script. Alternatively, launch the client with the `OCTO_API_TOKEN` environment variable set (see [Signing In With an API Token](#signing-in-with-an-api-token)). **Endpoint**: `POST http://localhost:58888/api/auth/login` **Request Body**: - `email` (string, required) - `password` (string, required) - `api_token` (string, optional) ```bash curl -X POST http://localhost:58888/api/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"useremail@domain.net","password":"userpassword"}' ``` #### Example Response ```json { "msg": "Logged in successfully" } ``` ### Logout **Endpoint**: `POST http://localhost:58888/api/auth/logout` ```bash curl -X POST http://localhost:58888/api/auth/logout ``` #### Example Response ```json { "msg": "Logged out successfully" } ``` ### Username Return the email of the currently signed-in user. **Endpoint**: `GET http://localhost:58888/api/username` ```bash curl -X GET http://localhost:58888/api/username ``` #### Example Response ```json { "username": "user@example.com" } ``` ## Updates ### Get Client Version Returns the installed and the latest available browser versions. `update_required: true` indicates the current version is no longer supported and an update is strongly recommended. **Endpoint**: `GET http://localhost:58888/api/update` ```bash curl -X GET http://localhost:58888/api/update ``` #### Example Response ```json { "current": "1.8.2", "latest": "1.8.3", "update_required": false } ``` ```json { "current": "1.7.6", "latest": "1.8.3", "update_required": true } ``` ### Update Client Trigger an in-place update to the latest available version. Returns `409` if the browser is already up to date. **Endpoint**: `POST http://localhost:58888/api/update` ```bash curl -X POST http://localhost:58888/api/update ``` #### Example Response ```json { "msg": "update to 1.8.3 triggerred successfully" } ``` #### Error Responses `409`: ```json { "code": "client.update_failed", "error": "Update is not triggered. No available version" } ``` ## Python Example ```python import requests LOCAL = "http://localhost:58888" PROFILE_UUID = "2bbfd1dbaf3349cf979787f15a9e413d" start = requests.post(f"{LOCAL}/api/profiles/start", json={ "uuid": PROFILE_UUID, "headless": True, "debug_port": True, }).json() print("ws_endpoint:", start["ws_endpoint"]) # ... drive Chromium via Playwright/Puppeteer ... requests.post(f"{LOCAL}/api/profiles/stop", json={"uuid": PROFILE_UUID}) ``` --- # File: api/docker.md # Docker & Kubernetes Octo Browser does not ship a public registry image — you build a container yourself by downloading the Linux release inside a Dockerfile. The recipes below come straight from the official Postman documentation and run the desktop app inside a virtual X server (`Xvfb`) so its local API on port `58888` is reachable from the host. ## Dockerfile Builds an Ubuntu 22.04 image, installs Chrome, Octo dependencies, the Octo Browser AppImage, and starts everything under `Xvfb` in headless mode. ```dockerfile FROM ubuntu:22.04 ARG TZ=America/Los_Angeles ARG DEBIAN_FRONTEND=noninteractive ENV LANG="C.UTF-8" RUN apt-get update && apt-get install -y \ apt-transport-https \ ca-certificates \ curl \ gnupg \ unzip \ libgles2 libegl1 xvfb \ --no-install-recommends \ && curl -sSL https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \ && echo "deb https://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \ && apt-get update && apt-get install -y \ fontconfig \ fonts-ipafont-gothic \ fonts-kacst \ fonts-noto \ fonts-symbola \ fonts-thai-tlwg \ fonts-wqy-zenhei \ connect-proxy \ dnsutils \ fonts-freefont-ttf \ iproute2 \ iptables \ iputils-ping \ net-tools \ openvpn \ procps \ socat \ ssh \ sshpass \ sudo \ tcpdump \ telnet \ traceroute \ tzdata \ vim-nox RUN curl https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb --output /tmp/chrome.deb RUN apt install -y /tmp/chrome.deb # Octo dependencies RUN apt update && apt install -y libgl1 libglib2.0-0 xvfb zip RUN mkdir -p /home/octo/browser # Create the unprivileged "octo" user RUN groupadd -r octo \ && useradd -r -g octo -s /bin/bash -m -G audio,video,sudo -p $(echo 1 | openssl passwd -1 -stdin) octo \ && mkdir -p /home/octo/ \ && chown -R octo:octo /home/octo RUN mkdir -p /etc/sudoers.d \ && echo 'octo ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/octo \ && chmod 0440 /etc/sudoers.d/octo RUN usermod -a -G sudo octo USER octo # Install Octo Browser RUN curl -o /home/octo/browser/octo-browser.tar.gz https://binaries.octobrowser.net/releases/installer/OctoBrowser.linux.tar.gz RUN tar -xzf /home/octo/browser/octo-browser.tar.gz -C /home/octo/browser # Start Xvfb and Octo Browser in headless mode CMD Xvfb :1 -ac -screen 0 "1920x1080x24" -nolisten tcp +extension GLX +render -noreset & \ sudo chown -R octo:octo /home/octo && \ sleep 5 && DISPLAY=:1 OCTO_HEADLESS=1 /home/octo/browser/OctoBrowser.AppImage ``` ## run.sh Build the image, run the container with the local API mapped to host port `58895`, then sign in and start a profile via the local API. ```bash export EMAIL=your_email export PASSWORD=your_password export PROFILE_UUID=PUT_UUID_HERE docker build -t octobrowser:latest . docker run --name octo -it --rm \ --security-opt seccomp:unconfined \ -v '/srv/docker_octo/cache:/home/octo/.Octo Browser/' \ -p 58895:58888 \ octobrowser:latest # Drive the container's local API (xh: https://github.com/ducaale/xh/releases) xh POST localhost:58895/api/auth/login email=${EMAIL} password=${PASSWORD} xh POST localhost:58895/api/profiles/start uuid=${PROFILE_UUID} headless:=true debug_port:=true ``` The volume mount under `/home/octo/.Octo Browser/` persists profile cache between runs. After `start`, the response contains `ws_endpoint` and `debug_port`. Connect your automation library (Puppeteer, Playwright, Selenium) to those exactly as in [automation.md](automation.md). ## Golden Image (Pre-Authenticated Snapshot) The basic recipe above authenticates with email/password on every container start and re-downloads ~1.8–2.2 GB of browser components (`Octium`, `Tentacle`, `Profile_Parser`, `Tentacle_Ssh`, `userdata`) unless the cache is persisted. Octo offers two ways to avoid that: - **Method 1 — components folder on the host.** Suitable when you have a static host that builds and runs the containers. Mount the components directory into every container: ```bash -v '/srv/docker_octo/cache:/home/octo/.Octo Browser/' \ ``` or in `docker-compose.yaml`: ```yaml volumes: - /srv/docker_octo/cache:/home/octo/.Octo Browser/ ``` - **Method 2 — snapshot baked into the image.** Suitable when there is no static host: the image can be shipped to any number of machines and containers start with components already in place. Authentication is done by passing an API token via the `OCTO_API_TOKEN` environment variable. The rest of this section covers this method. Caveat for either method: you may hit operational limits if you run more than 5 containers simultaneously **and** they all share the same IP address **and** log into the same Octo account. ### Requirements - Docker, Bash, and `jq` on the build machine (Git Bash or WSL2 on Windows). - An Octo Browser API token. - About 6 GB of free disk space: `octo:base` ~2.3 GB, `octo:ready` ~4.9 GB (layers shared with the base image are not duplicated), snapshot ~700 MB. - 15–40 minutes for the first full build depending on network speed — most of it is package installation in the base image and downloading ~1.8–2.2 GB of components. Place all five files below in one folder and run the build from that folder. ### Step 1: `creds.json` ```json {"octo_api_token":"your_token_here"} ``` Replace `your_token_here` with your API token. ### Step 2: `dockerfile` — base image Same as the basic Dockerfile, but sets `HOME`, `APPIMAGE_EXTRACT_AND_RUN=1` and `OCTO_DATA` in `ENV`, adds `jq x11vnc libgl1 libglib2.0-0 zip`, and ends with `CMD ["/bin/bash"]` instead of launching the browser — the entrypoint is added later in `dockerfile.final`. ```dockerfile FROM ubuntu:22.04 ARG TZ=America/Los_Angeles ARG DEBIAN_FRONTEND=noninteractive ENV LANG="C.UTF-8" HOME=/home/octo \ APPIMAGE_EXTRACT_AND_RUN=1 \ OCTO_DATA="/home/octo/.Octo Browser" RUN apt-get update && apt-get install -y \ apt-transport-https ca-certificates curl gnupg unzip jq \ libgles2 libegl1 xvfb x11vnc --no-install-recommends \ && curl -sSL https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \ && echo "deb https://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \ && apt-get update && apt-get install -y \ fontconfig fonts-ipafont-gothic fonts-kacst fonts-noto fonts-symbola \ fonts-thai-tlwg fonts-wqy-zenhei fonts-freefont-ttf \ connect-proxy dnsutils iproute2 iptables iputils-ping net-tools \ openvpn procps socat ssh sshpass sudo tcpdump telnet traceroute \ tzdata vim-nox libgl1 libglib2.0-0 zip \ && rm -rf /var/lib/apt/lists/* RUN curl https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb -o /tmp/chrome.deb \ && apt-get update && apt-get install -y /tmp/chrome.deb && rm -f /tmp/chrome.deb \ && rm -rf /var/lib/apt/lists/* RUN groupadd -r octo \ && useradd -r -g octo -s /bin/bash -m -G audio,video,sudo \ -p "$(echo 1 | openssl passwd -1 -stdin)" octo \ && mkdir -p /home/octo/browser && chown -R octo:octo /home/octo \ && mkdir -p /etc/sudoers.d \ && echo 'octo ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/octo && chmod 0440 /etc/sudoers.d/octo USER octo RUN curl -o /home/octo/browser/octo-browser.tar.gz \ https://binaries.octobrowser.net/releases/installer/OctoBrowser.linux.tar.gz \ && tar -xzf /home/octo/browser/octo-browser.tar.gz -C /home/octo/browser \ && rm -f /home/octo/browser/octo-browser.tar.gz CMD ["/bin/bash"] ``` Build the base image: ```bash docker build -t octo:base -f dockerfile . ``` ### Step 3: `entrypoint.sh` — launching the browser in the container Starts a virtual display and Octo Browser in headless mode on every container start. The browser is kept as a direct child so signals reach it and the container lives exactly as long as the browser does. ```bash #!/usr/bin/env bash # entrypoint.sh — boot headless OctoBrowser inside octo:ready. set -u export DISPLAY=:1 OCTO_DATA="${OCTO_DATA:-/home/octo/.Octo Browser}" rm -f /tmp/.X1-lock "$OCTO_DATA/local_port" rm -rf /tmp/.X11-unix Xvfb :1 -ac -screen 0 1920x1080x24 >/tmp/x.log 2>&1 & for _ in $(seq 1 10); do sleep 1 [ -S /tmp/.X11-unix/X1 ] && break done # Keep OctoBrowser as our own child so signals reach it and the container # lives exactly as long as the browser does. OCTO_HEADLESS=1 /home/octo/browser/OctoBrowser.AppImage >/tmp/octo.log 2>&1 & OCTO_PID=$! term() { kill -TERM "$OCTO_PID" 2>/dev/null || true; } trap term TERM INT wait "$OCTO_PID" ``` ### Step 4: `dockerfile.final` — image with the snapshot ```dockerfile # dockerfile.final — build octo:ready from octo:base + the golden snapshot. # Build: docker build -t octo:ready -f ./dockerfile.final . FROM octo:base # --chown so the unprivileged Octo user can extract the snapshot and own the result. COPY --chown=octo:octo logged-in-state.tar.gz /tmp/logged-in-state.tar.gz USER octo RUN mkdir -p "$OCTO_DATA" \ && tar -xzf /tmp/logged-in-state.tar.gz -C "$OCTO_DATA" \ && rm -f /tmp/logged-in-state.tar.gz COPY --chown=octo:octo entrypoint.sh /home/octo/entrypoint.sh RUN chmod +x /home/octo/entrypoint.sh ENTRYPOINT ["/home/octo/entrypoint.sh"] ``` You do not build this manually — `build-golden.sh` does it. ### Step 5: `build-golden.sh` — creating the snapshot Copy the script verbatim. What it does, in order: 1. Reads the token from `creds.json`, starts an `octo-golden` container from `octo:base` with `OCTO_API_TOKEN` set, launches `Xvfb` + `OctoBrowser.AppImage` with `OCTO_HEADLESS=1`. 2. Waits up to 90 s for the client to write its port to `~/.Octo Browser/local_port`. 3. Verifies authentication via `GET /api/username` on that port (restarts the app every 4th failed attempt). 4. Waits up to 20 min for components under `~/.Octo Browser/bin` — `Octium`, `Tentacle`, `Profile_Parser`, `Tentacle_Ssh`, `userdata` — to appear and their size to stabilise. `Ui` is the GUI frontend and is not downloaded in headless mode. 5. Warms up the component cache by starting a one-time profile (`POST /api/profiles/one_time/start`, `headless: true`, flags `--no-sandbox --disable-dev-shm-usage`), waits for `~/.Octo Browser/tmp/components` to stabilise, then stops it. 6. Tars `~/.Octo Browser` into `logged-in-state.tar.gz`, excluding `bcache`, `logs`, `local_port`, `localpersist.data` and `updates`. `localpersist.data` holds the machine HID — baking it in would make every container look like the same machine, and a second container would steal the first one's session. 7. Builds `octo:ready` from `dockerfile.final`, starts a throw-away `octo-check` container and confirms `GET /api/username` returns a username; on failure it dumps `docker logs`, `/tmp/octo.log` and `/tmp/x.log`. ```bash #!/usr/bin/env bash # build-golden.sh — capture a golden snapshot and build octo:ready. # Requires: octo:base already built, creds.json with an API token, docker, jq. # Run: bash build-golden.sh set -euo pipefail cd "$(dirname "$0")" step() { printf '\n\033[36m=== %s ===\033[0m\n' "$1"; } err() { printf '\033[31m%s\033[0m\n' "$*" >&2; } ok() { printf '\033[32m%s\033[0m\n' "$*"; } warn() { printf '\033[33m%s\033[0m\n' "$*"; } filesize(){ stat -c%s "$1" 2>/dev/null || stat -f%z "$1" 2>/dev/null || echo 0; } TOKEN=$(jq -r '.octo_api_token // empty' creds.json 2>/dev/null || true) if [ -z "$TOKEN" ] || printf '%s' "$TOKEN" | grep -qE '^(PASTE|<|TOKEN)'; then err 'creds.json does not contain octo_api_token. Format: {"octo_api_token":"..."}' exit 1 fi printf 'Token: %s... (length %d)\n' "${TOKEN:0:8}" "${#TOKEN}" step "Starting golden container" docker rm -f octo-golden 2>/dev/null || true docker run -d --name octo-golden --security-opt seccomp:unconfined \ -e "OCTO_API_TOKEN=$TOKEN" \ --entrypoint bash octo:base -lc "sleep 3600" step "Start Xvfb + OctoBrowser (auth via OCTO_API_TOKEN)" docker exec octo-golden bash -lc ' rm -f /tmp/.X1-lock "$HOME/.Octo Browser/local_port"; rm -rf /tmp/.X11-unix setsid bash -c "Xvfb :1 -ac -screen 0 1920x1080x24 >/tmp/x.log 2>&1" & for i in $(seq 1 10); do sleep 1; [ -S /tmp/.X11-unix/X1 ] && break; done DISPLAY=:1 setsid bash -c "OCTO_HEADLESS=1 /home/octo/browser/OctoBrowser.AppImage >/tmp/octo.log 2>&1" & ' step "Waiting for local_port to appear (up to 90 sec)" PORT="" for i in $(seq 1 18); do sleep 5 PORT=$(docker exec octo-golden bash -lc 'cat "$HOME/.Octo Browser/local_port" 2>/dev/null' 2>/dev/null || true) if [ -n "$PORT" ]; then printf ' port: %s\n' "$PORT"; break; fi printf ' ...not yet (%ds)\n' "$((i*5))" done if [ -z "$PORT" ]; then err "OctoBrowser did not come up. docker exec octo-golden cat /tmp/octo.log" exit 1 fi step "Checking authorization (OCTO_API_TOKEN)" AUTHED=false for i in $(seq 1 12); do RESP=$(docker exec octo-golden bash -lc ' P=$(cat "$HOME/.Octo Browser/local_port" 2>/dev/null) [ -n "$P" ] && curl -s http://127.0.0.1:$P/api/username 2>/dev/null ' 2>/dev/null || true) printf ' [attempt %d] %s\n' "$i" "$RESP" if printf '%s' "$RESP" | grep -q '"username"'; then AUTHED=true; break; fi if [ $((i % 4)) -eq 0 ] && [ "$i" -lt 12 ]; then warn " restarting the application..." docker exec octo-golden bash -lc ' pkill -f OctoBrowser.AppImage; sleep 3 rm -f "$HOME/.Octo Browser/local_port" DISPLAY=:1 setsid bash -c "OCTO_HEADLESS=1 /home/octo/browser/OctoBrowser.AppImage >>/tmp/octo.log 2>&1" & ' >/dev/null 2>&1 || true sleep 20 fi sleep 5 done if [ "$AUTHED" != "true" ]; then err "Authorization not confirmed. Check the token / docker exec octo-golden cat /tmp/octo.log" exit 1 fi step "Waiting for components to download (up to 20 min)" docker exec octo-golden bash -lc ' B="$HOME/.Octo Browser/bin" stable=0; prev=0 for i in $(seq 1 120); do cur=$(du -sm "$B" 2>/dev/null | cut -f1); cur=${cur:-0} missing="" # Ui is the GUI frontend and is not downloaded in headless mode. for c in Octium Tentacle Profile_Parser Tentacle_Ssh userdata; do sz=$(du -sm "$B/$c" 2>/dev/null | cut -f1); sz=${sz:-0} [ "$sz" -ge 10 ] || missing="$missing $c" done if [ -z "$missing" ] && [ "$cur" = "$prev" ]; then stable=$((stable+1)); else stable=0; fi [ "$stable" -ge 6 ] && { echo "stable ${cur}MB, all components present"; break; } prev=$cur; sleep 10 done [ -n "$missing" ] && echo "WARNING: did not wait for:$missing" du -sm "$B"/* 2>/dev/null | sort -rn ' step "Starting profile (warm-up + component-cache stabilization)" docker exec octo-golden bash -lc ' COMP="$HOME/.Octo Browser/tmp/components" P=$(cat "$HOME/.Octo Browser/local_port") RESP=$(curl -s -X POST http://127.0.0.1:$P/api/profiles/one_time/start \ -H "Content-Type: application/json" \ -d "{\"profile_data\":{\"fingerprint\":{\"os\":\"win\"}},\"headless\":true,\"timeout\":120,\"flags\":[\"--no-sandbox\",\"--disable-dev-shm-usage\"]}") UUID=$(echo "$RESP" | jq -r ".uuid // empty") echo "UUID=$UUID resp=$RESP" stable=0; prev=-1 for i in $(seq 1 60); do cur=$(du -sm "$COMP" 2>/dev/null | cut -f1); cur=${cur:-0} if [ "$cur" = "$prev" ] && [ "$cur" -gt 0 ]; then stable=$((stable+1)); else stable=0; fi echo " components: ${cur}MB (stable=$stable)" [ "$stable" -ge 3 ] && { echo "stable ${cur}MB"; break; } prev=$cur; sleep 5 done [ "${cur:-0}" -eq 0 ] && echo "WARN: component cache stayed empty (non-fatal)" [ -n "$UUID" ] && curl -s -X POST http://127.0.0.1:$P/api/profiles/stop \ -H "Content-Type: application/json" -d "{\"uuid\":\"$UUID\"}" >/dev/null \ && echo "profile stopped" || echo "WARN: stop failed (non-fatal)" ' step "Capturing snapshot logged-in-state.tar.gz" docker exec octo-golden bash -lc ' sleep 2 cd "$HOME/.Octo Browser" # localpersist.data holds the machine HID: baking it in makes every container # look like the same machine, and a second container steals the first session. # The --warning/--ignore-failed-read flags keep the still-running daemon from # turning a changed file into a fatal tar error. tar --warning=no-file-changed --warning=no-file-removed --ignore-failed-read \ --exclude=bcache --exclude=logs --exclude=local_port --exclude=localpersist.data \ --exclude=updates \ -czf /tmp/logged-in-state.tar.gz . || [ "$?" -eq 1 ] ' docker cp octo-golden:/tmp/logged-in-state.tar.gz ./logged-in-state.tar.gz docker rm -f octo-golden SZ=$(filesize logged-in-state.tar.gz) printf 'Snapshot: %d MB\n' "$((SZ / 1024 / 1024))" step "Building octo:ready" docker build -t octo:ready -f ./dockerfile.final . step "Checking octo:ready" docker rm -f octo-check 2>/dev/null || true docker run -d --init --name octo-check --security-opt seccomp:unconfined \ -e "OCTO_API_TOKEN=$TOKEN" octo:ready OK=false DEAD=false for i in $(seq 1 18); do sleep 5 if [ "$(docker inspect -f '{{.State.Running}}' octo-check 2>/dev/null || true)" != "true" ]; then DEAD=true; break fi RESP=$(docker exec octo-check bash -lc ' P=$(cat "$HOME/.Octo Browser/local_port" 2>/dev/null) [ -n "$P" ] && curl -s http://127.0.0.1:$P/api/username 2>/dev/null ' 2>/dev/null || true) if printf '%s' "$RESP" | grep -q '"username"'; then ok " OK: $RESP"; OK=true; break fi printf ' ...%ds: %s\n' "$((i*5))" "$RESP" done if [ "$OK" != "true" ]; then # Collect the logs before removing the container, or the evidence is gone. err "Check failed." if [ "$DEAD" = "true" ]; then CODE=$(docker inspect -f '{{.State.ExitCode}}' octo-check 2>/dev/null || true) err " container is not running (exit code: ${CODE:-unknown})" fi printf '\n[docker logs octo-check]\n' docker logs octo-check 2>&1 | tail -n 40 || true printf '\n[/tmp/octo.log]\n' docker exec octo-check bash -lc 'tail -n 40 /tmp/octo.log' 2>&1 \ || echo "(unavailable — container not running)" printf '\n[/tmp/x.log]\n' docker exec octo-check bash -lc 'tail -n 20 /tmp/x.log' 2>&1 \ || echo "(unavailable — container not running)" fi docker rm -f octo-check >/dev/null if [ "$OK" = "true" ]; then ok "DONE: octo:ready built, authorization works." else exit 1 fi ``` Run it: ```bash bash build-golden.sh ``` A successful run ends with: ```text === Checking octo:ready === OK: {"username":"your@email"} DONE: octo:ready built, authorization works. ``` ### Step 6: Running the Finished Image ```bash docker run -d --init --name octo-1 --security-opt seccomp:unconfined \ -e "OCTO_API_TOKEN=your_token_here" octo:ready ``` - `-e OCTO_API_TOKEN` is required for authentication — the client reads it at startup and signs in without `/api/auth/login`. - `--security-opt seccomp:unconfined` is required for Chrome's sandbox. - Add `-p 127.0.0.1::58888` if you need to reach the local API from the host; the upstream command omits it, so `octo-1` as shown is only controllable from inside the container (or from other containers on the same Docker network). ### Updating the Snapshot The snapshot also pins component versions, so it goes stale over time. To rebuild it, delete `logged-in-state.tar.gz` and run `bash build-golden.sh` again. If the browser itself must be updated, rebuild the base image without cache first: ```bash docker build --no-cache -t octo:base -f dockerfile . ``` ## Kubernetes Running the same image in a Kubernetes pod requires extra capabilities and shared-memory tuning, otherwise Chromium will crash: ```yaml apiVersion: apps/v1 kind: Deployment spec: template: spec: containers: - name: cloud-instance image: {{ .Values.octoImage }}:{{ .Values.tag }} securityContext: capabilities: add: - NET_ADMIN - SYS_ADMIN volumeMounts: - name: dshm mountPath: /dev/shm volumes: - name: default-data emptyDir: medium: Memory sizeLimit: 1Gi - name: dshm emptyDir: medium: Memory sizeLimit: 4Gi ``` Key points: - `NET_ADMIN` is required for proxy and DNS configuration. - `SYS_ADMIN` is required by Chromium's sandbox (alternative: launch Chromium with `--no-sandbox`, but this weakens isolation). - `/dev/shm` defaults to 64 MB in Kubernetes, which is too small for Chromium — mount an in-memory volume of at least 1–4 GB. ## Best Practices - **Persist the profile cache** with a volume mount on `/home/octo/.Octo Browser/` so re-runs do not re-download fingerprint data. - **Bind the local API to the host loopback** (`-p 127.0.0.1:58895:58888`) — the local API has no auth. - **Use `OCTO_HEADLESS=1`** as in the Dockerfile; combined with `Xvfb` this is the supported headless mode. - **One container per parallel session** — the local API on `:58888` controls a single desktop app instance. - **Prefer `OCTO_API_TOKEN` over email/password in containers** — the client honours it at startup and signs in headlessly, so no `/api/auth/login` call is needed (see [Golden Image](#golden-image-pre-authenticated-snapshot)). - **Discover the local port from `~/.Octo Browser/local_port`** — the client writes its listening port there once it is up; poll that file instead of sleeping a fixed time. - **Pass `--no-sandbox --disable-dev-shm-usage`** in `flags` when starting profiles inside a container; these are the recommended in-container Chromium flags. --- # File: api/automation.md # Automation Libraries Drive Octo Browser profiles from Selenium, Playwright, Puppeteer, and similar Chromium-automation frameworks. The pattern is the same in every language: 1. Use the **cloud API** (`https://app.octobrowser.net/api/v2/automation`) to manage profiles. 2. Use the **local client API** (`http://localhost:58888`) to start a profile with `debug_port: true`. 3. Connect your automation library to the returned `ws_endpoint` (CDP WebSocket) or `debug_port`. ## Prerequisites - Octo Browser desktop app running and signed in. - A profile UUID (from the dashboard or `GET /profiles`). - The automation library of your choice installed in your environment. See [profiles.md](profiles.md) for profile creation and [local-client.md](local-client.md) for the start/stop API. ## Puppeteer (Node.js) Connects via `browserWSEndpoint` from the local start response. ```javascript const puppeteer = require('puppeteer'); const axios = require('axios'); const OCTO_REMOTE_API = axios.create({ baseURL: 'https://app.octobrowser.net/api/v2/automation/', timeout: 2000, headers: { 'X-Octo-Api-Token': 'YOUR_API_TOKEN' }, }); const OCTO_LOCAL_API = axios.create({ baseURL: 'http://localhost:58888/api/profiles/', timeout: 100000, }); async function createProfile() { const { data } = await OCTO_REMOTE_API.post('/profiles', { title: 'API Test profile', fingerprint: { os: 'win' }, }); return data; } async function startProfile(uuid) { const { data } = await OCTO_LOCAL_API.post('/start', { uuid, headless: true, debug_port: true, }); return data; } (async () => { const created = await createProfile(); const started = await startProfile(created.data.uuid); const browser = await puppeteer.connect({ browserWSEndpoint: started.ws_endpoint, defaultViewport: null, }); const page = await browser.newPage(); await page.goto('https://google.com/'); })(); ``` ## Pyppeteer (Python) ```python import asyncio import logging import os import httpx import pyppeteer logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(name)s: %(message)s') log = logging.getLogger('octo') OCTO_TOKEN = os.getenv('OCTO_TOKEN', 'PUT_TOKEN_HERE') OCTO_API = 'https://app.octobrowser.net/api/v2/automation/profiles' LOCAL_API = 'http://localhost:58888/api/profiles/start' HEADERS = {'X-Octo-Api-Token': OCTO_TOKEN} async def get_profile(cli): profiles = (await cli.get(OCTO_API, headers=HEADERS)).json() return profiles['data'][0]['uuid'] async def get_cdp(cli): uuid = await get_profile(cli) resp = (await cli.post(LOCAL_API, json={'uuid': uuid, 'debug_port': True})).json() return resp['ws_endpoint'] async def main(): async with httpx.AsyncClient() as cli: ws_url = await get_cdp(cli) browser = await pyppeteer.launcher.connect(browserWSEndpoint=ws_url) try: page = await browser.newPage() await page.goto('https://duckduckgo.com/') finally: await browser.close() if __name__ == '__main__': asyncio.run(main()) ``` ## Playwright (Node.js) ```javascript const axios = require('axios'); const pw = require('playwright'); const OCTO_REMOTE_API = axios.create({ baseURL: 'https://app.octobrowser.net/api/v2/automation/', timeout: 2000, headers: { 'X-Octo-Api-Token': 'YOUR_API_TOKEN' }, }); const OCTO_LOCAL_API = axios.create({ baseURL: 'http://localhost:58888/api/profiles/', timeout: 100000, }); async function createProfile() { const { data } = await OCTO_REMOTE_API.post('/profiles', { title: 'API Test profile', fingerprint: { os: 'win' }, }); return data; } async function startProfile(uuid) { const { data } = await OCTO_LOCAL_API.post('/start', { uuid, headless: false, debug_port: true, }); return data; } (async () => { const created = await createProfile(); const started = await startProfile(created.data.uuid); const browser = await pw.chromium.connectOverCDP(started.ws_endpoint); const context = browser.contexts()[0]; const page = context.pages()[0]; await page.goto('https://google.com'); })(); ``` ## Playwright Sync Python ```python import httpx from playwright.sync_api import sync_playwright PROFILE_UUID = "UUID_OF_YOUR_PROFILE" def main(): with sync_playwright() as p: start_response = httpx.post( 'http://localhost:58888/api/profiles/start', json={'uuid': PROFILE_UUID, 'headless': False, 'debug_port': True}, ) if not start_response.is_success: print(f'Start response is not success: {start_response.json()}') return ws_endpoint = start_response.json().get('ws_endpoint') browser = p.chromium.connect_over_cdp(ws_endpoint) page = browser.contexts[0].pages[0] page.goto('https://google.com') browser.close() if __name__ == '__main__': main() ``` ## Playwright Async Python ```python import asyncio import httpx from playwright.async_api import async_playwright PROFILE_UUID = "UUID_OF_YOUR_PROFILE" async def main(): async with async_playwright() as p: async with httpx.AsyncClient() as client: response = await client.post( 'http://localhost:58888/api/profiles/start', json={'uuid': PROFILE_UUID, 'headless': False, 'debug_port': True}, ) if not response.is_success: print(f'Start response is not successful: {response.json()}') return ws_endpoint = response.json().get('ws_endpoint') browser = await p.chromium.connect_over_cdp(ws_endpoint) page = browser.contexts[0].pages[0] await page.goto('https://google.com') await browser.close() if __name__ == '__main__': asyncio.run(main()) ``` ## Selenium (Python) > **Heads-up:** Selenium is detectable by some anti-bot solutions. If you hit detection issues, switch to Puppeteer/Playwright or [undetected-chromedriver](https://github.com/ultrafunkamsterdam/undetected-chromedriver). ```python import requests from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service PROFILE_ID = 'PROFILE_UUID' WEBDRIVER_PATH = Service(executable_path=r'./chromedriver/chromedriver-win64/chromedriver.exe') LOCAL_API = 'http://localhost:58888/api/profiles' def get_webdriver(port): chrome_options = Options() chrome_options.add_experimental_option('debuggerAddress', f'127.0.0.1:{port}') return webdriver.Chrome(service=WEBDRIVER_PATH, options=chrome_options) def get_debug_port(profile_id): data = requests.post( f'{LOCAL_API}/start', json={'uuid': profile_id, 'headless': False, 'debug_port': True}, ).json() return data['debug_port'] def main(): port = get_debug_port(PROFILE_ID) driver = get_webdriver(port) driver.get('http://amazon.com') if __name__ == '__main__': main() ``` ## Selenium (Java) ```java import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.json.JSONObject; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; public class Main { private static final String PROFILE_ID = "PROFILE_UUID"; private static final String CHROME_DRIVER = "./path/to/chromedriver.exe"; private static final String LOCAL_API = "http://localhost:58888/api/profiles"; public static void main(String[] args) throws Exception { int port = getDebugPort(PROFILE_ID); WebDriver driver = getWebDriver(port); driver.get("http://google.com"); } public static WebDriver getWebDriver(int port) { ChromeOptions chromeOptions = new ChromeOptions(); chromeOptions.setExperimentalOption("debuggerAddress", "127.0.0.1:" + port); System.setProperty("webdriver.chrome.driver", CHROME_DRIVER); return new ChromeDriver(chromeOptions); } public static int getDebugPort(String profileId) throws Exception { try (CloseableHttpClient httpClient = HttpClients.createDefault()) { HttpPost httpPost = new HttpPost(LOCAL_API + "/start"); JSONObject json = new JSONObject(); json.put("uuid", profileId); json.put("headless", false); json.put("debug_port", true); httpPost.setEntity(new StringEntity(json.toString())); httpPost.setHeader("Accept", "application/json"); httpPost.setHeader("Content-type", "application/json"); try (CloseableHttpResponse response = httpClient.execute(httpPost)) { JSONObject responseJson = new JSONObject(EntityUtils.toString(response.getEntity())); return responseJson.getInt("debug_port"); } } } } ``` ## Selenium (Visual Basic .NET) ```vbnet Imports System Imports System.Net.Http Imports System.Text Imports Newtonsoft.Json.Linq Imports OpenQA.Selenium Imports OpenQA.Selenium.Chrome Module Program Public Class Main Private Shared ReadOnly PROFILE_ID As String = "PROFILE_UUID_SHOULD_BE_HERE" Private Shared ReadOnly CHROME_DRIVER As String = "./chromedriver-win64/chromedriver.exe" Private Shared ReadOnly LOCAL_API As String = "http://localhost:58888/api/profiles" Public Shared Sub Main(args As String()) Dim port As Integer = GetDebugPort(PROFILE_ID) Dim driver As IWebDriver = GetWebDriver(port) driver.Navigate().GoToUrl("http://google.com") End Sub Public Shared Function GetWebDriver(port As Integer) As IWebDriver Dim chromeOptions As New ChromeOptions() chromeOptions.DebuggerAddress = "127.0.0.1:" & port Environment.SetEnvironmentVariable("webdriver.chrome.driver", CHROME_DRIVER) Return New ChromeDriver(chromeOptions) End Function Public Shared Function GetDebugPort(profileId As String) As Integer Dim debugPort As Integer Using httpClient As New HttpClient() Dim httpPost As New HttpRequestMessage(HttpMethod.Post, LOCAL_API & "/start") Dim json As New JObject() json("uuid") = profileId json("headless") = False json("debug_port") = True httpPost.Content = New StringContent(json.ToString(), Encoding.UTF8, "application/json") httpPost.Headers.Accept.ParseAdd("application/json") Using response = httpClient.SendAsync(httpPost).Result Dim responseJson As JObject = JObject.Parse(response.Content.ReadAsStringAsync().Result) debugPort = responseJson("debug_port") End Using End Using Return debugPort End Function End Class End Module ``` ## Best Practices 1. **Stop profiles** when automation finishes (`POST /api/profiles/stop`) — abandoned profiles block re-launch. 2. **Use `ws_endpoint`** when the framework supports it (Puppeteer, Playwright) — `debug_port` is convenient for Selenium's `debuggerAddress` only. 3. **Pin `debug_port`** when running many profiles in parallel to avoid port-allocation races (`debug_port: 20000` and so on, valid range `1024–65534`). 4. **Use chromedrivers** matching your installed Chromium build — get them from [chrome-for-testing](https://googlechromelabs.github.io/chrome-for-testing/). ## Common Issues - **Connection refused** — Octo Browser app not running, or the profile was started without `debug_port: true`. - **Profile already running** — call `GET /api/profiles/active`, then `POST /api/profiles/stop` (or `force_stop`) before re-launching. - **Detected by anti-bot** — switch from Selenium to Puppeteer/Playwright, or use undetected-chromedriver. - **Timeouts on slow proxies** — raise the start `timeout` (in seconds) in the `POST /api/profiles/start` body. ---