Dyrra API v1

Единый API к Mac для ИИ-агентов. Подключённый Mac пользователя и облачный Mac вызываются одинаково — меняется только {target}.

База: https://api.dyrra.dev/v1 · Спецификация: GET /v1/openapi.json


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

Authorization: Bearer dk_live_…   # боевой ключ
Authorization: Bearer dk_test_…   # тестовый

Ключ показывается один раз при создании. В базе хранится только HMAC-SHA256. В каждом ответе есть заголовок X-Request-Id — указывайте его в обращениях в поддержку.

Для всех POST поддерживается Idempotency-Key (хранится 24 часа).


Координаты — это главное

Все экранные координаты в API — логические точки главного дисплея, а не пиксели Retina.

Скриншот по умолчанию уменьшается до размера в точках, поэтому пиксель картинки равен точке экрана, и координаты с картинки можно передавать в действия как есть.

Если запросили maxWidth, ответ содержит scale = imageWidth / widthPoints. Координату с такой картинки нужно поделить на scale:

const shot = await mac.screenshot({ maxWidth: 1280 });
// модель показала на (640, 400) в пикселях картинки
const x = 640 / shot.scale;
const y = 400 / shot.scale;
await mac.actions([{ type: "click", x, y }]);

Адаптеры из @dyrra/sdk (fromAnthropicAction, fromOpenAIAction) делают этот пересчёт сами, если передать им { scale }.

Координаты вне экрана → 400 invalid_request.


Права подключения

Право Что даёт По умолчанию
screen Скриншот, структура экрана, список окон обязательно
input Мышь, клавиатура, открытие приложений и ссылок выдаётся обычно
shell Команды в терминале выключено
files Чтение и запись в выбранных пользователем папках выключено
clipboard Буфер обмена выключено
record Передача записей сессий платформе выключено

Платформа запрашивает права в сессии подключения, но пользователь может выдать меньше. Действует более строгий вариант. Права проверяются и на сервере, и в приложении на Mac.


Подключение Mac

POST /v1/end-users
{ "externalId": "user_123" }
→ 201 { "id": "eu_…", … }

POST /v1/link-sessions
{ "endUserId": "eu_…", "scopes": ["screen", "input"], "redirectUrl": "https://…" }
→ 201 { "id": "lks_…", "url": "https://link.dyrra.dev/l/lks_…#токен", "code": "7K4Q-9XPM",
        "expiresAt": "…" }

Отправьте url пользователю. Он откроет её, приложение Dyrra Agent покажет экран согласия, и после подтверждения вы получите webhook connection.created с connectionId. Ссылка живёт 30 минут.

Один Mac служит только одному вашему пользователю. Попытка привязать тот же Mac к другому endUserId этой же платформы даёт machine_already_linked_to_other_user. Это требование лицензии macOS, а не наше ограничение.


Состояние Mac

GET /v1/connections/{connectionId} возвращает в machine:

Поле Значение
online есть ли сейчас связь с приложением
availability ready, asleep, screen_locked, no_gui_session — или unknown, когда Mac не на связи
lastSeenAt когда Mac последний раз был на связи; null, если ещё ни разу
paused пауза по последнему контакту

Без связи текущее состояние неизвестно, поэтому availabilityunknown, а не последнее запомненное значение. online: false вместе с ready больше не встречается.

availability говорит о Mac, а не о правах. Права подключения — в поле scopes того же ответа. Подключение только с screen видит экран при availability: "ready", но любое действие мышью или клавиатурой получит 403 scope_denied. Проверяйте scopes до начала работы.

MCP-инструмент mac_status сводит всё вместе: canSee, canControl, причины, почему нельзя, и выданные права с тем, что каждое из них даёт.


Управление

{target} — это connectionId (con_…) для подключённого Mac или cloudMacId (cmc_…) для облачного.

Структура экрана — начинайте с неё

POST /v1/targets/{target}/ui-tree
{ "scope": "frontmost", "interactiveOnly": true, "format": "compact" }
→ { "snapshotId": "snp_…",
    "app": { "bundleId": "com.apple.TextEdit", "name": "TextEdit" },
    "compact": "[e12] button \"Сохранить\" (812,640 96x28)\n[e13] textfield \"Поиск\" …",
    "elements": [ … ], "truncated": false }

Дешевле и точнее скриншота: агент кликает по элементу, а не угадывает пиксели. Снимок живёт 30 секунд, дальше — 410 snapshot_expired.

Значения полей паролей (AXSecureTextField) не возвращаются никогда.

Действия

POST /v1/targets/{target}/actions
{ "actions": [
    { "type": "click_element", "snapshotId": "snp_…", "elementId": "e12" },
    { "type": "type", "text": "Привет" },
    { "type": "key", "keys": ["cmd", "s"] }
  ],
  "screenshotAfter": { "maxWidth": 1280 } }
→ { "ok": true, "screenshot": { … } }

До 50 действий в серии. При ошибке выполнение останавливается и возвращается failedIndex. Одновременно на одном Mac выполняется только одна серия: параллельный запрос получит 409 machine_busy с Retry-After.

Типы действий: move, click, mouse_down, mouse_up, drag, scroll, type, key, wait, click_element, set_value.

Ввод в нужное приложение. Текст и клавиши уходят в активное приложение. Если фокус не успел переключиться, текст попадёт в чужое окно. Передайте expectBundleId — Mac сверит активное приложение перед каждым действием серии и остановит её, если активно другое:

POST /v1/targets/{target}/actions
{ "actions": [ { "type": "type", "text": "Привет" } ],
  "expectBundleId": "com.apple.Notes" }
→ 409 { "error": { "code": "app_not_frontmost",
                   "details": { "expected": "com.apple.Notes",
                                "actual": "com.anthropic.claudefordesktop",
                                "actualName": "Claude", "failedIndex": "0" } } }

Перед первым действием Mac ждёт активации до 2 секунд, перед остальными проверяет сразу. MCP подставляет expectBundleId в mac_type и mac_key сам — это последнее приложение, открытое через mac_open_app.

Скриншот

POST /v1/targets/{target}/screenshot
{ "format": "jpeg", "quality": 80, "maxWidth": 1280 }
→ { "mime": "image/jpeg", "data": "…base64…",
    "imageWidth": 1280, "imageHeight": 831,
    "widthPoints": 1512, "heightPoints": 982, "scale": 0.846 }

Скриншоты не блокируются управляющей сессией и не сохраняются на сервере.

Терминал, файлы, нативные действия

POST /v1/targets/{target}/shell        { "command": "xcodebuild -list", "timeoutMs": 60000 }
GET  /v1/targets/{target}/files?path=…
PUT  /v1/targets/{target}/files?path=…
POST /v1/targets/{target}/apps/open    { "bundleId": "com.apple.Safari" }
GET  /v1/targets/{target}/apps
GET  /v1/targets/{target}/windows
POST /v1/targets/{target}/urls/open    { "url": "https://…" }
GET  /v1/targets/{target}/clipboard
PUT  /v1/targets/{target}/clipboard    { "text": "…" }

apps/open и apps/activate отвечают, только когда приложение стало активным (ждут до 5 секунд), и возвращают { "ok": true, "bundleId": "…", "name": "…" }. Не стало — 409 app_not_frontmost, и дальше вводить в него нельзя.

click_element проверяет, что попадёт куда обещал. Перед кликом Mac сверяет, что активно приложение из снимка и что под точкой тот же элемент. Если окно сдвинулось — 410 snapshot_expired, а не «готово» без эффекта. Если активно другое приложение — 409 app_not_frontmost.

Опасные сочетания клавиш подтверждаются наравне с кнопками. Подтверждение зависит от последствия, а не от способа: ⌘⇧D в Mail отправляет письмо так же, как кнопка. Mac проверяет три случая — известное сочетание в этом приложении (отправка, удаление), Return или пробел при открытом чужом диалоге и Return, когда фокус стоит на кнопке отправки. Отказ — 403 denied_by_user.

Интерфейс Dyrra агенту недоступен. Окно подтверждения, рамка работы и бейдж не попадают ни в скриншот, ни в ui_tree, ни в список окон, а клик в их область отклоняется с 409 dyrra_ui_protected. Подтверждение — решение человека, и агент не может нажать «Разрешить» за него.

Пользователь может включить подтверждение опасных действий. Тогда команда выполнится только после нажатия «Разрешить» на Mac, и ответ придёт не сразу. Отказ → 403 denied_by_user, молчание → 408 confirmation_timeout.


Когда Mac недоступен

Добавьте к запросу whenUnavailable: "queue" и ttlSeconds (до 86 400):

POST /v1/targets/{target}/actions
{ "actions": [ … ], "whenUnavailable": "queue", "ttlSeconds": 3600 }
→ 202 { "commandId": "cmd_…", "status": "queued", "expiresAt": "…" }

GET    /v1/commands/{commandId}
DELETE /v1/commands/{commandId}      # пока команда в очереди

В очередь можно поставить: actions, shell, apps/open, apps/activate, urls/open и запись в буфер обмена (PUT clipboard). Скриншот, ui-tree и чтение буфера — нельзя: они имеют смысл только в моменте.

Как команда доходит до Mac:

  1. Mac выходит на связь, просыпается или снимается с паузы.
  2. Приложение показывает уведомление «{Платформа}: N задач ждали, пока Mac проснётся» и поступает по настройке владельца: спрашивает (по умолчанию), выполняет сразу или не выполняет.
  3. Принятые команды выполняются по одной, в порядке постановки, со всеми обычными проверками: права, скрытые приложения, подтверждения опасных действий.
Статус Когда
queued ждёт Mac или решения владельца
running выполняется
completed выполнена, webhook command.completed
failed не выполнилась или владелец отказался (error.code: denied_by_user), webhook command.failed
expired срок вышел раньше, webhook command.expired
canceled отменена через DELETE

Результат хранится 24 часа, без скриншотов. Вывод команд терминала на сервере не хранится: для shell в результате только exitCode, durationMs, truncated и timedOut.

Если владелец не ответил, это не отказ: команда ждёт до конца срока, а при следующем выходе Mac на связь предлагается снова.

Очередь работает для подключённых Mac. Облачный Mac, который не на связи, по-прежнему отвечает 409 machine_offline.

Dyrra не будит спящий Mac. Удалённое пробуждение не гарантировано, и мы его не обещаем.


Облачные Mac

POST /v1/cloud-macs
{ "term": "day", "hardware": "mac_mini_m4", "developerUseAcknowledged": true }
→ 201 { "id": "cmc_…", "status": "awaiting_payment", "hardware": "mac_mini_m4",
        "price": { "amount": 900, "currency": "usd" },
        "checkoutUrl": "https://app.norelio.eu/p/…?client_reference_id=cmc_…" }

Машина выдаётся не сразу. Аренда создаётся со статусом awaiting_payment; отправьте клиента на checkoutUrl. Машина закрепляется за ним только после подтверждённой оплаты — тогда статус станет ready (или queued, если свободной машины нет) и придёт webhook cloud_mac.ready.

Продление работает так же: POST /v1/cloud-macs/{id}/extend возвращает ссылку, а срок сдвигается по факту оплаты — от конца оплаченного периода, а не от «сейчас».

Конфигурации железа

hardware Откуда берётся Что будет при заказе
mac_mini_m4 (по умолчанию) из пула ready, если есть свободная машина
mac_mini_m4_pro из пула ready, если есть свободная машина
mac_studio_m4_max добываем под клиента всегда queued
mac_studio_m1_max добываем под клиента пока по запросу
mac_studio_m1_ultra добываем под клиента пока по запросу
mac_studio_m2_max добываем под клиента пока по запросу
mac_studio_m2_ultra добываем под клиента пока по запросу
mac_studio_m3_ultra добываем под клиента пока по запросу
mac_studio_m4_ultra по запросу
mac_studio_m5_max добываем под клиента по запросу
mac_studio_m5_ultra добываем под клиента по запросу
mac_pro добываем под клиента по запросу

Конфигурацию не подменяем: если в пуле нет машины запрошенного железа, аренда встаёт в очередь, а не выдаётся на другой машине. В пуле мы держим только Mac mini; всё остальное берётся под конкретного клиента, и такой заказ уходит администратору. Готовность придёт webhook'ом cloud_mac.ready.

Apple никогда не выпускала Mac Studio с M4 Ultra: в 2025 году вышли M4 Max и M3 Ultra. Позиция mac_studio_m4_ultra в перечне есть, но машины такой не существует, поэтому цены у неё не будет.

Что сдаётся, а что по запросу

Решает цена в конфиге, а не список в коде. Есть цена на запрошенный срок — создаётся аренда. Нет — запрос аренду не создаёт и денег не списывает: приходит заявка, и дальше отвечает человек.

POST /v1/cloud-macs
{ "term": "month", "hardware": "mac_pro", "developerUseAcknowledged": true,
  "contactEmail": "you@example.com", "note": "нужно 4 машины под CI" }
→ 202 { "id": "cmq_…", "status": "quote_requested", "hardware": "mac_pro",
        "term": "month", "message": "Заявка принята: Mac Pro, срок «месяц». …" }

contactEmail и note необязательны, но без почты нам некуда ответить.

«Скоро» — отдельное состояние. Mac Studio M5 Max и M5 Ultra вышли 22 сентября 2026, у провайдеров их ещё нет. Заявку на них принимаем, но в ответе честно сказано, что машина появится позже. В каталоге консоли у них стоит «скоро», а не «по запросу». Заявка сохраняется у нас и уходит письмом администратору; в консоли она видна со статусом «ждёт ответа». Цена может появиться отдельно по каждому сроку: тогда этот срок сдаётся, а остальные остаются по запросу.

Цены

Конфигурация Сутки Неделя Месяц
Mac mini M4 $10 $48 $189
Mac mini M4 Pro $20 $97 $385
Mac Studio M1 Max $18 $88 $349
Mac Studio M2 Max $18 $90 $357
Mac Studio M2 Ultra $26 $130 $517
Mac Studio M4 Max $21 $104 $413
Mac Studio M1 Ultra, M3 Ultra, Mac Pro по запросу по запросу по запросу
Mac Studio M5 Max, M5 Ultra скоро скоро скоро

Цена месяца выгоднее: сутки — это месяц ÷ 20, неделя — месяц ÷ 4. Остаток покрывает простой машины между арендами.

Цены живут в конфиге, не в коде. Пустая цена — это не ошибка, а признак того, что конфигурация сдаётся по запросу.

Облачные Mac сдаются только для разработки и тестирования ПО: сборка из исходников, автотесты, инструменты разработчика, симуляторы. Без developerUseAcknowledged: true запрос отклоняется. Минимальный срок аренды — сутки: досрочное завершение не освобождает машину раньше 24 часов и не возвращает деньги.

Это условия раздела 3 лицензии macOS.


Оплата подключённых Mac

Бесплатно до 10 активных Mac в месяц. Дальше — пакет: 50, 200 или 1000 Mac.

GET /v1/subscription
→ { "package": "free", "status": "active",
    "machineLimit": 10, "activeMachines": 7,
    "currentPeriodEnd": null, "cancelAtPeriodEnd": false,
    "upgradeUrls": { "p50": "https://app.norelio.eu/p/…", … } }

Тарификация пакетами, а не по факту: оплата идёт по внешней ссылке, а по ней нельзя списать переменную сумму. Когда пакет исчерпан, вызовы к новым Mac получают 402 с указанием подходящего пакета:

{ "error": { "code": "payment_required",
  "message": "Пакет «бесплатный» вмещает 10 активных Mac в месяц, сейчас использовано 10. Перейдите на пакет «50 Mac» в консоли.",
  "details": { "package": "free", "machineLimit": 10, "activeMachines": 10, "suggestedPackage": "p50" } } }

Если платёж не прошёл, подписка переходит в past_due, но лимит держится ещё 7 дней. После отсрочки остаётся бесплатный пакет.


Ошибки

{ "error": { "code": "machine_offline", "message": "…", "request_id": "req_…" } }
HTTP Код Когда
400 invalid_request Валидация, координаты вне экрана
401 unauthorized Ключ неверный или отозван
402 payment_required Больше 10 активных Mac без карты
403 forbidden Чужой ресурс или отозванное подключение
403 scope_denied У подключения нет права
403 denied_by_user Пользователь отклонил подтверждение
403 recording_not_shared Записи не разрешены платформе
404 not_found
408 confirmation_timeout Пользователь не ответил вовремя
409 machine_offline Приложение не подключено
409 machine_unavailable details.reason: asleep, screen_locked, no_gui_session
409 machine_paused Пользователь нажал паузу
409 machine_busy Идёт другая управляющая сессия
409 permission_missing details.permission: screenRecording или accessibility
409 hidden_app_active Действие затрагивает скрытое приложение
409 app_not_frontmost Активно не то приложение: ввод не выполнен
409 dyrra_ui_protected Окно самого Dyrra: ни снимка, ни дерева, ни клика
409 lease_not_active Облачный Mac не арендован или срок истёк
409 machine_already_linked_to_other_user Mac уже привязан к другому пользователю платформы
410 snapshot_expired Снимок ui_tree устарел
426 app_update_required Слишком старая версия приложения
429 rate_limited Смотрите Retry-After
504 timeout
500 internal_error

202 — команда поставлена в очередь.


Webhooks

Подпись: Dyrra-Signature: t=<ts>,v1=<hmac_sha256(secret, ts + "." + body)>. Проверяйте её до разбора тела: dyrra.webhooks.verify(header, rawBody, secret).

События: connection.created, connection.revoked, machine.online, machine.offline, machine.paused, machine.resumed, machine.permissions_changed, cloud_mac.ready, cloud_mac.expiring, cloud_mac.ended, billing.payment_failed, command.completed, command.failed, command.expired, recording.ready.

Повторы с экспоненциальной задержкой до 24 часов.


Лимиты


Чего API не делает