Dyrra API v1
New here? Start with the quickstart: your agent's first click in 10 minutes.
One API to drive any Mac for AI agents. A user's connected Mac and a cloud Mac
are called the same way — only {target} changes.
Base URL: https://api.dyrra.dev/v1 · Specification: GET /v1/openapi.json
Authentication
Authorization: Bearer dk_live_… # live key
Authorization: Bearer dk_test_… # test key
A key is shown once, when it is created. We store only its HMAC-SHA256.
Every response carries an X-Request-Id header — include it when you contact support.
Key format
A key is dk_live_ or dk_test_ followed by 60 characters [0-9A-Za-z]:
54 characters of randomness (40 random bytes) and a 6-character checksum
(CRC32 of everything before it, in base62). The checksum is not a secret — it
only catches typos:
@dyrra/sdkand@dyrra/mcpcheck it before sending anything and name the problem;- the API answers
401 unauthorizedwithdetails.reason:checksum(a typo),length(part of the key is missing),characters(spaces, quotes, line breaks) orprefix(not a Dyrra key).
Keys issued before September 2026 are shorter (dk_live_ + 43 characters,
no checksum). They keep working; new keys always use the format above.
Connect an agent (MCP)
@dyrra/mcp is a standard MCP server over stdio, started with npx -y @dyrra/mcp.
It needs two environment variables: DYRRA_API_KEY and DYRRA_TARGET
(a con_… connection or a cloud Mac id). The console's MCP page gives a ready
config with both filled in for each agent:
| Agent | How it is added |
|---|---|
| Claude Code | claude mcp add --env … --scope user --transport stdio dyrra -- npx -y @dyrra/mcp |
| Cursor | ~/.cursor/mcp.json, key mcpServers |
| VS Code | code --add-mcp '{"name":"dyrra","type":"stdio",…}' |
| OpenClaw | openclaw mcp set dyrra '{"command":"npx",…}' |
| Hermes | ~/.hermes/config.yaml, key mcp_servers, then /reload-mcp |
| Codex CLI | codex mcp add dyrra --env … -- npx -y @dyrra/mcp |
| Any other MCP client | JSON with mcpServers |
The server registers only the tools the connection's permissions allow.
Optional variables: DYRRA_AGENT_NAME — the name shown in the console (by
default it comes from the MCP client: Claude Code, Codex, Cursor…), and
DYRRA_TASK — the agent's task. The agent can also set its task itself with
mac_task.
Every POST supports Idempotency-Key (kept for 24 hours).
Several agents on one computer
A computer has one screen, one mouse, one keyboard and one clipboard. Several agents can still work with it: one at a time holds the turn and the others wait in line, in the order they came. The turn covers a whole "look → act → check" sequence, not a single click.
- Taking a screenshot, reading the screen tree or the window list, input, opening apps and links, and writing the clipboard need the turn. The terminal, files and reading the clipboard do not: agents use them in parallel.
- The turn passes on when its holder releases it
(
POST /v1/targets/{target}/turn/release,mac_releasein MCP), after 20 s without actions, or after 90 s while others wait. - A request waits in line for up to 25 s, then answers
409 machine_busywithRetry-After: 1anddetails: { holder, position }. Repeat it: the place in line is kept for 60 s. The SDK waits for you, up to 2 minutes. - If another agent used the computer since this agent last looked at the
screen, its input is not sent:
409 stale_view. Take a new screenshot or screen tree, then act — clicks never land on a screen that has changed. GET /v1/targets/{target}/turntells who holds the turn and your place in line.
Each running agent is an agent session. The client picks an id (as_ and
16–64 letters or digits) and sends it in the Dyrra-Agent-Session header with
every request; @dyrra/mcp and the SDK do it for you
(new Dyrra(key, { agentSession: newAgentSessionId() })).
POST /v1/agent-sessions
{ "id": "as_…", "client": "claude-code 2.1.0", "task": "Testing the signup form" }
→ { "id": "as_…", "name": "Claude Code", "status": "working", "paused": false, … }
Send it every 30 s while the agent runs: the console tells a working agent
from a lost one. DELETE /v1/agent-sessions/{id} ends the session and passes
its turn on at once. Requests without the header still work: everything made
with one API key counts as one agent named after the key.
The console's Live agents page shows each agent with its task, computer
and status — working, in line, waiting for the person, connected, paused,
lost — and lets the person pause one agent (409 agent_paused) or all of them.
Other agents keep working while one is paused.
Coordinates come first
All screen coordinates in the API are logical points of the main display, not Retina pixels.
By default a screenshot is scaled down to its size in points, so one image pixel equals one screen point, and coordinates taken from the image can be passed to actions as they are.
If you ask for maxWidth, the response includes scale = imageWidth / widthPoints.
Divide a coordinate taken from such an image by scale:
const shot = await mac.screenshot({ maxWidth: 1280 });
// the model pointed at (640, 400) in image pixels
const x = 640 / shot.scale;
const y = 400 / shot.scale;
await mac.actions([{ type: "click", x, y }]);
The @dyrra/sdk adapters (fromAnthropicAction, fromOpenAIAction) do this
conversion themselves when you pass them { scale }.
Coordinates outside the screen → 400 invalid_request.
Connection permissions
| Permission | What it allows | Default |
|---|---|---|
screen |
Screenshots, the screen tree, the window list | required |
input |
Mouse, keyboard, opening apps and links | pre-selected |
shell |
Terminal commands | off |
files |
Reading and writing in folders the user chose | off |
clipboard |
The clipboard | off |
record |
Sharing session recordings with the platform | off |
A platform requests permissions in the link session, but the user may grant fewer. The stricter set wins. Permissions are enforced both on our server and in the app on the Mac.
Connecting a 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_…#token", "code": "7K4Q-9XPM",
"expiresAt": "…" }
Send the url to your user. They open it, the Dyrra Agent app shows a consent
screen, and once they approve you receive a connection.created webhook with the
connectionId. The link is valid for 30 minutes.
One Mac serves only one of your users. Linking the same Mac to another
endUserId of the same platform returns machine_already_linked_to_other_user.
This is a requirement of the macOS license, not our own restriction.
Mac status
GET /v1/connections/{connectionId} returns in machine:
| Field | Meaning |
|---|---|
online |
whether the app is connected right now |
availability |
ready, asleep, screen_locked, no_gui_session — or unknown when the Mac is offline |
lastSeenAt |
when the Mac was last online; null if never |
paused |
pause state as of the last contact |
os |
macos, windows or linux. Pick shortcuts by it: cmd on macOS, ctrl elsewhere |
Without a connection the current state is unknown, so availability is
unknown rather than the last remembered value.
availability describes the Mac, not the permissions. The connection's
permissions are in the scopes field of the same response. A connection with
only screen sees the screen while availability is "ready", but every mouse
or keyboard action gets 403 scope_denied. Check scopes before you start.
Permissions live on the Mac. grantedOnMac in the same response says
whether the app on the Mac itself holds this connection's permissions. false
means they were reset there — the app was reinstalled, or updated from version
0.1.8 or older — and every action is refused with 403 scope_denied,
details.reason: "not_granted_on_mac". Only the owner can fix it: by
reconnecting the Mac (a new link session for the same Mac keeps the same
connectionId). null means unknown: the Mac is offline or its app is older
than 0.1.10.
The mac_status MCP tool puts it all together: canSee, canControl, the
reasons why not, and the granted permissions with what each one allows.
Control
{target} is a connectionId (con_…) for a connected Mac or a
cloudMacId (cmc_…) for a cloud one.
Screen tree — start here
POST /v1/targets/{target}/ui-tree
{ "scope": "frontmost", "interactiveOnly": true, "format": "compact" }
→ { "snapshotId": "snp_…",
"app": { "bundleId": "com.apple.TextEdit", "name": "TextEdit" },
"compact": "[e12] button \"Save\" (812,640 96x28)\n[e13] textfield \"Search\" …",
"elements": [ … ], "truncated": false }
Cheaper and more precise than a screenshot: the agent clicks an element instead
of guessing pixels. A snapshot lives for 30 seconds, then 410 snapshot_expired.
Password field values (AXSecureTextField) are never returned.
Actions
POST /v1/targets/{target}/actions
{ "actions": [
{ "type": "click_element", "snapshotId": "snp_…", "elementId": "e12" },
{ "type": "type", "text": "Hello" },
{ "type": "key", "keys": ["cmd", "s"] }
],
"screenshotAfter": { "maxWidth": 1280 } }
→ { "ok": true, "screenshot": { … } }
Up to 50 actions per batch. On an error the batch stops and returns
failedIndex. Only one batch runs on a Mac at a time: a second batch waits
for the first. Another agent waits for its turn — see
Several agents on one computer.
Action types: move, click, mouse_down, mouse_up, drag, scroll,
type, key, wait, click_element, set_value.
Modifiers: cmd, shift, alt, ctrl, fn, win. cmd exists only on
macOS and win only on Windows; a computer refuses a modifier its system lacks
rather than swapping it silently. Choose by machine.os.
Typing into the right app. Text and keys go to the app in front. If focus has
not switched yet, the text lands in someone else's window. Pass
expectBundleId — the Mac checks the app in front before every action of the
batch and stops the batch if another app is there:
POST /v1/targets/{target}/actions
{ "actions": [ { "type": "type", "text": "Hello" } ],
"expectBundleId": "com.apple.Notes" }
→ 409 { "error": { "code": "app_not_frontmost",
"details": { "expected": "com.apple.Notes",
"actual": "com.anthropic.claudefordesktop",
"actualName": "Claude", "failedIndex": "0" } } }
Before the first action the Mac waits up to 2 seconds for the app to come to the
front; before the others it checks right away. The MCP server fills in
expectBundleId for mac_type and mac_key itself — it is the app last opened
with mac_open_app.
Screenshot
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 }
Screenshots are not blocked by a control session and are never stored on our server.
Terminal, files, native actions
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 and apps/activate answer only once the app is actually in front
(they wait up to 5 seconds) and return { "ok": true, "bundleId": "…", "name": "…" }.
If it did not come to the front — 409 app_not_frontmost, and you should not type
into it.
click_element checks that it will land where it promised. Before clicking,
the Mac checks that the snapshot's app is in front and that the same element is
under the point. If the window moved — 410 snapshot_expired, not a silent
success. If another app is in front — 409 app_not_frontmost.
Confirmations
Dangerous actions need the person's approval by default: sending a message,
paying, deleting, answering another app's dialog. What matters is the
consequence, not the method — ⌘⇧D in Mail sends an email just like the Send
button, so it asks too. The Mac checks known dangerous shortcuts for the app in
front, Return or Space while another app's dialog is open, and Return while
focus is on a send button. Terminal commands and deleting files also ask.
The command runs only after the person clicks Allow on the Mac, so the
answer may take a while. Denied → 403 denied_by_user, no answer →
408 confirmation_timeout. The person can relax the policy per connection.
Dyrra's own interface is off limits to the agent. The confirmation window,
the activity frame and the badge never appear in a screenshot, in ui_tree or in
the window list, and a click into them is refused with 409 dyrra_ui_protected.
Approval is the person's decision; the agent cannot press Allow for them.
When the Mac is unavailable
Add whenUnavailable: "queue" and ttlSeconds (up to 86,400) to the request:
POST /v1/targets/{target}/actions
{ "actions": [ … ], "whenUnavailable": "queue", "ttlSeconds": 3600 }
→ 202 { "commandId": "cmd_…", "status": "queued", "expiresAt": "…" }
GET /v1/commands/{commandId}
DELETE /v1/commands/{commandId} # while the command is queued
These can be queued: actions, shell, apps/open, apps/activate,
urls/open and writing the clipboard (PUT clipboard). Screenshots, ui-tree
and reading the clipboard cannot: they only make sense in the moment.
How a command reaches the Mac:
- The Mac comes online, wakes up or is unpaused.
- The app shows a notification — "{Platform}: N tasks were waiting for the Mac to wake up" — and acts on the owner's setting: ask (default), run right away, or don't run.
- Accepted commands run one at a time, in the order they were queued, with all the usual checks: permissions, hidden apps, confirmations for dangerous actions.
| Status | When |
|---|---|
queued |
waiting for the Mac or for the owner's decision |
running |
running |
completed |
done, command.completed webhook |
failed |
failed, or the owner declined (error.code: denied_by_user), command.failed webhook |
expired |
the term ran out first, command.expired webhook |
canceled |
canceled with DELETE |
The result is kept for 24 hours, without screenshots. Terminal output is never
stored on the server: for shell the result has only exitCode,
durationMs, truncated and timedOut.
If the owner does not answer, that is not a refusal: the command waits until its term ends and is offered again the next time the Mac comes online.
The queue works for connected Macs. A cloud Mac that is offline still returns
409 machine_offline.
Dyrra does not wake a sleeping Mac. Remote wake is not reliable, and we do not promise it.
Cloud Macs
POST /v1/cloud-macs
{ "term": "day", "hardware": "mac_mini_m4", "developerUseAcknowledged": true }
→ 201 { "id": "cmc_…", "status": "awaiting_payment", "hardware": "mac_mini_m4",
"price": { "amount": 1000, "currency": "usd" },
"checkoutUrl": "https://app.norelio.eu/p/…?client_reference_id=cmc_…" }
The machine is not handed over right away. The lease is created as
awaiting_payment; send your customer to checkoutUrl. The machine is assigned
only after the payment is confirmed, with no one on our side involved:
status |
What it means |
|---|---|
awaiting_payment |
Created, not paid. checkoutUrl works for 24 hours. |
queued |
Paid; we are preparing the machine. readyBy = paidAt + 24 hours. The paid term has not started yet. Calls to the target answer 409 lease_not_active. |
ready |
Handed over. The paid term runs from leaseStartedAt to leaseEndsAt. A cloud_mac.ready webhook arrives and the account owner gets an email. |
ended |
Finished or cancelled before payment. |
A lease becomes ready straight away when a machine of the requested model is
free and online in our pool; otherwise it waits in queued and gets the first
machine of that model that joins the pool, in order of payment.
Extending works the same way: POST /v1/cloud-macs/{id}/extend returns a link,
and the term moves on payment — from the end of the paid period, not from "now".
Machines
hardware |
Where it comes from | On order |
|---|---|---|
mac_mini_m4 (default) |
our pool | ready if a machine is free, else queued |
mac_mini_m4_pro |
our pool | on request: out of stock at our providers |
mac_studio_m1_max |
built to order | queued until the machine is sourced |
mac_studio_m2_max |
built to order | on request: out of stock at our providers |
mac_studio_m2_ultra |
built to order | queued until the machine is sourced |
mac_studio_m4_max |
built to order | on request: out of stock at our providers |
mac_studio_m1_ultra |
built to order | on request |
mac_studio_m3_ultra |
built to order | on request |
mac_pro |
built to order | on request |
mac_mini_m5_pro |
— | coming soon |
mac_mini_m6 |
— | coming soon |
mac_studio_m5_max |
— | coming soon |
mac_studio_m5_ultra |
— | coming soon |
We never substitute hardware: if the pool has no machine of the requested kind,
the lease waits in the queue instead of running on a different machine. We
promise a machine within 24 hours of payment; the paid term starts only when
it is handed over. Readiness arrives as a cloud_mac.ready webhook.
Memory and storage
Each machine has a base configuration — the one in the price table. More memory or a larger SSD is a surcharge on top of the base price:
POST /v1/cloud-macs
{ "term": "month", "hardware": "mac_studio_m2_ultra", "ramGb": 128,
"developerUseAcknowledged": true, "contactEmail": "you@example.com" }
→ 202 { "id": "cmq_…", "status": "quote_requested", "hardware": "mac_studio_m2_ultra",
"spec": { "ramGb": 128, "ssdGb": 2048 }, "estimateCents": 63000, … }
ramGb and ssdGb are optional; leaving them out means the base configuration.
An upgraded configuration is ordered as a request with a calculated price, not
as a lease: we send you an invoice. A size the machine does not come in →
400 invalid_request with the available sizes. If the provider does not offer
that combination, estimateCents is null and the price is on request.
| Machine | Base configuration | Memory options | SSD options |
|---|---|---|---|
| Mac mini M4 | 16 GB / 256 GB | 24, 32 GB | 512 GB – 2 TB |
| Mac mini M4 Pro | 64 GB / 1 TB | — | 2 – 8 TB |
| Mac Studio M1 Max | 64 GB / 2 TB | — | 4 – 8 TB |
| Mac Studio M2 Max | 64 GB / 1 TB | 96 GB | 2 – 8 TB |
| Mac Studio M2 Ultra | 64 GB / 2 TB | 128, 192 GB | 4 – 8 TB |
| Mac Studio M4 Max | 64 GB / 1 TB | 128 GB | 2 – 8 TB |
| Mac mini M5 Pro | 24 GB / 512 GB, 15-core CPU | 48, 64 GB | 1 – 8 TB |
| Mac mini M6 | 16 GB / 256 GB, 12-core CPU | 24, 32 GB | 512 GB – 2 TB |
Mac mini M5 Pro also comes with an 18-core CPU and 20-core GPU: pass
"chip": "18-core CPU, 20-core GPU". A chip other than the base one is always a
request, like an upgrade.
The console's Cloud Macs page shows every option with its surcharge, or "on request" when it is not offered or not priced yet.
On request and coming soon
What can be leased is decided by the price in our configuration, not by a list in code. If there is a price for the requested term, a lease is created. If not, the request does not create a lease and charges nothing: you get a request id, and a person replies.
POST /v1/cloud-macs
{ "term": "month", "hardware": "mac_pro", "developerUseAcknowledged": true,
"contactEmail": "you@example.com", "note": "4 machines for CI" }
→ 202 { "id": "cmq_…", "status": "quote_requested", "hardware": "mac_pro",
"term": "month", "message": "Request received: Mac Pro, month term. …" }
contactEmail and note are optional, but without an email we cannot reply.
Coming soon is a separate state. Apple announced Mac mini M5 Pro and M6 and Mac Studio M5 Max and M5 Ultra on 25 August 2026 and ships them from 22 September 2026; providers don't have them yet. We accept requests for them, and the response says plainly that the machine will be available later.
Prices
| Machine | Day | Week | Month |
|---|---|---|---|
| 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 | on request | on request | on request |
| Mac mini M5 Pro, M6 | coming soon | coming soon | coming soon |
| Mac Studio M5 Max, M5 Ultra | coming soon | coming soon | coming soon |
A month is the best value: a day is a month ÷ 20, a week is a month ÷ 4. The difference covers idle time between leases.
Every machine comes with Xcode and developer tools preinstalled, and the API and MCP ready to use.
Cloud Macs are provided for software development and testing only: building
from source, automated tests, developer tools, simulators. Without
developerUseAcknowledged: true the request is refused. The minimum lease is
24 hours: ending early does not release the machine sooner than 24 hours and is
not refunded.
These are the terms of section 3 of the macOS license. By passing
developerUseAcknowledged: true you also agree to
Apple's macOS license and the terms of the
software on the Mac, such as Xcode. If you give the Mac to your own users,
section 3 applies to you as well: each of them must agree to those terms
before using it.
Billing for connected Macs
Free for up to 10 active Macs per month. Beyond that, a package: 50, 200 or 1000 Macs.
GET /v1/subscription
→ { "package": "free", "status": "active",
"machineLimit": 10, "activeMachines": 7,
"currentPeriodEnd": null, "cancelAtPeriodEnd": false,
"upgradeUrls": { "p50": "https://app.norelio.eu/p/…", … } }
We bill by package, not by usage: payment goes through an external link, and a
link cannot charge a variable amount. When the package is used up, calls to new
Macs get 402 with the suggested package:
{ "error": { "code": "payment_required",
"message": "The free plan allows 10 active Macs per month, 10 are in use. Move to the 50 Mac plan in the console.",
"details": { "package": "free", "machineLimit": 10, "activeMachines": 10, "suggestedPackage": "p50" } } }
If a payment fails, the subscription becomes past_due, but the limit holds for
7 more days. After that grace period the free package remains.
Session recordings
A video of what agents do on a Mac. Recording is off by default and stays on the Mac (Movies → Dyrra, kept 7 days) unless the owner shares it. Your platform receives recordings only when all three hold:
- the owner set Dyrra Agent → Session recording → Share with platforms I allow;
- the owner allowed your platform in a separate consent window;
- the connection has the
recordpermission.
Cloud Macs are recorded for the platform that leases them, without a separate consent. The app records only while an agent acts: it starts on the first action and stops after 10 seconds without actions (pauses are cut from the video). H.264 MP4, 8 frames per second, up to 1440p. Hidden apps and Dyrra's own windows are not in the picture.
GET /v1/targets/{target}/recordings
→ 200 { "data": [ { "id": "rec_…", "target": "con_…", "status": "ready",
"url": "https://…signed…", "expiresAt": "…",
"deletesAt": "…", "durationSec": 42, "createdAt": "…" } ] }
GET /v1/recordings/{recordingId}
→ 200 { "id": "rec_…", "status": "processing" | "ready", "url"?: "…", … }
urlis a signed link to the MP4 file, valid for 24 hours (expiresAt). Request the recording again for a fresh one.- Recordings are deleted after 7 days (
deletesAt). If the owner stops sharing, recordings already shared with you are deleted at once. 403 recording_not_shared— the owner has not allowed sharing with your platform (or the connection lacksrecord).503 recordings_unavailable— recording storage is not set up on the server.- The
recording.readywebhook arrives when a recording is uploaded:{ recordingId, target, durationSec, deletesAt }.
Record and replay
Record what an agent does once and run it again later without the model — a setup repeated on every new Mac, a check after a change. Recording happens on your side, in the SDK or the MCP server: Dyrra's servers never store a script, so typed text and commands stay with you.
- SDK:
dyrra.record(target)returns a client with the same methods asdyrra.targets(target);recording.script()gives the steps.dyrra.replay(target, script)runs them on any Mac. - MCP:
mac_record_start,mac_record_stopwith a name,mac_replay,mac_replays. Scripts are saved in~/.dyrra/replayson the computer where the MCP server runs.
A script is JSON ("format": "dyrra-replay", version 1) with steps of five
kinds: actions, open_app, open_url, shell and clipboard_set, each
with atMs — milliseconds since the recording started. Screenshots and screen
trees are not steps. Clicks on elements are saved as the element — its role
plus identifier, title or description — because snapshot ids live 30 seconds;
on replay the element is found again in a fresh tree of the front app. A
match without any label finds nothing: replay never guesses which of several
buttons was meant.
Replay calls the ordinary endpoints, so every rule of a live session applies:
pause, availability, hidden apps and the owner's confirmations. When the
screen differs from the recording, replay stops at that step with
element_not_found and reports which steps ran. Pauses between steps are the
recorded ones, capped at 1.5 s by default.
Audit log
What happened on your Macs and which API key did it — for your security team
or SIEM. Newest first, 50 per page (up to 200 with limit).
GET /v1/audit-events?limit=50&connectionId=con_…&type=shell.run
→ 200 { "data": [ { "id": "req_…", "at": "…", "actor": "platform", "type": "shell.run",
"connectionId": "con_…", "machineId": "mch_…",
"apiKey": { "id": "key_…", "prefix": "dk_live_4Tz8", "name": "CI" },
"details": { "command": "xcodebuild -scheme App" } } ],
"nextBefore": "req_…" }
Pass before=<nextBefore> for the next page. Recorded: actions (count and
types), terminal commands (the command, up to 200 characters), clipboard reads
and writes (length only), opened apps, opened links (site only), file uploads
and downloads (path), connections, API keys, cloud leases and payments. Never
recorded: typed text, command output, clipboard and file contents, full URLs,
screenshots.
Errors
{ "error": { "code": "machine_offline", "message": "…", "request_id": "req_…" } }
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_request |
Validation failed, coordinates outside the screen |
| 401 | unauthorized |
The key is wrong or revoked |
| 402 | payment_required |
The package limit is reached, or payment is not set up |
| 403 | forbidden |
Another account's resource, or a revoked connection |
| 403 | scope_denied |
The connection lacks the permission; details.reason: "not_granted_on_mac" — permissions were reset on the Mac, the owner has to reconnect it |
| 403 | denied_by_user |
The person denied the confirmation |
| 403 | recording_not_shared |
The owner has not allowed sharing recordings with your platform |
| 404 | not_found |
|
| 408 | confirmation_timeout |
The person did not answer in time |
| 409 | machine_offline |
The app is not connected |
| 409 | machine_unavailable |
details.reason: asleep, screen_locked, no_gui_session |
| 409 | machine_paused |
The person paused the agent |
| 409 | machine_busy |
Another agent holds the turn; details: holder, position. Repeat — the place in line is kept |
| 409 | stale_view |
Another agent used the computer since your last look: take a new screenshot or tree first |
| 409 | agent_paused |
A person paused this agent in the console |
| 409 | permission_missing |
details.permission: screenRecording or accessibility |
| 409 | hidden_app_active |
The action involves a hidden app |
| 409 | app_not_frontmost |
Another app is in front: the input was not sent |
| 409 | dyrra_ui_protected |
Dyrra's own window: no screenshot, no tree, no click |
| 409 | lease_not_active |
The cloud Mac is not leased or the lease has ended |
| 409 | machine_already_linked_to_other_user |
The Mac is already linked to another user of the platform |
| 410 | snapshot_expired |
The ui_tree snapshot has expired |
| 410 | link_session_expired |
The link session has expired |
| 426 | app_update_required |
The app version is too old |
| 429 | rate_limited |
See Retry-After |
| 503 | recordings_unavailable |
Recording storage is not set up on the server |
| 504 | timeout |
|
| 500 | internal_error |
202 — the command was queued, or a cloud Mac request was received.
Webhooks
Signature: Dyrra-Signature: t=<ts>,v1=<hmac_sha256(secret, ts + "." + body)>.
Verify it before parsing the body: dyrra.webhooks.verify(header, rawBody, secret).
Events: 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.
Retries with exponential backoff for up to 24 hours.
Limits
- 50 requests per second per key, bursts up to 100.
- Per Mac: 5 screenshots per second, one action batch at a time, up to 4 terminal commands.
- Per account: up to 300 agent sessions at once.
What the API does not do
- It does not store screenshots or command output on the server.
- It does not open inbound ports on the Mac: the app connects to us.
- It does not type the user's password or unlock the screen.
- It does not show or touch apps the user hid. The exception is stated plainly
in the interface: the
shellandfilespermissions work around that list.