Afina API: Antidetect Browser Automation via REST

One POST request - and you have 100 profiles created with unique fingerprints, attached proxies and browsers spun up in headless mode or with a CDP endpoint ready for Puppeteer. No cloud quotas, no tier-based tokens, no API rate limits: Afina's API is fully local, runs on 127.0.0.1:50778, and the only thing deciding how many requests you make is your own machine.
In this article we break down how the Afina antidetect browser API is built: 79 endpoints, the auth scheme, how to spin up your first profile and wire it into Puppeteer or Playwright in 5 minutes, which scenarios stop being painful once you move from the UI to the API, and where the boundary lies between the REST API and the MCP server for those wondering which one to pick.
Why You Need an Antidetect Browser API
If you work on multi-accounting across hundreds of profiles, UI routine quickly becomes the bottleneck:
- Affiliate marketing. Creating 200 profiles for a campaign by hand - two hours. Through the API - one
forloop and 30 seconds of work. - Account farming. Preparing profiles by geolocation with proxies, tags and auto-logins. Rotating proxies, warm-up routines - none of this exists without scripting.
- Web scraping. Parallel data collection from dozens of sites - every profile has its own fingerprint, its own cookie jar, its own proxy. The UI was not built for this.
- E-commerce and competitor monitoring. Prices, descriptions, promotions - impossible to gather manually, but through the API you can run as much as your infrastructure handles.
- Bonus hunting and airdrop campaigns. Sophisticated launch schedules with time distribution to bypass anti-fraud clustering - this is all API + cron.
- Betting and arbitrage. Reaction speed measured in seconds, synchronized actions across dozens of profiles. Programmatic only.
If you do the same thing more than twice - it is already a job for the API. Afina exposes a REST interface that covers 100% of the UI's capabilities - anything you can click can be triggered with a single HTTP call.
How Afina's API Differs From Cloud Alternatives
Most antidetect browsers (Octo, Multilogin, Dolphin) ship cloud APIs - your requests hit the vendor's server, pass through your plan's quota, and only then get relayed to the local application. That model has obvious downsides:
- Subscription-based limits. 1000-5000 requests per day on top-tier plans - not enough for serious automation.
- Network latency. Every call travels through the internet twice - to the cloud and back to the local client.
- Tied to cloud availability. The vendor's server goes down - all of your automation goes down with it, even if your own hardware is healthy.
- Data leaves your machine. Whatever the vendor promises, metadata about your profiles and your request parameters ends up in their infrastructure.
Afina's API is built differently: it is a local HTTP server on 127.0.0.1 that starts together with the desktop application. No cloud middlemen, no rate limits, no latency above a millisecond. Every request goes straight into the application's Rust backend, which does all the work anyway - the API just opens a second channel to it.
| Parameter | Cloud API (Octo and others) | Afina Local API |
|---|---|---|
| Requests per day | Subscription-based (1k-10k) | Unlimited |
| Latency | 50-300 ms | 1-10 ms |
| Depends on vendor cloud | Yes | No |
| Data leaves your machine | Yes | No |
| Works offline | No | Yes (for local operations) |
| Cost | Per-plan | Free |
| Direct CDP access | No, only wrapper commands | Yes, wsEndpoint is returned in the response |
This difference fundamentally changes how you architect your solutions. You can hammer tens of thousands of calls per hour without watching quotas, keep bots running on an internal network without internet egress (for anything that does not need external services), and connect directly to the Chrome DevTools Protocol for the most delicate scenarios.
5 Minutes to Get Started
Step 1. Grab the API key
Launch Afina, open Settings -> API and copy the key. It is a 32-character [A-Za-z0-9] string generated automatically on first launch and stored encrypted (libsodium sealed box) in the local database.
The key is passed in the X-API-Key header of every request. Without a valid key the server silently drops the connection - no response, no HTTP code. This is intentional: an attacker gets zero information about why the request failed and cannot distinguish "wrong key" from "server not responding".
Step 2. Check the server is alive
curl http://127.0.0.1:50778/api/health
Response:
{ "status": "ok", "version": "2.0.4" }
If you get Connection refused - Afina is not running. The health endpoint does not require a key.
Step 3. Fetch the profile list
curl http://127.0.0.1:50778/api/profiles/list \
-H "X-API-Key: YOUR_KEY"
Response:
{
"data": [
{ "accountId": "550e8400-...", "name": "Profile 1", "tags": ["test"], "isRunning": false },
{ "accountId": "660e8400-...", "name": "Profile 2", "tags": ["prod"], "isRunning": true }
],
"count": 2
}
If that call worked - everything is in place. From here on it is only a matter of imagination.
API Anatomy: 79 Endpoints by Category
All endpoints are grouped by entity domain. The base URL is http://127.0.0.1:50778, every endpoint accepts Content-Type: application/json and the X-API-Key header. Full list below.
Profiles (Accounts) - 10 Endpoints
| Path | Method | Purpose |
|---|---|---|
/api/profiles/list | GET | All accounts with filters by tags/groups |
/api/profiles/get | GET, POST | A single account by ID or UUID |
/api/profiles/create | POST | Create a permanent profile |
/api/profiles/one-time | POST | Create a one-time profile (auto-deleted on stop) |
/api/profiles/update | POST | Update parameters |
/api/profiles/start | POST | Launch the browser -> returns wsEndpoint for CDP |
/api/profiles/stop | POST | Stop (graceful shutdown with cookie flush) |
/api/profiles/delete | POST | Soft-delete (moved to trash, restorable) |
/api/profiles/hard-delete | POST | Final deletion with full data cleanup |
/api/profiles/cookies/set, /cookies/export | POST | Cookie jar import/export |
When you create a profile, all parameters travel in a single JSON - proxy, tags, groups, screen, languages, fingerprint noise, startup URLs, blocked ports, extraArgs, settings. The unique fingerprint (user agent, WebGL renderer, CPU/memory, fonts, time zone) is generated by the same backend the UI uses - these are not "random values", they are valid combinations that survive even sophisticated fingerprint checks.
Soft-delete and hard-delete are split intentionally: the first one moves the profile to trash (restorable with a single click or call), the second wipes everything - including the profile folder, cookies, cache and the token in the keychain. A running browser at hard-delete time is stopped gracefully, with no data loss.
Browser Control (CDP Commands) - 5 Endpoints
| Path | Method | Purpose |
|---|---|---|
/api/profiles/eval | POST | Run JavaScript in the current tab |
/api/profiles/screenshot | POST | Page screenshot -> base64 PNG |
/api/profiles/cookies/set | POST | Set a cookie via CDP Network.setCookies |
/api/profiles/cookies/export | POST | Read all cookies via CDP |
These are "quick touch" tools - for one-off operations without writing a full script. For serious automation (form filling, navigation, waiting for elements) you are better off using Puppeteer/Playwright via wsEndpoint - more on that below.
eval accepts a JavaScript expression, runs it in the context of the active page, auto-awaits promises and uses returnByValue. You can return a string, a number, an object - anything serializable to JSON.
RPA Scripts - 7 Endpoints
| Path | Method | Purpose |
|---|---|---|
/api/scripts/list | GET | All RPA scripts |
/api/scripts/get | GET | A single script by ID |
/api/scripts/create | POST | Create an RPA script (JSON blocks) |
/api/scripts/update | POST | Update |
/api/scripts/run | POST | Run a script on a profile -> task_uuid |
/api/scripts/run-logs | GET | Logs of a direct run |
/api/scripts/stop | POST | Stop execution |
Afina's RPA scripts are JSON structures with blocks (click, type, goto, condition, loop, executeModule and so on) and connections between them. Each block describes a step of the scenario, the executor walks them along the graph.
For programmatic script creation - pass a full JSON with elements and connections arrays to /api/scripts/create. The structure matches exactly what the visual editor sees, and any script created via API can be opened and edited in the UI.
RPA Modules - 7 Endpoints
| Path | Method | Purpose |
|---|---|---|
/api/modules/list | GET | All modules |
/api/modules/get | GET | A single module + file list |
/api/modules/create | POST | Scaffold (index.js, utils_<id>.js, package.json, settings.json) + npm install |
/api/modules/update | POST | Update files or metadata |
/api/modules/resign | POST | Recompute the Ed25519 signature |
/api/modules/delete | POST | Soft-delete |
/api/modules/hard-delete | POST | Final deletion |
When visual RPA blocks are not enough - you write a custom JavaScript module. The API does this end-to-end: creation (with automatic npm install for dependencies), file editing, re-signing.
Important detail: every module is signed with Ed25519 over an MD5 manifest of the folder. Any file change without a resign call breaks the signature - the executor blocks the run with a clear error. This protects against code tampering: you cannot run a module someone has modified behind your back.
Task Groups and Tasks - 17 Endpoints
| Path | Method | Purpose |
|---|---|---|
/api/task-groups/list | GET | List of groups |
/api/task-groups/get | GET | A group with its tasks |
/api/task-groups/tasks | GET | Just the tasks of a group |
/api/task-groups/create | POST | Create a group with a schedule |
/api/task-groups/update | POST | Update schedule/name |
/api/task-groups/start | POST | Activate (active=1) |
/api/task-groups/stop | POST | Deactivate + stop running tasks |
/api/task-groups/restart | POST | Restart error/finished tasks |
/api/task-groups/delete | DELETE, POST | Soft-delete |
/api/task-groups/hard-delete | POST | Final deletion |
/api/tasks/list | GET | All tasks (filters by status, scriptId) |
/api/tasks/active | GET | Only the ones running right now |
/api/tasks/logs | GET | Logs by task_uuid |
/api/tasks/create | POST | Add tasks to a group (bulk) |
/api/tasks/update | POST | Change status/executeAt/tag |
/api/tasks/delete | POST | Soft-delete (bulk) |
/api/tasks/stop | POST | Stop execution (with optional closeBrowser) |
This is the most powerful category in the API. A group is a schedule configuration (time windows, repeats, timeouts, parallelism limits), a task is a single run of a script on a specific account.
The schedule supports natural date formats for the executeAt field:
"now"- immediately"in 5m"- in 5 minutes"+1h"- in an hour"tomorrow 09:00"- tomorrow at 9:00 local time"2026-05-15 14:30"- a specific moment- ISO 8601:
"2026-05-15T14:30:00Z" - Unix epoch:
1715787000
Time windows and parallelism are configured at the group level:
{
"schedule": true,
"timeFrom": "09:00",
"timeTo": "18:00",
"scheduleTime": true,
"startHour": 9.0,
"endHour": 18.0,
"activeSession": 5,
"isRepeatable": true,
"repeatCount": 2,
"timeout": 300
}
The group will run at most 5 tasks in parallel, only inside the 09:00-18:00 window, will repeat each task 2 additional times, and every task auto-times-out after 5 minutes. This is the configuration for serious production campaigns - in the UI this is usually 10 minutes of clicks, but via the API it is a single POST.
Proxies - 3 Endpoints
| Path | Method | Purpose |
|---|---|---|
/api/proxies/add | POST | Add and validate |
/api/proxies/check | POST | Check one or several |
/api/proxies/check-all | POST | Audit every proxy in the table |
All checks include a real network test, including a UDP test for SOCKS5 (critical, because Afina supports HTTP/3 over SOCKS5 with QUIC). Proxies that fail are not saved. That guarantees only live connections live in your database.
More on proxy types and scenarios - in the dedicated piece on proxies and their kinds.
Databases, Variables, Keys - 15 Endpoints
| Path | Method | Purpose |
|---|---|---|
/api/databases/* | GET/POST | CRUD for connections (SQLite/MySQL/PostgreSQL) used by the RPA database block |
/api/global-vars/* | GET/POST | CRUD for global variables ${name}, available in every script |
/api/keys/* | GET/POST | Catalog of keys and API tokens (OpenAI, Telegram, ChatGPT) |
/api/accounts/vars | GET | All variables of an account (plain + encrypted) |
/api/accounts/vars/set | POST | Write (plain or encrypted) |
/api/accounts/vars/delete | POST | Delete |
Pay special attention to account_vars. They are stored in two modes:
| Storage | Where | Visible via API |
|---|---|---|
| Plain | account.settings | Yes |
| Encrypted | account_data_blob | No (write and read by key only) |
Encrypted variables are encrypted with sealed box (libsodium). Via the API you can write a value and later read it back by the same key - but the plaintext itself is never returned over the API. Decryption happens only inside the script executor right before execution. This is the perfect store for 2FA secrets, passwords and crypto wallet private keys.
Email / IMAP - 2 Endpoints
| Path | Method | Purpose |
|---|---|---|
/api/emails/list | GET | List of configured email addresses |
/api/emails/toggle | POST | Enable/disable IMAP monitoring |
Management of IMAP connections for automated email parsing (confirmation codes, recovery links, internal notifications).
Language Choice: Node, Python or Something Else
Afina's API is plain HTTP+JSON. It does not care what you make requests with. The most common options:
| Language | When to use | Recommended libraries |
|---|---|---|
| Node.js | If you already work with Puppeteer/Playwright (they are Node too) | axios, node-fetch, undici |
| Python | Data science, scraping, ML pipelines | requests, httpx, aiohttp |
| Go | High-throughput parallel processing | standard net/http |
| Bash + curl | Deployment scripts, orchestration, CI | curl + jq |
| PHP, Ruby, C#, Java | To fit an existing team stack | any HTTP library |
If you have code in one language and need to port it to another - just ask ChatGPT or Claude. With plain REST this always works. The structure stays the same: URL, method, X-API-Key header, JSON body.
For Puppeteer/Playwright/Selenium you will still need Node or Python - but they are needed not for Afina's API, but for controlling the browser itself after start_browser. These three tools are the industry standard for CDP/WebDriver automation, and Afina plays well with them out of the box.
Wiring Puppeteer and Playwright Through wsEndpoint
The most powerful feature of Afina's API is direct access to the Chrome DevTools Protocol. When you launch a profile via /api/profiles/start, the response includes wsEndpoint - a fully featured CDP WebSocket you can connect any tool to.
Node.js + Puppeteer Example
import axios from 'axios';
import puppeteer from 'puppeteer';
const API = 'http://127.0.0.1:50778';
const KEY = process.env.AFINA_API_KEY;
const headers = { 'X-API-Key': KEY };
async function main() {
// 1. Launch the profile via Afina's API
const { data } = await axios.post(
`${API}/api/profiles/start`,
{ profileId: '550e8400-e29b-41d4-a716-446655440000' },
{ headers }
);
console.log('Browser started, CDP:', data.wsEndpoint);
// 2. Connect Puppeteer to the existing browser
const browser = await puppeteer.connect({
browserWSEndpoint: data.wsEndpoint,
defaultViewport: null,
});
// 3. From here on - regular Puppeteer
const page = (await browser.pages())[0] || await browser.newPage();
await page.goto('https://example.com');
await page.waitForSelector('h1');
const title = await page.$eval('h1', el => el.innerText);
console.log('H1:', title);
// 4. Disconnect (the browser stays running)
await browser.disconnect();
// 5. Close the profile via the API
await axios.post(`${API}/api/profiles/stop`,
{ profileId: '550e8400-e29b-41d4-a716-446655440000' },
{ headers }
);
}
main().catch(console.error);
Python + Playwright Example
import os
import requests
from playwright.sync_api import sync_playwright
API = 'http://127.0.0.1:50778'
KEY = os.environ['AFINA_API_KEY']
HEADERS = {'X-API-Key': KEY}
profile_id = '550e8400-e29b-41d4-a716-446655440000'
# 1. Launch the profile
resp = requests.post(
f'{API}/api/profiles/start',
json={'profileId': profile_id},
headers=HEADERS,
).json()
ws = resp['wsEndpoint']
print('CDP:', ws)
# 2. Playwright connect
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(ws)
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto('https://example.com')
print('Title:', page.title())
browser.close()
# 3. Close the profile
requests.post(
f'{API}/api/profiles/stop',
json={'profileId': profile_id},
headers=HEADERS,
)
Selenium WebDriver
Selenium works too - via chromedriver with the --remote-debugging-port flag. Slightly less convenient (needs extra setup), but for legacy projects it is a viable option. Details in the browser action automation guide.
Ready-Made Recipes for Common Scenarios
Recipe 1. Create 100 Profiles With Proxies in a Minute
import axios from 'axios';
const API = 'http://127.0.0.1:50778';
const headers = { 'X-API-Key': process.env.AFINA_API_KEY };
const proxies = JSON.parse(await fs.readFile('proxies.json', 'utf8'));
for (let i = 0; i < 100; i++) {
await axios.post(`${API}/api/profiles/create`, {
name: `Campaign-1 #${i + 1}`,
tags: ['campaign-1', 'phase-1'],
proxy: proxies[i % proxies.length],
settings: {
languages: ['en-US', 'en'],
startupURLs: ['https://example.com/landing'],
},
}, { headers });
}
100 profiles with unique fingerprints, attached proxies and tags - ready to launch in a minute. The same job in the UI would take an hour.
Recipe 2. Bulk-Run a Script on 50 Accounts With a Schedule
// 1. Create a group with a schedule
const group = (await axios.post(`${API}/api/task-groups/create`, {
tag: 'warmup-batch-1',
active: false,
schedule: true,
timeFrom: '09:00',
timeTo: '18:00',
activeSession: 5,
isRepeatable: false,
}, { headers })).data;
// 2. Fetch the list of accounts with the tag
const accounts = (await axios.get(
`${API}/api/profiles/list?tag=campaign-1`, { headers }
)).data.data;
// 3. Add a task for each one with time staggering
const tasks = accounts.map((acc, i) => ({
accountId: acc.accountId,
scriptId: 42,
executeAt: `+${i * 3}m`,
}));
await axios.post(`${API}/api/tasks/create`, {
groupId: group.id,
tasks,
}, { headers });
// 4. Start the group
await axios.post(`${API}/api/task-groups/start`,
{ id: group.id }, { headers });
Warming up 50 accounts at 3-minute intervals, inside a 09:00-18:00 window, with at most 5 running in parallel - one file, 30 lines of code, exactly one run.
Recipe 3. Pull Logs of Every Failed Task for the Day
curl "http://127.0.0.1:50778/api/tasks/list?status=error&since=2026-05-14T00:00:00Z" \
-H "X-API-Key: $AFINA_API_KEY" \
| jq -r '.data[] | .taskUuid' \
| while read uuid; do
echo "=== $uuid ==="
curl -s "http://127.0.0.1:50778/api/tasks/logs?uuid=$uuid" \
-H "X-API-Key: $AFINA_API_KEY" \
| jq -r '.data[] | "\(.time) [\(.level)] \(.message)"'
done
Every error of the day in a single report. You can feed it to an LLM or parse with regexes to label failure causes.
Recipe 4. Eval JavaScript Against a Running Browser
curl -X POST http://127.0.0.1:50778/api/profiles/eval \
-H "X-API-Key: $AFINA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"profileId": "550e8400-...",
"code": "JSON.stringify({ url: location.href, title: document.title, cookies: document.cookie })"
}'
Response:
{
"result": "{\"url\":\"https://example.com\",\"title\":\"Example Domain\",\"cookies\":\"\"}"
}
The middle ground between nothing and full-blown Puppeteer - for quick checks and page probing.
Recipe 5. Back Up Cookies Of Every Profile With a Tag
import requests, json, os
API = 'http://127.0.0.1:50778'
H = {'X-API-Key': os.environ['AFINA_API_KEY']}
accounts = requests.get(f'{API}/api/profiles/list?tag=prod', headers=H).json()['data']
for a in accounts:
cookies = requests.post(
f'{API}/api/profiles/cookies/export',
json={'profileId': a['accountId']},
headers=H,
).json()
with open(f'backup/{a["accountId"]}.json', 'w') as f:
json.dump(cookies, f)
A full cookie-jar backup of all production profiles - use it on every CI/CD heartbeat, after a major update or simply as insurance.
Security
Afina's API was designed with the assumption that any local attack surface is an attack surface. Here is what is in place:
- Localhost only. The server binds only to
127.0.0.1. From the outside it cannot be reached even in theory. If you need remote access - do it via SSH tunnel (ssh -L 50778:127.0.0.1:50778 user@host) and decide for yourself who you trust with that tunnel. - API key on every request. Without
X-API-Keythe server drops the connection silently, with no HTTP response. An attacker cannot even tell whether a working server is listening, let alone brute-force keys. - Encrypted account vars. Sensitive data (2FA, passwords, seed phrases) is encrypted with sealed box and never returned in plaintext over the API. Only the RPA executor at script-run time has access to it.
- Signed modules. Every custom JS module is signed with Ed25519. Tampering with files without
resignbreaks the signature - the executor blocks the run. This protects against the scenario "someone reached disk and swappedindex.js". - Profile isolation. A baseline Afina feature unrelated to the API: every profile gets its own cookie jar, localStorage, IndexedDB, cache. Plugging the API in does not loosen that isolation.
- Connection limit. 128 concurrent requests at a time - a guard against accidental DoS from your own code.
- Request size limit. 16 MB per request - blocks attacks via huge payloads.
API or MCP: Which One to Pick
Afina also has an MCP server - a separate product, a thin wrapper on top of the REST API that exposes the same capabilities to AI agents (Claude, ChatGPT, Cursor) over the Model Context Protocol. The natural question follows: which one do you use - MCP or the raw API?
| Criterion | Direct REST API | MCP Server |
|---|---|---|
| Who is the user | Developer with code | AI agent or human in chat |
| Control over logic | Full | Through prompt |
| Development speed | Slower (you write code) | Instant (a sentence in chat) |
| Reproducibility | 100% (code is deterministic) | Depends on the model |
| Fit for CI/CD | Yes | No (no AI in the pipeline) |
| Integrates into existing code | Perfectly | Not designed for that |
| Level of abstraction | Endpoint level | Intent level "do X" |
Pick the REST API if:
- You have a team and a codebase where Afina integration is one of the modules.
- You need full reproducibility and control (CI/CD, recurring jobs, production campaigns).
- You want to wrap the API in your own UI or API gateway.
Pick MCP if:
- You are in a Claude/ChatGPT chat and want to "just say what you need".
- You are prototyping ideas, exploring possibilities.
- The team has no programmers, but an AI agent is already at hand.
Most serious teams end up using both: MCP - for research and quick experiments, REST API - for production.
Bottom Line
An antidetect browser without an API is a tool for solo operators. The UI is fine while you are doing tens of operations. At hundreds it slows you down; at thousands it becomes impossible.
The API turns Afina into a platform. The same backend serves both the UI buttons and the REST calls from your scripts - nothing gets lost in translation, no cloud middlemen, no request limits. Plug Puppeteer or Playwright in via wsEndpoint, orchestrate work through task groups, encrypt sensitive data in encrypted variables, sign your own JS code - and you end up with full-blown infrastructure for multi-accounting at any scale.
If your current browser (Octo, Multilogin, Dolphin) constrains you with cloud quotas and 1000 requests per day - moving to Afina with its local API gives you two orders of magnitude in scale and removes the dependency on someone else's servers.
Install Afina Browser for free, grab the API key in settings in 30 seconds, and try your first call - start with /api/health, the rest will follow. In an hour you will have automation that used to take you days in the UI.
FAQ — Frequently Asked Questions
Is keeping the API open safe? Can someone steal data through the API?
The API binds only to 127.0.0.1 - no external access, no local firewall needed. Inside the machine - any process that knows the key can call the API. If you have untrusted processes on the machine (you run third-party code) - that is not the kind of machine to run an antidetect browser on anyway. Encrypted variables are not returned even with a valid key.
Do local API calls count against any limit?
No. The API is fully local, no quotas on the plan and no vendor-side limits. You can make a million requests per hour and the only thing you hit is your machine's hardware.
Can the server port be changed?
In the current version port 50778 is hardcoded in feature_flags.rs. You can change it before compiling. At runtime - not yet, but if 50778 is busy the server automatically tries 50779-50787.
How do I know the profile is actually up and ready for CDP?
The response from /api/profiles/start only comes back after the browser has launched and opened the CDP endpoint. If you received wsEndpoint - you can connect immediately. If you got an error - the browser failed to start, read error in the response.
What happens with very heavy concurrent API traffic?
The limit is 128 simultaneous connections. Anything above that waits in queue. If you need more - batch your requests (most endpoints support bulk operations: tasks/create accepts an array, proxies/check too, and so on).
Can the API be called from another machine on the local network?
Not directly (127.0.0.1 only). But you can forward through an SSH tunnel or stand a local nginx/Caddy in front as a reverse proxy - at your own risk. The best option is to keep everything on a single machine or to use Afina's team access with role separation.
Are webhooks supported - for the API to push events on its own?
In the current version - via polling (/api/tasks/active, /api/tasks/list?status=error). Webhooks are on the roadmap.
What if I would rather not deal with raw HTTP and grab a ready library?
For Node there is the afina-mcp package - alongside the MCP server side, it exports a typed client for the REST API. You can use it as a regular library without the MCP bridge. For Python a ready-made SDK does not exist yet, but a wrapper around requests takes about an hour to write.
How long does wsEndpoint live?
As long as the browser is running. After /api/profiles/stop or a manual browser close - the endpoint becomes invalid. If the process crashed and the browser restarted, the new wsEndpoint must be fetched via another start call (it will return alreadyRunning: true together with the current wsEndpoint without restarting).
Does it behave the same on macOS, Windows and Linux?
Yes. Afina's API is an HTTP server inside the desktop application, and it is built for all three platforms from a single Rust codebase. There are no platform differences in the API.