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 |
пауза по последнему контакту |
Без связи текущее состояние неизвестно, поэтому availability — unknown,
а не последнее запомненное значение. 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:
- Mac выходит на связь, просыпается или снимается с паузы.
- Приложение показывает уведомление «{Платформа}: N задач ждали, пока Mac проснётся» и поступает по настройке владельца: спрашивает (по умолчанию), выполняет сразу или не выполняет.
- Принятые команды выполняются по одной, в порядке постановки, со всеми обычными проверками: права, скрытые приложения, подтверждения опасных действий.
| Статус | Когда |
|---|---|
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 часов.
Лимиты
- 50 запросов в секунду на ключ, пик 100.
- На один Mac: скриншоты 5/с, одна серия действий, до 4 команд терминала.
Чего API не делает
- Не хранит скриншоты и вывод команд на сервере.
- Не открывает входящих портов на Mac: приложение само подключается к нам.
- Не вводит пароль пользователя и не снимает блокировку экрана.
- Не показывает и не трогает приложения, которые пользователь скрыл.
Исключение честно названо в интерфейсе: права
shellиfilesработают в обход этого списка.