Afina HTTP API#
Local HTTP server for external scripts, automation, and AI agents via MCP. It starts together with the Afina desktop app.
- Base URL:
http://127.0.0.1:50778 - Default port:
50778; if busy, the server tries the next ports. - JSON: all responses use
application/json, except/api/tasks/logsand/api/scripts/run-logs(text/plain). - Source of truth:
src-tauri/src/browser/server.rs.
Authentication#
All /api/* endpoints require x-api-key from Settings → General → API key. If the key is missing or invalid: 401 Unauthorized / silent connection close.
Only GET /api/health, GET /, and GET /oauth/callback?code=... are available without a key. CORS is allowed only for localhost origins (http://localhost, http://127.0.0.1, tauri://); methods GET, POST, DELETE, OPTIONS; headers Content-Type, X-API-Key.
Conventions#
id= numeric PK in SQLite;accountId= profile UUID. Most write endpoints accept both.- Boolean:
true/falseor0/1; datetime: ISO-8601, defaultdatetime('now'). */delete= soft delete (isDeleted=1);*/hard-delete= physical deletion from DB/disk.*/update= PATCH for submitted fields only.- Write endpoints emit
reload-eventfor UI refresh.
Health#
GET /api/health
Heartbeat without API key. Response: { "status": "ok", "running": 2 }, where running is the number of running browsers.
Profiles - accounts (CRUD)#
GET /api/profiles/list
Lists accounts; each item includes isRunning, tags:[{id,name,color}], groups:[{id,name}].
Query params (optional, AND):
| Parameter | Type | Description |
|---|---|---|
isRunning | true/false | Running/stopped |
groupId | int | Filter by group |
tagId | int | Filter by tag |
Response: { message, count, accounts:[{ id, accountId, name, ua, proxyId, os, macChip, isRunning, tags, groups }] }.
GET /api/profiles/get / POST /api/profiles/get
One account by UUID: query/body { accountId }. Response: { message, profile:{...} }.
POST /api/profiles/create
Creates a profile. fingerprint: omitted → generated; full → used as provided with normalization; partial → base is generated and merged with incoming-wins.
Canonical fingerprint fields: userAgent, platform, platformVersion, vendor, productSub, CPUcores, deviceMemory, language, languages, AcceptLanguage, Brand, BrandVersion, BrandFullVersion, WebGLRenderer, WebGLVendor, Arch, Bitness, wow64, plus NoiseCanvas/Audio/Rects/GL.
Always ignored:
| Field | Why |
|---|---|
userAgent | Must match the Afina-Chromium build |
BrandFullVersion | Build constant linked to UA |
deviceMemory | For ua<147, the browser reports max 8; 16/32 is allowed with ua>=147 |
Fingerprint aliases:
| Submitted | Becomes |
|---|---|
uaPlatformVersion, platform_version | platformVersion |
uaArchitecture, architecture | Arch |
uaBitness | Bitness |
webGLVendor, webglVendor, unmasked_vendor | WebGLVendor |
webGLRenderer, webglRenderer, unmasked_renderer | WebGLRenderer |
hardwareConcurrency, hardware_concurrency, cpuCores | CPUcores |
device_memory | deviceMemory |
user_agent | userAgent |
product_sub | productSub |
accept_language, acceptLanguage | AcceptLanguage |
brandVersion / brandFullVersion / uaFullVersion | BrandVersion / BrandFullVersion |
ua_full_version | BrandFullVersion |
uais taken from local{data_dir}/browser/UA*; the client value is ignored.osfor generation: payload →fingerprint.platform→macos; allowed:macos,windows10,windows11.osin DB:Windows10/Windows11/x86/arm64; Intel WebGL givesos="x86",macChip="",platform="x86",Arch="".macChip: WebGLRenderer (Apple M2→m2) →payload.chip/macChip→fp.chip→"".osArch:arm64/x86for macOS; ignored on Windows.
Other parameters (name, proxy*, tags*, settings, noise flags, screenSize, etc.) are listed below.
Body (everything is optional except scenario-specific requirements):
| Field | Type | Description |
|---|---|---|
name | string | Name |
accountId | string | Force UUID |
os | macos/windows10 | FP generation; default macos |
osArch | arm64/x86 | macOS arch; default arm64 |
chip | string | m1/m2/m3/m4/intel |
ua | string | Ignored; local UA is used |
browserType | string | afina/mimic/octo/vision/ads/dolphin; default afina |
note | string | Note |
proxyId | int | Saved proxy; mutually exclusive with proxyData |
proxyType | saved/set/without_proxy | Proxy type |
proxyData | object | Inline proxy. Required: host, port, type.Optional: username, password, changeIpUrl, remark, country, countryCode, timezone, visible_ip, isActive, isUDPSupported |
tagIds / tagNames | array | Tags by id/name; new tags are created |
accountGroupIds / accountGroupNames | array | Groups by id/name |
languages | array | Language override |
language / timezone | string | If *_from_ip=false |
timezone_from_ip / language_from_ip / languages_from_ip | bool | Auto from IP; default true |
languageInterfaceType | from_language/real_value | Default real_value |
screenSize | string | 1920x1080; avail auto |
useScreenDefault | bool | Default screen |
availWidth/availHeight/colorDepth/pixelDepth | int | Screen |
blockedPorts | [int] | `--afina-fp |
blockOnProxyCountryChange | bool/null | null=global, true=block, false=allow |
localCacheMode | default/no_cache | no_cache → --disk-cache-size=0 |
startupUrls | [string] | Open on first launch |
extraArgs | string | Chromium CLI args |
settings | object | Per-account KV ${key} |
isNoiseCanvasEnabled / isNoiseAudioEnabled / isNoiseRectsEnabled / isNoiseGLEnabled | bool | FP noise |
fingerprint | object | Full/partial FP |
teamUuid | string | License team |
Success: { "status": "success", "id": 50, "accountId": "<uuid>", "account": {...} }.
POST /api/profiles/update
PATCH by { id } or { accountId }. Accepts create fields, isDeleted, skipServerSync, tags tagIds + selection(replace|append|delete|clear), groups accountGroupIds + selectionGroups(replaceGroup|appendGroup|deleteGroup|clearGroup).
POST /api/profiles/delete
Soft-delete. Body { id } or { accountId } (UUID-string). Success: { "message": "Account successfully deleted" }.
POST /api/profiles/hard-delete
Permanently deletes account/profile/junction, proxy_usage, and profile files; syncs Afina server DELETE /profiles/:uuid; closes the browser before deleting. Body { id } / { ids:[...] } or { accountId } / { accountIds:[...] }. Success: { "status":"success","deleted":3 }.
Browser control#
POST /api/profiles/start
Starts a browser. Body { "profileId": "<UUID>" }. Response contains wsEndpoint and data.port; if already running: alreadyRunning:true. wsEndpoint works with Puppeteer/Playwright/CDP.
POST /api/profiles/stop
Closes a running browser via CDP Browser.close, then graceful kill after timeout. Body { "profileId": "<UUID>" }. Returns 404 if the profile is not running.
One-time profiles#
POST /api/profiles/one-time
Creates a disposable profile, starts it immediately, and hard-deletes it after stop. Body = /api/profiles/create fields; name default one-time-<ts>.
- Creation uses
/api/profiles/createwith partial-fingerprint merge and internal__oneTime_internal__. - Returns
id,accountId,wsEndpoint,port,isOneTime:true. - Stop:
POST /api/profiles/stop,browser.close(), or crash; after that the profile disappears from/api/profiles/listand Trash. - If
start_browserfails, the created one-time profile is deleted synchronously. - RPM/RPH: one call = create + start + stop + delete; do not call more often than about 1/sec.
isOneTime cannot be set through /api/profiles/create or /api/profiles/update; hard-delete in the exit handler runs only after SQL check account.isOneTime=1.
Browser introspection - eval / screenshot#
POST /api/profiles/eval
Runs JS in the current visible tab of a running profile. Body { profileId, code }; code is wrapped in an IIFE, promises are awaited, result uses returnByValue. 404 if the browser is not running. Example response: { "value": "https://example.com" }.
POST /api/profiles/screenshot
Screenshot of the current visible tab. Body { profileId, format:"png" }; response { mimeType:"image/png", data:"iVBORw0..." }.
Cookies - cookie import/export#
POST /api/profiles/cookies/set
Puts cookies into the import queue at {data_dir}/cookies/{uuid}/cookies_{ts}.json; on the next browser start, the server injects them via CDP. A pending cookies_*.json is overwritten.
Body:
| Field | Type | Description |
|---|---|---|
accountId | int/UUID | account.id or UUID; alias id |
cookies | array | Cookie objects: domain/name/value/path/expirationDate/secure/httpOnly/sameSite/... |
Success: { "status":"success", "data": { "count": 1 } }.
The account must not be running: cookies are applied on the next /api/profiles/start. For hot injection, use CDP Network.setCookies through /api/profiles/eval or wsEndpoint.
POST /api/profiles/cookies/export
Exports decrypted cookies from the open profile folder, then .zip, then .afbk. Chrome extensions format: domain/name/value/path/expirationDate/hostOnly/httpOnly/sameSite/secure/session/storeId.
Body:
| Field | Type | Description |
|---|---|---|
id | int | One account.id |
ids | [int] | Bulk ids |
accountId | UUID | One UUID |
accountIds | [UUID] | Bulk UUID |
path | string | File or folder |
- Single without
path→ cookies indata.cookies. - Single +
pathending in.json→ one file. - Bulk or
pathwithout.json→ folder; filescookie_{accountName}_{ts}.json. - Response:
{ status:"success", data:{ id?, accountName?, count?, cookies?, path?, exported?, errors? } }.
Unlocked session required: cookie key is read from vault. Otherwise 500 Cookie key not available - decrypt keys first.
Account vars#
Variables are available in RPA as ${key}:
plain=account.settings, unencrypted JSON.encrypted=account_data_blob, sealed-box; requires master-password, executor decrypts before launch.
GET /api/accounts/vars?accountId=N (or ?accountUuid=UUID)
Returns { accountId, plain:{...}, encrypted:{...} }.
POST /api/accounts/vars/set
One key. Body { accountId | accountUuid, key, value, encrypted?:bool }; encrypted=false writes to account.settings and registers the key in catalog; encrypted=true decrypt → merge → encrypt. value can be string/number/bool/object/array.
POST /api/accounts/vars/delete
Deletes one key. Body { accountId | accountUuid, key, encrypted?:bool }; response contains removed.
Proxies#
POST /api/proxies/check
Checks account proxies with the same checker (ipapicom/ipinfoio), plus UDP for socks5. On success updates proxy (visible_ip, country, timezone, UDP, active) and per-account proxy_usage; respects country-block.
Body (AND filters):
| Field | Type | Description |
|---|---|---|
accountIds | [int] | account.id |
groupId | int | Accounts in group |
tagId | int | Accounts with tag |
| (nothing) | - | All non-deleted with proxy |
Response: { message, checked, results:[{ accountId, accountName, accountUuid, proxyId, host, port, type, result }] }; result.status = success / error / no_proxy.
POST /api/proxies/check-all
Checks all proxy records; updates proxy and proxy_usage for all accounts using each proxy. Response: { message, checked, results:[...] }.
POST /api/proxies/add
Adds a proxy after warm-up validation; on fail, it is not saved. Body { host, port, type?, username?, password?, remark?, changeIpUrl? }, type default http. Success response { added:true, proxyId, result } or fail { added:false, result:{status:"error",message} }.
Databases#
Connections for the RPA database block; table connections.
GET /api/databases/list
Database list: { message, count, databases:[{ id, name, type, filePath?|host?|port?|user?|database?|ssl? }] }.
GET /api/databases/get?id=N
One database: { message, database:{...} }.
POST /api/databases/create
Body:
| Field | Type | Description |
|---|---|---|
name | string | Name |
type | sqlite/postgres/mysql/mssql/mongodb/redis | Default sqlite |
filePath | string | .db for sqlite |
host/port/user/password/database | - | Network DBs |
ssl | bool | TLS |
uri | string | Connection URI override |
folderId | int/null | UI folder |
isCreateFile | bool | For sqlite, create .db in <dataDir>/databases/ |
Success: { "status":"success", "data": { "id":3, "name":"scratch", "type":"sqlite", "filePath":"..." } }.
POST /api/databases/update
PATCH by id; create fields + isFavorite, isDeleted, tagIds + selection.
POST /api/databases/delete
Soft-delete. Body { id } or { ids:[...] }.
POST /api/databases/hard-delete
Deletes connections, connections_tags_tag, and the disk file if filePath exists. Body { id } or { ids:[...] }.
Global vars#
Name/value from Settings → Environment variables, available in RPA as ${name}; table settings.
GET /api/global-vars/list
Response: { message, count, vars:[{ id, name, value, enable, isRestricted, owner }] }.
POST /api/global-vars/create
Body { name, value }; both must be unique. Duplicates: setting.error.duplicate_name / setting.error.duplicate_value.
POST /api/global-vars/update
Body { id, name?, value? }; same uniqueness rules.
POST /api/global-vars/delete
Body { id } / { ids:[...] } / { settingIds:[...] }.
Key catalog#
key_entity stores key names from account.settings and account_data_blob; values are in /api/accounts/vars.
GET /api/keys/list
Response: { message, count, keys:[{ id, key, createdAt }] }.
POST /api/keys/delete
Deletes only the name catalog, not account values. Body { ids:[...] } or { globalKeyIds:[...] }.
Scripts - RPA scripts#
GET /api/scripts/list
All non-deleted scripts with settings tree and form. Response: { message, count, scripts:[{ id, name, hash, form, settings, isFavorite }] }. form = input/select/checkbox fields passed into tasks through additionalData.
GET /api/scripts/get?id=N
Full structure of one script: { message, script:{ id, name, settings, form } }.
POST /api/scripts/create
Creates a script. Body { name, settings, form?, tab?, browser?, headlessMode?, noBrowser?, extraArgs? }.
Critical settings format:
elements[]:{ id, start, type, left, top, label:"", hash:"", note:"", settings:{} };left/topare on the element, notposition.- Exactly one
start:true;settings.startElement= its id. connections[]:{ sourceId, targetId, sourcePosition:"bottom|right|left", targetPosition:"top|left|right" };targetPositionis required.visualGroups:[]optional.
Success: { status:"success", data:{ id, hash, name, settings } }.
POST /api/scripts/update
PATCH by id: name, settings (full replacement), form, tab, browser, headlessMode, noBrowser, isFavorite, folderId, tagIds, extraArgs.
POST /api/scripts/run
Direct script run on a profile without task-group/tasks. Body { profileId:"<UUID>", scriptId:<numeric|hash>, closeBrowserAfter?:bool }; response { status:"success", uuid:"<task-uuid>" }.
GET /api/scripts/run-logs?uuid=UUID (or ?taskUuid=UUID)
Direct-run log (text/plain), equivalent to /api/tasks/logs for /api/scripts/run.
POST /api/scripts/stop
Stops a running script by task uuid. Body { uuid:"<task-uuid>" } or { taskUuid }; executor stops at the next await.
Modules - RPA modules (executeModule)#
A module is JS code for the executeModule block: module row + folder (index.js, utils_<id>.js, package.json, settings.json).
Workflow: POST /api/modules/create → edit files in moduleDirAbs (index.js without the IPC block process.on('message',...) and process.send({status:'ready'}), settings.json, package.json) → POST /api/modules/resign. Without a fresh Ed25519 signature, executor returns modules.error.signature_invalid.
GET /api/modules/list
Response: { message, count, modules:[{ id, hash, name, sig, moduleDir, moduleDirAbs }] }.
GET /api/modules/get?id=N (or ?hash=UUID)
Returns row + moduleDirAbs + top-level files.
POST /api/modules/create
Body:
| Field | Type | Description |
|---|---|---|
name | string | required |
hash | string | Force UUID |
code | string | UI mirror of index.js |
settings | object | { type:"module", fields:[{name,label,type:"text"|"number"|"checkbox"|"select",default,options?,groupId?}] } |
folderId | int/null | Folder |
tagIds | [int] | Tags |
allowedFunctions | [string] | Node API whitelist |
useCustomFolder | bool | Use customFolder |
customFolder | string | Existing folder |
Success: { status:"success", data:{ id, hash, moduleDir, moduleDirAbs } }.
After create, the server runs npm install and signs the module in the background. Always call /api/modules/resign after editing files.
POST /api/modules/update
PATCH DB row, not files: id, name, code, moduleDir, warnReason, settings, allowedFunctions, hashes, warningFindings, flags isFavorite/isDirty/isMigrated/isWarn/requiresReview/isDeleted, folderId, tagIds + selection.
POST /api/modules/resign
Recalculate folder signature. Body { id }; success { status:"success", sig:"base64-ed25519-signature..." }.
POST /api/modules/delete
Soft-delete. Body { id } or { ids:[...] }; files remain.
POST /api/modules/hard-delete
Deletes module row, module_tags_tag, and folder. Body { id } or { ids:[...] }.
Task Groups#
Task group = schedule/retry/timeout/concurrency container. active=1 starts scheduler.
| Field | Type | Description |
|---|---|---|
schedule | bool | timeFrom/timeTo window |
timeFrom / timeTo | string HH:MM | Requires schedule=true |
scheduleTime | bool | startHour/endHour window |
startHour / endHour | int 0-23 | Requires scheduleTime=true |
isRepeatable | bool | Repeat group |
repeatCount | int | Repeat count |
timeout | int sec | 0 = none |
activeSession | int | Concurrency; 0 = unlimited |
waitForOtherTaskCompletion | bool | Wait for other groups |
folderId | int/null | UI folder |
GET /api/task-groups/list
Response: { message, count, groups:[{ id, tag, active, schedule, timeFrom, timeTo, isRepeatable, timeout, activeSession }] }.
GET /api/task-groups/get?id=N
Group + tasks: { message, group:{...}, tasks:[{ id, uuid, scriptId, accountId, status, executeAt, additionalData }], tasksCount }.
GET /api/task-groups/tasks?groupId=N
Only group tasks. Statuses: waiting, working, finished, error, stop, stopWithError.
POST /api/task-groups/create
Creates an empty group. Body { tag?:string, name?:string, active?:bool, ...scheduleFields }. Recommended: active:false, then create tasks and call start.
POST /api/task-groups/update
PATCH by id: schedule fields, tag, active, isFavorite, isDeleted, isRescheduled.
POST /api/task-groups/start
Body { id } or { groupId }. Sets active=1; scheduler picks up waiting tasks. Idempotent, does not reset completed tasks.
POST /api/task-groups/restart
Body { id }. Sets active=1 and moves finished/error/stop/stopWithError back to waiting with executeAt=now().
POST /api/task-groups/stop
Body { id }. Sets active=0, moves working to stopWithError; does not close browsers.
POST /api/task-groups/delete or DELETE /api/task-groups/delete?id=N
Soft-deletes the group and its tasks (isDeleted=1, UI deleteGroup). POST body { id }.
POST /api/task-groups/hard-delete
Deletes group tasks and the group row. Body { id } or { ids:[...] }; response contains deletedGroups, deletedTasks, ids.
Tasks#
Task = scriptId × accountId with executeAt, status, additionalData, sort.
POST /api/tasks/create
Creates one/multiple tasks in a group in one transaction. Body { groupId:int, tasks:[...] }; response { message, created, requested, errors }.
Task field:
| Field | Type | Description |
|---|---|---|
accountId | int | required, account.id |
scriptId | int/string | required |
additionalData | object | script form fields; otherwise defaultValue |
executeAt | ISO string | default now |
tag | string | Label |
sort | int | Order |
additionalData is critical when a script has form fields.
POST /api/tasks/update
PATCH by id or taskId: status, tag, description, executeAt, sort, additionalData.
GET /api/tasks/list
Flat task list; AND filters.
Query params:
| Parameter | Type | Description |
|---|---|---|
status | string | CSV: working,waiting,finished,error,stop,stopWithError |
groupId | int | Group |
accountId | int | account.id |
scriptId | int/string | Script |
limit | int | Default 500, max 5000 |
Response: { message, count, tasks:[{ id, uuid, status, groupId, accountId, scriptId, executeAt }] }.
GET /api/tasks/active
All working tasks with account:{id,name,accountId} and script:{id,name}.
POST /api/tasks/delete
Permanently deletes tasks. Body { id } or { ids:[...] }.
POST /api/tasks/stop
Stops working/waiting: status → stop + abort executor by uuid; closeBrowser:true additionally closes the account browser. Body { id } / { ids:[...] } / [{ id, uuid? }, ...], plus closeBrowser?:bool.
Logs#
GET /api/tasks/logs?taskUuid=UUID (or ?taskId=UUID)
Text log by task.uuid (not numeric task.id), Content-Type: text/plain; charset=utf-8; can be read during execution. 404 if the file has not been created yet or was deleted.
GET /api/scripts/run-logs?uuid=UUID
Same for direct runs through /api/scripts/run.
Emails - IMAP credentials#
GET /api/emails/list
List of IMAP credentials; passwords are not returned. Response: { message, count, emails:[{ id, email, imapServer, port, isActive, mailboxes }] }.
POST /api/emails/toggle
Enables/disables IMAP monitoring: updates isActive and synchronously opens/closes the connection. Body { email:"user@gmail.com", isActive:true }.
Error format#
Error: HTTP 4xx/5xx + { "error": "Описание ошибки" }.
- 400 - required fields / JSON.
- 401 - invalid/missing
x-api-key(connection closes). - 404 - resource not found.
- 500 - DB/internal error.
- Some business errors use HTTP 200 with
{ status:"error", code:"<i18n-key>", message:"..." }, for example duplicate global vars.
Full example: run a scheduled script on accounts#
Scenario: MintNFT (scriptId=12) with walletPassword, mintCount on accounts 42/43/44, window 08:00-20:00, repeat 2, concurrency 5, start 2026-05-11T09:00:00.000Z.
- Find IDs:
GET /api/scripts/list;GET /api/profiles/list?groupId=5. - Check/warm up proxy:
POST /api/proxies/check, body{ accountIds:[42,43,44] }. - Create group:
POST /api/task-groups/create, body{ tag:"MintNFT run", active:false, schedule:true, timeFrom:"08:00", timeTo:"20:00", isRepeatable:true, repeatCount:2, activeSession:5 }; savegroup.id. - Create tasks:
POST /api/tasks/create, body{ groupId:7, tasks:[{accountId:42,scriptId:12,additionalData:{walletPassword:"pwd1",mintCount:"1"},executeAt:"2026-05-11T09:00:00.000Z"}, ...] }. - Activate:
POST /api/task-groups/start, body{ id:7 }. - Monitor:
GET /api/task-groups/get?id=7, thenGET /api/tasks/logs?taskUuid=<uuid>. - Control:
GET /api/tasks/active;POST /api/tasks/stopwith{ ids:[100], closeBrowser:true };POST /api/task-groups/restart;POST /api/task-groups/hard-delete.
Direct browser connection via CDP#
After POST /api/profiles/start, use wsEndpoint.
Python pyppeteer: browser = await connect(browserWSEndpoint=ws_endpoint). Without a CDP client, use POST /api/profiles/eval and POST /api/profiles/screenshot.
OAuth callback#
GET / or GET /oauth/callback?code=AUTH_CODE does not require api-key; it is used for Google Drive OAuth flow, passes code to the internal channel, and closes the window.
Source of truth#
Implementation: src-tauri/src/browser/server.rs. If endpoint behavior differs from the docs, source code is source of truth.