Обновлено:

Afina HTTP API#

Локальный HTTP-сервер для внешних скриптов, автоматизации и AI-агентов через MCP. Стартует вместе с десктоп-приложением Afina.

  • Base URL: http://127.0.0.1:50778
  • Default port: 50778; если занят, сервер пробует следующие порты.
  • JSON: все ответы application/json, кроме /api/tasks/logs и /api/scripts/run-logs (text/plain).
  • Source of truth: src-tauri/src/browser/server.rs.

Аутентификация#

Все /api/* требуют x-api-key из Настройки → Основные → API ключ. Если ключ не задан или неверный: 401 Unauthorized / тихое закрытие соединения.

bash

Без ключа доступны только GET /api/health, GET /, GET /oauth/callback?code=.... CORS разрешён только для localhost-origin (http://localhost, http://127.0.0.1, tauri://); методы GET, POST, DELETE, OPTIONS; headers Content-Type, X-API-Key.

Конвенции#

  • id = numeric PK в SQLite; accountId = UUID профиля. Большинство write-endpoints принимают оба.
  • Boolean: true/false или 0/1; datetime: ISO-8601, default datetime('now').
  • */delete = soft delete (isDeleted=1); */hard-delete = физическое удаление из БД/диска.
  • */update = PATCH только переданных полей.
  • Write-endpoints эмитят reload-event для UI refresh.

Health#

GET /api/health
Heartbeat без API key. Ответ: { "status": "ok", "running": 2 }, где running = число запущенных браузеров.

Profiles - аккаунты (CRUD)#

GET /api/profiles/list
Список аккаунтов; к каждому добавляются isRunning, tags:[{id,name,color}], groups:[{id,name}].

Query params (опциональны, AND):

ПараметрТипОписание
isRunningtrue/falseЗапущенные/остановленные
groupIdintФильтр по группе
tagIdintФильтр по тегу

Ответ: { message, count, accounts:[{ id, accountId, name, ua, proxyId, os, macChip, isRunning, tags, groups }] }.

GET /api/profiles/get / POST /api/profiles/get
Один аккаунт по UUID: query/body { accountId }. Ответ: { message, profile:{...} }.

POST /api/profiles/create
Создаёт профиль. fingerprint: не передан → генерируется; полный → берётся как есть с нормализацией; частичный → генерируется база и мерджится incoming-wins.

Канон-поля fingerprint: userAgent, platform, platformVersion, vendor, productSub, CPUcores, deviceMemory, language, languages, AcceptLanguage, Brand, BrandVersion, BrandFullVersion, WebGLRenderer, WebGLVendor, Arch, Bitness, wow64, плюс NoiseCanvas/Audio/Rects/GL.

Игнорируются всегда:

ПолеПочему
userAgentДолжен совпадать с Afina-Chromium build
BrandFullVersionКонстанта build, связана с UA
deviceMemoryПри ua<147 браузер отдаёт максимум 8; 16/32 допустимы с ua>=147

Алиасы fingerprint:

ПрислалСтало
uaPlatformVersion, platform_versionplatformVersion
uaArchitecture, architectureArch
uaBitnessBitness
webGLVendor, webglVendor, unmasked_vendorWebGLVendor
webGLRenderer, webglRenderer, unmasked_rendererWebGLRenderer
hardwareConcurrency, hardware_concurrency, cpuCoresCPUcores
device_memorydeviceMemory
user_agentuserAgent
product_subproductSub
accept_language, acceptLanguageAcceptLanguage
brandVersion / brandFullVersion / uaFullVersionBrandVersion / BrandFullVersion
ua_full_versionBrandFullVersion
  • ua берётся из локального {data_dir}/browser/UA*; клиентское значение игнорируется.
  • os для генерации: payload → fingerprint.platformmacos; допустимо macos, windows10, windows11.
  • os в БД: Windows10 / Windows11 / x86 / arm64; Intel WebGL даёт os="x86", macChip="", platform="x86", Arch="".
  • macChip: WebGLRenderer (Apple M2m2) → payload.chip/macChipfp.chip"".
  • osArch: arm64/x86 для macOS; Windows игнорирует.

Остальные параметры (name, proxy*, tags*, settings, шумы, screenSize и т.д.) перечислены ниже.

Body (всё опционально, кроме требований конкретного сценария):

ПолеТипОписание
namestringИмя
accountIdstringФорсировать UUID
osmacos/windows10Генерация FP; default macos
osArcharm64/x86Арх macOS; default arm64
chipstringm1/m2/m3/m4/intel
uastringИгнорируется; берётся локальный UA
browserTypestringafina/mimic/octo/vision/ads/dolphin; default afina
notestringЗаметка
proxyIdintSaved proxy; взаимоискл. с proxyData
proxyTypesaved/set/without_proxyТип прокси
proxyDataobjectInline proxy.
Required: host, port, type.
Optional: username, password, changeIpUrl, remark, country, countryCode, timezone, visible_ip, isActive, isUDPSupported
tagIds / tagNamesarrayТеги по id/name; новые создаются
accountGroupIds / accountGroupNamesarrayГруппы по id/name
languagesarrayOverride языков
language / timezonestringЕсли *_from_ip=false
timezone_from_ip / language_from_ip / languages_from_ipboolАвто из IP; default true
languageInterfaceTypefrom_language/real_valueDefault real_value
screenSizestring1920x1080; avail авто
useScreenDefaultboolДефолтный экран
availWidth/availHeight/colorDepth/pixelDepthintЭкран
blockedPorts[int]`--afina-fp
blockOnProxyCountryChangebool/nullnull=global, true=block, false=allow
localCacheModedefault/no_cacheno_cache--disk-cache-size=0
startupUrls[string]Открыть при первом запуске
extraArgsstringCLI args Chromium
settingsobjectPer-account KV ${key}
isNoiseCanvasEnabled / isNoiseAudioEnabled / isNoiseRectsEnabled / isNoiseGLEnabledboolFP noise
fingerprintobjectПолный/частичный FP
teamUuidstringКоманда лицензии

Успех: { "status": "success", "id": 50, "accountId": "<uuid>", "account": {...} }.

POST /api/profiles/update
PATCH по { id } или { accountId }. Принимает поля create, isDeleted, skipServerSync, теги tagIds + selection(replace|append|delete|clear), группы accountGroupIds + selectionGroups(replaceGroup|appendGroup|deleteGroup|clearGroup).

POST /api/profiles/delete
Soft-delete. Body { id } или { accountId } (UUID-string). Успех: { "message": "Account successfully deleted" }.

POST /api/profiles/hard-delete
Безвозвратно удаляет account/profile/junction, proxy_usage, файлы профиля; синхронизирует Afina server DELETE /profiles/:uuid; перед удалением закрывает браузер. Body { id } / { ids:[...] } или { accountId } / { accountIds:[...] }. Успех: { "status":"success","deleted":3 }.

Browser control - управление браузером#

POST /api/profiles/start
Запускает браузер. Body { "profileId": "<UUID>" }. Ответ содержит wsEndpoint и data.port; если уже запущен: alreadyRunning:true. wsEndpoint пригоден для Puppeteer/Playwright/CDP.

POST /api/profiles/stop
Закрывает запущенный браузер через CDP Browser.close, затем graceful kill по таймауту. Body { "profileId": "<UUID>" }. 404, если профиль не запущен.

One-time profiles - одноразовые профили#

POST /api/profiles/one-time
Создаёт disposable profile, сразу запускает и hard-delete после остановки. Body = поля /api/profiles/create; name default one-time-<ts>.

  • Создание идёт через /api/profiles/create с partial-fingerprint merge и внутренним __oneTime_internal__.
  • Возвращает id, accountId, wsEndpoint, port, isOneTime:true.
  • Остановка: POST /api/profiles/stop, browser.close() или crash; после этого профиль исчезает из /api/profiles/list и Trash.
  • Если start_browser упал, созданный one-time профиль удаляется синхронно.
  • RPM/RPH: один вызов = create + start + stop + delete; не чаще примерно 1/сек.

isOneTime нельзя выставить через /api/profiles/create или /api/profiles/update; hard-delete в exit-handler срабатывает только после SQL-проверки account.isOneTime=1.

Browser introspection - eval / screenshot#

POST /api/profiles/eval
Выполняет JS в текущей видимой вкладке запущенного профиля. Body { profileId, code }; код оборачивается в IIFE, promises await, результат returnByValue. 404, если браузер не запущен. Пример ответа: { "value": "https://example.com" }.

POST /api/profiles/screenshot
Скриншот текущей видимой вкладки. Body { profileId, format:"png" }; ответ { mimeType:"image/png", data:"iVBORw0..." }.

Cookies - импорт/экспорт куков#

POST /api/profiles/cookies/set
Кладёт cookies в очередь импорта {data_dir}/cookies/{uuid}/cookies_{ts}.json; при следующем старте браузера сервер заливает их через CDP. Непринятый cookies_*.json перезаписывается.

Body:

ПолеТипОписание
accountIdint/UUIDaccount.id или UUID; алиас id
cookiesarrayCookie objects: domain/name/value/path/expirationDate/secure/httpOnly/sameSite/...

Успех: { "status":"success", "data": { "count": 1 } }.

Аккаунт не должен быть запущен: куки применятся на следующем /api/profiles/start. Для горячего ввода используйте CDP Network.setCookies через /api/profiles/eval или wsEndpoint.

POST /api/profiles/cookies/export
Экспорт расшифрованных cookies из открытой папки профиля, затем .zip, затем .afbk. Формат Chrome extensions: domain/name/value/path/expirationDate/hostOnly/httpOnly/sameSite/secure/session/storeId.

Body:

ПолеТипОписание
idintОдин account.id
ids[int]Bulk ids
accountIdUUIDОдин UUID
accountIds[UUID]Bulk UUID
pathstringФайл или папка
  • Single без path → cookies в data.cookies.
  • Single + path на .json → один файл.
  • Bulk или path без .json → папка; файлы cookie_{accountName}_{ts}.json.
  • Ответ: { status:"success", data:{ id?, accountName?, count?, cookies?, path?, exported?, errors? } }.

Нужна разлоченная сессия: cookie key берётся из vault. Иначе 500 Cookie key not available - decrypt keys first.

Account vars - переменные аккаунта#

Переменные доступны в RPA как ${key}:

  • plain = account.settings, JSON без шифрования.
  • encrypted = account_data_blob, sealed-box; требует master-password, executor расшифровывает перед запуском.

GET /api/accounts/vars?accountId=N (или ?accountUuid=UUID)
Возвращает { accountId, plain:{...}, encrypted:{...} }.

POST /api/accounts/vars/set
Один ключ. Body { accountId | accountUuid, key, value, encrypted?:bool }; encrypted=false пишет в account.settings и регистрирует ключ в catalog; encrypted=true decrypt → merge → encrypt. value может быть string/number/bool/object/array.

POST /api/accounts/vars/delete
Удаляет один ключ. Body { accountId | accountUuid, key, encrypted?:bool }; ответ содержит removed.

Proxies - прокси#

POST /api/proxies/check
Проверяет прокси аккаунтов тем же checker (ipapicom/ipinfoio), socks5 дополнительно UDP. При успехе обновляет proxy (visible_ip, country, timezone, UDP, active) и proxy_usage per-account; учитывает country-block.

Body (фильтры AND):

ПолеТипОписание
accountIds[int]account.id
groupIdintАккаунты группы
tagIdintАккаунты тега
(ничего)-Все non-deleted с proxy

Ответ: { message, checked, results:[{ accountId, accountName, accountUuid, proxyId, host, port, type, result }] }; result.status = success / error / no_proxy.

POST /api/proxies/check-all
Проверяет все записи proxy; обновляет proxy и proxy_usage всех аккаунтов, использующих прокси. Ответ: { message, checked, results:[...] }.

POST /api/proxies/add
Добавляет proxy после прогрев-проверки; при fail не сохраняет. Body { host, port, type?, username?, password?, remark?, changeIpUrl? }, type default http. Ответ success { added:true, proxyId, result } или fail { added:false, result:{status:"error",message} }.

Databases - подключения к БД#

Подключения для RPA-блока database; таблица connections.

GET /api/databases/list
Список БД: { message, count, databases:[{ id, name, type, filePath?|host?|port?|user?|database?|ssl? }] }.

GET /api/databases/get?id=N
Одна БД: { message, database:{...} }.

POST /api/databases/create

Body:

ПолеТипОписание
namestringИмя
typesqlite/postgres/mysql/mssql/mongodb/redisDefault sqlite
filePathstring.db для sqlite
host/port/user/password/database-Сетевые БД
sslboolTLS
uristringConnection URI override
folderIdint/nullUI-папка
isCreateFileboolДля sqlite создать .db в <dataDir>/databases/

Успех: { "status":"success", "data": { "id":3, "name":"scratch", "type":"sqlite", "filePath":"..." } }.

POST /api/databases/update
PATCH по id; поля create + isFavorite, isDeleted, tagIds + selection.

POST /api/databases/delete
Soft-delete. Body { id } или { ids:[...] }.

POST /api/databases/hard-delete
Удаляет connections, connections_tags_tag и файл с диска, если есть filePath. Body { id } или { ids:[...] }.

Global vars - глобальные переменные#

Name/value из Настройки → Переменные окружения, доступны в RPA как ${name}; таблица settings.

GET /api/global-vars/list
Ответ: { message, count, vars:[{ id, name, value, enable, isRestricted, owner }] }.

POST /api/global-vars/create
Body { name, value }; оба должны быть уникальны. Дубликаты: setting.error.duplicate_name / setting.error.duplicate_value.

POST /api/global-vars/update
Body { id, name?, value? }; те же правила уникальности.

POST /api/global-vars/delete
Body { id } / { ids:[...] } / { settingIds:[...] }.

Key catalog - каталог ключей#

key_entity хранит имена ключей из account.settings и account_data_blob; значения лежат в /api/accounts/vars.

GET /api/keys/list
Ответ: { message, count, keys:[{ id, key, createdAt }] }.

POST /api/keys/delete
Удаляет только реестр имён, не значения аккаунтов. Body { ids:[...] } или { globalKeyIds:[...] }.

Scripts - RPA-скрипты#

GET /api/scripts/list
Все non-deleted скрипты с деревом settings и form. Ответ: { message, count, scripts:[{ id, name, hash, form, settings, isFavorite }] }. form = поля input/select/checkbox, передаются в задачах через additionalData.

GET /api/scripts/get?id=N
Полная структура одного скрипта: { message, script:{ id, name, settings, form } }.

POST /api/scripts/create
Создаёт скрипт. Body { name, settings, form?, tab?, browser?, headlessMode?, noBrowser?, extraArgs? }.

Критичный формат settings:

  • elements[]: { id, start, type, left, top, label:"", hash:"", note:"", settings:{} }; left/top на элементе, не position.
  • Ровно один start:true; settings.startElement = его id.
  • connections[]: { sourceId, targetId, sourcePosition:"bottom|right|left", targetPosition:"top|left|right" }; targetPosition обязателен.
  • visualGroups: [] опционально.

Успех: { status:"success", data:{ id, hash, name, settings } }.

POST /api/scripts/update
PATCH по id: name, settings (полная замена), form, tab, browser, headlessMode, noBrowser, isFavorite, folderId, tagIds, extraArgs.

POST /api/scripts/run
Прямой запуск скрипта на профиле без task-group/tasks. Body { profileId:"<UUID>", scriptId:<numeric|hash>, closeBrowserAfter?:bool }; ответ { status:"success", uuid:"<task-uuid>" }.

GET /api/scripts/run-logs?uuid=UUID (или ?taskUuid=UUID)
Лог прямого запуска (text/plain), аналог /api/tasks/logs для /api/scripts/run.

POST /api/scripts/stop
Остановить running script по task uuid. Body { uuid:"<task-uuid>" } или { taskUuid }; executor прерывается на ближайшем await.

Modules - RPA-модули (executeModule)#

Модуль = JS-код для блока executeModule: ряд module + папка (index.js, utils_<id>.js, package.json, settings.json).

Workflow: POST /api/modules/create → редактировать файлы в moduleDirAbs (index.js без IPC-блока process.on('message',...) и process.send({status:'ready'}), settings.json, package.json) → POST /api/modules/resign. Без свежей Ed25519-подписи executor вернёт modules.error.signature_invalid.

GET /api/modules/list
Ответ: { message, count, modules:[{ id, hash, name, sig, moduleDir, moduleDirAbs }] }.

GET /api/modules/get?id=N (или ?hash=UUID)
Возвращает ряд + moduleDirAbs + топ-уровень files.

POST /api/modules/create

Body:

ПолеТипОписание
namestringrequired
hashstringФорсировать UUID
codestringUI-мирор index.js
settingsobject{ type:"module", fields:[{name,label,type:"text"|"number"|"checkbox"|"select",default,options?,groupId?}] }
folderIdint/nullПапка
tagIds[int]Теги
allowedFunctions[string]Node API whitelist
useCustomFolderboolИспользовать customFolder
customFolderstringСуществующая папка

Успех: { status:"success", data:{ id, hash, moduleDir, moduleDirAbs } }.

После create сервер в фоне запускает npm install и подписывает модуль. После правки файлов всегда вызывайте /api/modules/resign.

POST /api/modules/update
PATCH ряда БД, не файлов: id, name, code, moduleDir, warnReason, settings, allowedFunctions, hashes, warningFindings, flags isFavorite/isDirty/isMigrated/isWarn/requiresReview/isDeleted, folderId, tagIds + selection.

POST /api/modules/resign
Пересчитать подпись папки. Body { id }; успех { status:"success", sig:"base64-ed25519-signature..." }.

POST /api/modules/delete
Soft-delete. Body { id } или { ids:[...] }; файлы остаются.

POST /api/modules/hard-delete
Удаляет ряд module, module_tags_tag и папку. Body { id } или { ids:[...] }.

Task Groups - группы задач#

Группа задач = контейнер расписания/повторов/таймаута/параллелизма. active=1 запускает scheduler.

ПолеТипОписание
scheduleboolОкно timeFrom/timeTo
timeFrom / timeTostring HH:MMТребует schedule=true
scheduleTimeboolОкно startHour/endHour
startHour / endHourint 0-23Требует scheduleTime=true
isRepeatableboolПовторять группу
repeatCountintКол-во повторов
timeoutint sec0 = нет
activeSessionintПараллелизм; 0 = unlimited
waitForOtherTaskCompletionboolЖдать другие группы
folderIdint/nullUI-папка

GET /api/task-groups/list
Ответ: { message, count, groups:[{ id, tag, active, schedule, timeFrom, timeTo, isRepeatable, timeout, activeSession }] }.

GET /api/task-groups/get?id=N
Группа + задачи: { message, group:{...}, tasks:[{ id, uuid, scriptId, accountId, status, executeAt, additionalData }], tasksCount }.

GET /api/task-groups/tasks?groupId=N
Только задачи группы. Статусы: waiting, working, finished, error, stop, stopWithError.

POST /api/task-groups/create
Создаёт пустую группу. Body { tag?:string, name?:string, active?:bool, ...scheduleFields }. Рекомендуется active:false, затем создать задачи и вызвать start.

POST /api/task-groups/update
PATCH по id: schedule-поля, tag, active, isFavorite, isDeleted, isRescheduled.

POST /api/task-groups/start
Body { id } или { groupId }. Ставит active=1; waiting-задачи подхватывает scheduler. Идемпотентно, завершённые не ресетит.

POST /api/task-groups/restart
Body { id }. Ставит active=1 и переводит finished/error/stop/stopWithError обратно в waiting с executeAt=now().

POST /api/task-groups/stop
Body { id }. Ставит active=0, переводит working в stopWithError; браузеры не закрывает.

POST /api/task-groups/delete или DELETE /api/task-groups/delete?id=N
Soft-delete группы и её задач (isDeleted=1, UI deleteGroup). POST body { id }.

POST /api/task-groups/hard-delete
Удаляет задачи группы и ряд группы. Body { id } или { ids:[...] }; ответ содержит deletedGroups, deletedTasks, ids.

Tasks - задачи#

Задача = scriptId × accountId с executeAt, status, additionalData, sort.

POST /api/tasks/create
Создаёт одну/несколько задач в группе одной транзакцией. Body { groupId:int, tasks:[...] }; ответ { message, created, requested, errors }.

Поле task:

ПолеТипОписание
accountIdintrequired, account.id
scriptIdint/stringrequired
additionalDataobjectform-поля скрипта; иначе defaultValue
executeAtISO stringdefault now
tagstringМетка
sortintПорядок

additionalData критично, если у скрипта есть form-поля.

POST /api/tasks/update
PATCH по id или taskId: status, tag, description, executeAt, sort, additionalData.

GET /api/tasks/list
Плоский список задач; фильтры AND.

Query params:

ПараметрТипОписание
statusstringCSV: working,waiting,finished,error,stop,stopWithError
groupIdintГруппа
accountIdintaccount.id
scriptIdint/stringСкрипт
limitintDefault 500, max 5000

Ответ: { message, count, tasks:[{ id, uuid, status, groupId, accountId, scriptId, executeAt }] }.

GET /api/tasks/active
Все working задачи с account:{id,name,accountId} и script:{id,name}.

POST /api/tasks/delete
Безвозвратное удаление задач. Body { id } или { ids:[...] }.

POST /api/tasks/stop
Останавливает working/waiting: status → stop + abort executor по uuid; closeBrowser:true дополнительно закрывает браузер аккаунта. Body { id } / { ids:[...] } / [{ id, uuid? }, ...], плюс closeBrowser?:bool.

Logs#

GET /api/tasks/logs?taskUuid=UUID (или ?taskId=UUID)
Текстовый лог по task.uuid (не numeric task.id), Content-Type: text/plain; charset=utf-8; можно читать во время выполнения. 404, если файл ещё не создан или удалён.

GET /api/scripts/run-logs?uuid=UUID
То же для прямых запусков через /api/scripts/run.

Emails - IMAP-учётные данные#

GET /api/emails/list
Список IMAP-учётных данных; пароли не возвращаются. Ответ: { message, count, emails:[{ id, email, imapServer, port, isActive, mailboxes }] }.

POST /api/emails/toggle
Включает/выключает IMAP-мониторинг: обновляет isActive и синхронно открывает/закрывает соединение. Body { email:"user@gmail.com", isActive:true }.

Error format#

Ошибка: HTTP 4xx/5xx + { "error": "Описание ошибки" }.

  • 400 - required-поля / JSON.
  • 401 - неверный/нет x-api-key (соединение закрывается).
  • 404 - ресурс не найден.
  • 500 - DB/внутренняя ошибка.
  • Некоторые бизнес-ошибки идут HTTP 200 с { status:"error", code:"<i18n-key>", message:"..." }, например duplicate global vars.

Полный пример: запустить скрипт на аккаунтах с расписанием#

Сценарий: MintNFT (scriptId=12) с walletPassword, mintCount на аккаунтах 42/43/44, окно 08:00-20:00, repeat 2, параллелизм 5, старт 2026-05-11T09:00:00.000Z.

  1. Найти IDs: GET /api/scripts/list; GET /api/profiles/list?groupId=5.
  2. Проверить/прогреть прокси: POST /api/proxies/check, body { accountIds:[42,43,44] }.
  3. Создать группу: 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 }; сохранить group.id.
  4. Создать задачи: POST /api/tasks/create, body { groupId:7, tasks:[{accountId:42,scriptId:12,additionalData:{walletPassword:"pwd1",mintCount:"1"},executeAt:"2026-05-11T09:00:00.000Z"}, ...] }.
  5. Активировать: POST /api/task-groups/start, body { id:7 }.
  6. Мониторить: GET /api/task-groups/get?id=7, затем GET /api/tasks/logs?taskUuid=<uuid>.
  7. Управление: GET /api/tasks/active; POST /api/tasks/stop with { ids:[100], closeBrowser:true }; POST /api/task-groups/restart; POST /api/task-groups/hard-delete.

Прямое подключение к браузеру через CDP#

После POST /api/profiles/start используйте wsEndpoint.

js

Python pyppeteer: browser = await connect(browserWSEndpoint=ws_endpoint). Без CDP-клиента используйте POST /api/profiles/eval и POST /api/profiles/screenshot.

OAuth callback#

GET / или GET /oauth/callback?code=AUTH_CODE не требует api-key; используется Google Drive OAuth flow, передаёт code во внутренний канал и закрывает окно.

Source of truth#

Реализация: src-tauri/src/browser/server.rs. Если поведение endpoint'а отличается от документации, source code is source of truth.

Связанные термины глоссария