# macOS Sandboxes (/docs/macos-sandboxes)

macOS sandboxes are **macOS virtual machines (VMs)** on dedicated Apple silicon hardware. Each sandbox runs on a Mac Mini that your organization reserves. You can run commands in a macOS sandbox, connect with SSH, see and control its desktop, and keep disk between sessions.

A Mac is reserved for a 24-hour period. You choose the cores, memory and disk of each sandbox when you create it: one sandbox can use the whole Mac, or two sandboxes share it. macOS sandboxes run only on reservations. When your organization holds no reservation, Daytona can buy one for you when you create a sandbox.

<MacosSandboxesVisual />

| **Term**         | **Description**                                                                                              |
| ---------------- | ------------------------------------------------------------------------------------------------------------ |
| **Reservation**  | One dedicated Mac for your organization for a 24-hour period. Nobody else uses that Mac while you hold it.  |
| **Size**         | The cores, memory and disk of one sandbox. You choose it when you create the sandbox.                        |
| **Model**        | The type of Mac.                                                            |
| **Auto-reserve** | An organization setting that buys a Mac for you when you create a macOS sandbox and hold no reservation.    |
| **Image**        | A macOS disk that a sandbox starts from: a base image or a snapshot of your sandbox.         |
| **Persistent**   | A sandbox that keeps its disk across stop and start. Stop, start, pause, resume and snapshots need it.       |

## Use cases [#use-cases]

macOS sandboxes are used when the software, the build, or the task requires macOS or Apple tooling.

- **Build and test Apple apps.** An agent or a CI job builds and tests iOS and macOS apps with Xcode and the iPhone simulator on a clean Mac. The base image ships with Xcode, its iPhone simulator, Homebrew, and Python. The image runs macOS 26.6.2, and its Homebrew includes CocoaPods, fastlane, and Node.js 24.

- **Computer-use agents on macOS.** An agent operates macOS applications through screenshots and mouse and keyboard input. Computer use also reads the accessibility tree, acts on controls, and records the screen. The desktop is 1280 by 800, and your own [VNC client](#vnc-client) can connect to it over a WebSocket.

- **Long-lived build machines.** A persistent sandbox keeps its disk across stop and start, so installed tools and build caches stay between sessions. To keep the disk after the reservation ends, take a [filesystem snapshot](#filesystem-snapshot). Automatic renewal stops when every sandbox on the Mac is stopped or paused, and the reservation then ends with its period.

- **Remote access over SSH.** A developer or a script connects to the Mac over SSH to run commands and inspect builds. The CLI opens the connection itself, and other SSH clients sign in with a short-lived sandbox token in the user name. The token is limited to one sandbox and to SSH, so no project API key goes into the SSH command.

- **Fleets of Macs.** One request reserves up to 100 Macs for parallel builds and tests. Each Mac is a separate reservation with its own ID, period, charge, and renewal. A minimum count accepts fewer Macs when not enough are available.

## Requirements [#requirements]

Your organization must have macOS sandboxes turned on. Without it, reserving a Mac or creating a macOS sandbox fails with a [feature error](#errors). Sign in to the dashboard at [app.daytona.com ↗](https://app.daytona.com) and create a project API key. The examples read the key from an environment variable.

### Endpoints [#endpoints]

macOS sandboxes have their own API address. Send all macOS requests to the macOS API.

| **What**                                  | **Address**                                     |
| ----------------------------------------- | ----------------------------------------------- |
| Dashboard                                 | **`https://app.daytona.com`**                   |
| Daytona API (organizations, API keys)     | **`https://api.daytona.com`**                   |
| macOS API (reservations, macOS sandboxes) | **`https://api.us-macos.daytona.com`**          |
| Sandbox SSH                               | **`ssh.us-macos.daytona.com`**, port **`2222`** |
| Region of the Macs                        | **`macos-1`**                                   |

The SDKs and the CLI read the API address and your key from environment variables. Give the region of the Macs, listed in the table above, on each reservation and each macOS create.

```bash
export DAYTONA_API_KEY=dtn_...
export DAYTONA_API_URL=https://api.us-macos.daytona.com
export DAYTONA_REGION=macos-1    # the CLI's default region
```

### Install [#install]

Pin these exact versions. Pre-release and latest installs pull other versions that do not have the macOS features.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

Python 3.10 or later:

```bash
pip install daytona==2.0.0a11
```

<Callout type="caution">
Do not use `pip install --pre daytona`. It installs another version that does not have the macOS features.
</Callout>

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

Node.js 22.4 or later:

```bash
npm install @daytona/sdk@2.0.0-alpha.11
```

<Callout type="caution">
Do not use `npm install @daytona/sdk@latest`. It installs another version that does not have the macOS features.
</Callout>

</TabItem>
<TabItem label="CLI" icon="seti:shell">

On macOS or Linux:

```bash
curl -fsSL https://github.com/daytona/clients/releases/download/v2.0.0-alpha.11/install.sh | sh -s -- --version 2.0.0-alpha.11
```

With the API key set in the environment, the CLI needs no login.

</TabItem>
</Tabs>

### Connect [#connect]

The SDK clients read your key and the macOS API address from the environment.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
from daytona import Daytona

daytona = Daytona()  # reads DAYTONA_API_KEY and DAYTONA_API_URL
# AsyncDaytona has the same methods with await
```

<Callout type="note" title="Units">
- `memory` and `disk` are in GiB.
- Timeouts are in seconds.
</Callout>

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona() // reads DAYTONA_API_KEY and DAYTONA_API_URL
```

<Callout type="note" title="Units">
- `resources.memory` and `resources.disk` are in GiB.
- Every timeout is in seconds: `timeoutSeconds`, `waitSeconds` and `createTimeoutSeconds`.
</Callout>

</TabItem>
</Tabs>

## Create a macOS sandbox [#create-a-macos-sandbox]

Create a macOS sandbox from a macOS image. Each sandbox runs on one of your [Mac reservations](#mac-reservations). When your organization holds no reservation in the region and [auto-reserve](#auto-reserve) is on, which it is by default, the create buys one for you, so you do not need to reserve a Mac first. With auto-reserve off, reserve a Mac before you create a sandbox. The base image has macOS, Xcode and common build tools. The following table lists its contents.

| **Property**     | **Value**                                                                                                   |
| ---------------- | ----------------------------------------------------------------------------------------------------------- |
| Image            | `base-macos`                                                                                                |
| Operating system | macOS 26.6.2                                                                                                |
| Tools            | Xcode 26.6 and its iPhone simulator, Homebrew (with git, Node.js 24, Ruby, CocoaPods, fastlane, gh and jq) and Python 3.14 |
| User             | `lume`, with home `/Users/lume`                                                                             |
| Password         | `lume`. See [user and password](#user-and-password)                                                         |

You choose the [size](#sizes) of each sandbox when you create it. Two sandboxes share one Mac when their sizes fit in the Mac together.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create_macos("base-macos", region="macos-1")
```

The following example creates two sandboxes of different sizes on one reservation:

```python
from daytona.v2 import MacosResources

r = daytona.v2.reservations.create(region="macos-1", model="m4pro-12c-24g",
                             idempotency_key="shared-mac-1")
a = daytona.v2.create_macos("base-macos", region="macos-1", reservation=r.id,
                             resources=MacosResources(cpu=4, memory=8, disk=100))
b = daytona.v2.create_macos("base-macos", region="macos-1", reservation=r.id,
                             resources=MacosResources(cpu=8, memory=16, disk=120))
```

The following example creates a persistent sandbox that uses the whole Mac:

```python
from daytona.v2 import MacosResources

r = daytona.v2.reservations.create(region="macos-1", model="m4pro-12c-24g",
                             idempotency_key="ios-builder-mac-1")
sandbox = daytona.v2.create_macos(
    "base-macos",
    region="macos-1",
    reservation=r.id,
    resources=MacosResources(cpu=12, memory=24, disk=220),
    persistent=True,
    name="ios-builder",
    wait_seconds=120,
)
print(sandbox.id, sandbox.reservation_id, sandbox.cpu, sandbox.memory, sandbox.disk)
```

| **Parameter**                | **Type**  | **Description**                                                                                                                       |
| ---------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `image`                  | str       | Sets the macOS image that the sandbox starts from, for example `base-macos` or a snapshot. This is the first positional argument. |
| `region`                 | str       | Sets the region, `macos-1`.                                                                                                       |
| `reservation`            | str       | Sets the reservation that the sandbox runs on: a reservation ID, or `"new"`. See [which reservation](#which-reservation).         |
| `auto_renew`             | bool      | Sets [automatic renewal](#automatic-renewal) of the reservation that the create buys. Only with `reservation="new"`.              |
| `resources`              | MacosResources | Sets the size: `cpu`, the number of CPU cores; `memory`, the memory in GiB; and `disk`, the whole guest disk in GiB, macOS included. See [sizes](#sizes). |
| `persistent`             | bool      | Keeps the disk across stop and start. See [filesystem persistence](#filesystem-persistence).                                          |
| `name`                   | str       | Sets the display name.                                                                                                                |
| `wait_seconds`           | int       | Sets how many seconds the call waits for the sandbox to start.                                                                        |
| `create_timeout_seconds` | int       | Sets how many seconds the server tries before it deletes the waiting sandbox.                                                         |

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- The other create options, `env`, `labels`, `secrets`, `ports`, and `network_policy`, work as on [Linux sandboxes](/docs/linux-sandboxes#create-a-linux-sandbox).
</Callout>

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.createMacos('base-macos', { region: 'macos-1' })
```

The following example creates two sandboxes of different sizes on one reservation:

```typescript
const r = await daytona.v2.reservations.create({
  region: 'macos-1', model: 'm4pro-12c-24g', idempotencyKey: 'shared-mac-1',
})
const a = await daytona.v2.createMacos('base-macos', {
  region: 'macos-1', reservation: r.id, resources: { cpu: 4, memory: 8, disk: 100 },
})
const b = await daytona.v2.createMacos('base-macos', {
  region: 'macos-1', reservation: r.id, resources: { cpu: 8, memory: 16, disk: 120 },
})
```

The following example creates a persistent sandbox that uses the whole Mac:

```typescript
const r = await daytona.v2.reservations.create({
  region: 'macos-1', model: 'm4pro-12c-24g', idempotencyKey: 'ios-builder-mac-1',
})
const sandbox = await daytona.v2.createMacos('base-macos', {
  region: 'macos-1',
  reservation: r.id,
  resources: { cpu: 12, memory: 24, disk: 220 },
  persistent: true,
  name: 'ios-builder',
  waitSeconds: 120,
})
console.log(sandbox.id, sandbox.reservationId)
```

| **Parameter**              | **Type** | **Description**                                                                                                                       |
| -------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `image`                | string   | Sets the macOS image that the sandbox starts from, for example `base-macos` or a snapshot. This is the first positional argument. |
| `region`               | string   | Sets the region, `macos-1`.                                                                                                       |
| `reservation`          | string   | Sets the reservation that the sandbox runs on: a reservation ID, or `'new'`. See [which reservation](#which-reservation).         |
| `autoRenew`            | boolean  | Sets [automatic renewal](#automatic-renewal) of the reservation that the create buys. Only with `reservation: 'new'`.             |
| `resources`            | MacosResources | Sets the size: `cpu`, the number of CPU cores; `memory`, the memory in GiB; and `disk`, the whole guest disk in GiB, macOS included. See [sizes](#sizes). |
| `persistent`           | boolean  | Keeps the disk across stop and start. See [filesystem persistence](#filesystem-persistence).                                          |
| `name`                 | string   | Sets the display name.                                                                                                                |
| `waitSeconds`          | number   | Sets how many seconds the call waits for the sandbox to start.                                                                        |
| `createTimeoutSeconds` | number   | Sets how many seconds the server tries before it deletes the waiting sandbox.                                                         |

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- The other create options, `env`, `labels`, `secrets`, `ports`, and `networkPolicy`, work as on [Linux sandboxes](/docs/linux-sandboxes#create-a-linux-sandbox).
</Callout>

</TabItem>
<TabItem label="CLI" icon="seti:shell">

```bash
daytona sandbox create-macos --region macos-1 --image base-macos
```

The following example creates two sandboxes of different sizes on one reservation:

```bash
daytona sandbox create-macos --region macos-1 --image base-macos --reservation RESERVATION_ID \
  --cores 4 --memory 8192 --disk 102400
daytona sandbox create-macos --region macos-1 --image base-macos --reservation RESERVATION_ID \
  --cores 8 --memory 16384 --disk 122880
```

The following example creates a persistent sandbox that uses the whole Mac:

```bash
daytona sandbox create-macos --region macos-1 --image base-macos --reservation RESERVATION_ID \
  --cores 12 --memory 24576 --disk 225280 --persistent --name ios-builder
```

| **Flag**            | **Type** | **Description**                                                                                                         |
| ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- |
| `--image`       | string   | Sets the macOS image that the sandbox starts from, for example `base-macos` or a snapshot.                          |
| `--region`      | string   | Sets the region, `macos-1`.                                                                                         |
| `--reservation` | string   | Sets the reservation that the sandbox runs on: a reservation ID, or `new`. See [which reservation](#which-reservation). |
| `--auto-renew`  | bool     | Sets [automatic renewal](#automatic-renewal) of the reservation that the create buys. Only with `--reservation new`. |
| `--cores`       | int32    | Sets the number of CPU cores. See [sizes](#sizes).                                                                      |
| `--memory`      | int32    | Sets the memory in MiB. See [sizes](#sizes).                                                                            |
| `--disk`        | int32    | Sets the whole guest disk in MiB, macOS included. See [sizes](#sizes).                                                  |
| `--persistent`  | bool     | Keeps the disk across stop and start. See [filesystem persistence](#filesystem-persistence).                            |
| `--name`        | string   | Sets the display name.                                                                                                  |
| `--server-wait` | duration | Sets how long the server holds the create response for the sandbox to start, as a Go duration. `0s` answers at once.    |
| `--create-ttl`  | duration | Sets how long the server tries before it deletes the waiting sandbox, as a Go duration.                                 |

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- Other create options work as on [Linux sandboxes](/docs/linux-sandboxes#create-a-linux-sandbox), including the network flags `--block-all`, `--allow-domain`, and `--allow-cidr`.
</Callout>

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.us-macos.daytona.com/sandboxes/macos' \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --data '{ "image": "base-macos", "region_id": "macos-1" }'
```

The following example creates two sandboxes of different sizes on one reservation:

```bash
curl 'https://api.us-macos.daytona.com/sandboxes/macos' \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --data '{ "image": "base-macos", "region_id": "macos-1", "reservation": "RESERVATION_ID",
            "cores": 4, "memory_mib": 8192, "disk_mib": 102400 }'
curl 'https://api.us-macos.daytona.com/sandboxes/macos' \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --data '{ "image": "base-macos", "region_id": "macos-1", "reservation": "RESERVATION_ID",
            "cores": 8, "memory_mib": 16384, "disk_mib": 122880 }'
```

The following example creates a persistent sandbox that uses the whole Mac:

```bash
curl 'https://api.us-macos.daytona.com/sandboxes/macos' \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Idempotency-Key: ios-builder-create-1' \
  --data '{
  "image": "base-macos",
  "region_id": "macos-1",
  "reservation": "RESERVATION_ID",
  "cores": 12,
  "memory_mib": 24576,
  "disk_mib": 225280,
  "persistent": true,
  "name": "ios-builder",
  "wait_seconds": 120
}'
```

| **Field**                | **Type** | **Description**                                                                                                                  |
| ------------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `image`                  | string   | Sets the macOS image that the sandbox starts from, by name or `img_` ID, for example `base-macos` or a snapshot. This field is required. |
| `region_id`              | string   | Sets the region, `macos-1`.                                                                                                      |
| `reservation`            | string   | Sets the reservation that the sandbox runs on: a reservation ID, or `new`. See [which reservation](#which-reservation).          |
| `auto_renew`             | boolean  | Sets [automatic renewal](#automatic-renewal) of the reservation that the create buys. Only with `"reservation": "new"`.          |
| `cores`                  | integer  | Sets the number of CPU cores. See [sizes](#sizes).                                                                               |
| `memory_mib`             | integer  | Sets the memory in MiB. See [sizes](#sizes).                                                                                     |
| `disk_mib`               | integer  | Sets the whole guest disk in MiB, macOS included. See [sizes](#sizes).                                                           |
| `persistent`             | boolean  | Keeps the disk across stop and start. See [filesystem persistence](#filesystem-persistence).                                     |
| `name`                   | string   | Sets the display name.                                                                                                           |
| `wait_seconds`           | integer  | Sets how many seconds the server holds the request for the sandbox to start. `0` returns immediately. The default is 3.          |
| `create_timeout_seconds` | integer  | Sets how many seconds the server tries before it deletes the waiting sandbox.                                                    |
| `idempotency_key`        | string   | Makes a retried create return the same sandbox and never buy a second reservation. You can also send it as the `Idempotency-Key` header. |

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- The other create fields, `env`, `labels`, `secrets`, `ports`, and `network_policy`, work as on [Linux sandboxes](/docs/linux-sandboxes#create-a-linux-sandbox).
</Callout>

</TabItem>
</Tabs>

### Sizes [#sizes]

A macOS sandbox takes its cores, memory and disk from the Mac of its reservation. The disk is the whole guest disk, macOS included. On the M4 Pro [model](#models-and-prices), the sandboxes of one Mac share 12 cores, 24 GiB memory and 220 GiB disk. Up to two sandboxes run on one Mac at the same time, and their sizes must fit in the Mac together. For example, one sandbox of 12 cores, 24 GiB memory and 220 GiB disk uses the whole Mac, and a sandbox of 4 cores, 8 GiB and 100 GiB fits beside a sandbox of 8 cores, 16 GiB and 120 GiB. A Mac fixes the size when it accepts the sandbox, and the sandbox reports that size from then on.

| **Setting** | **SDK (GiB)**     | **CLI (MiB)**  | **API (MiB)**      | **Minimum**        | **Maximum on `m4pro-12c-24g`** | **If you omit it, on `m4pro-12c-24g`** |
| ----------- | ----------------- | -------------- | ------------------ | ------------------ | ------------------------------ | -------------------------------------- |
| Cores       | **`resources.cpu`** (cores) | **`--cores`**  | **`cores`**        | 4                  | 12                             | 6                                      |
| Memory      | **`resources.memory`**      | **`--memory`** | **`memory_mib`**   | 4 GiB (4096 MiB)   | 24 GiB (24576 MiB)             | 12 GiB (12288 MiB)                     |
| Disk        | **`resources.disk`**        | **`--disk`**   | **`disk_mib`**     | 80 GiB (81920 MiB) | 220 GiB (225280 MiB)           | 110 GiB (112640 MiB)                   |

### Which reservation [#which-reservation]

A create places the sandbox on a reservation. The reservation option decides which one, and whether the create buys a new one.

| **Reservation option**   | **Where the sandbox goes**                                                                                                                       |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Omitted                  | Any of your reservations in the region that has room. If you have none, [auto-reserve](#auto-reserve) buys one (if it is on).                    |
| `reservation="<id>"`     | That reservation.                                                                                                                                |
| `reservation="new"`      | A new reservation that the create always buys for this sandbox. Only here can you also set `auto_renew`.                                         |

The create can wait for room on a reservation. Two options set how long: in seconds in the SDKs, and as a Go duration in the CLI. If the time runs out, a reservation that the create bought stays bought.

| **Option**                                                     | **Description**                                                      |
| -------------------------------------------------------------- | -------------------------------------------------------------------- |
| `wait_seconds` (`waitSeconds`, `--server-wait`)                | How long the call waits for the sandbox to start.                    |
| `create_timeout_seconds` (`createTimeoutSeconds`, `--create-ttl`) | How long the server tries before it deletes the waiting sandbox. |

### Get a sandbox [#get-a-sandbox]

Get a macOS sandbox by its ID, or list the sandboxes in the macOS region.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
sandbox = daytona.v2.get("SANDBOX_ID")
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
const sandbox = await daytona.v2.get('SANDBOX_ID')
```

</TabItem>
<TabItem label="CLI" icon="seti:shell">

```bash
daytona sandbox get SANDBOX_ID
daytona sandbox list --region macos-1
```

</TabItem>
</Tabs>

## Mac reservations [#mac-reservations]

A reservation gives your organization one dedicated Mac for a 24-hour period. Nobody else uses that Mac while you hold it. You pay for each period when it starts. The first period starts when a Mac accepts the reservation, not when you send the request, and time spent waiting for a Mac is not charged.

You cannot end a reservation early. It ends at the end of its period unless it renews. macOS sandboxes run only on your reservations: a create picks a reservation with room left, or you name one. Up to two sandboxes run on the Mac at the same time, and you choose the [size](#sizes) of each sandbox when you create it.

<Collapsible title="Reservation statuses">

| **Status**           | **Description**                                                                                                                                       |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pending_charge` | We are charging the period.                                                                                                                           |
| `pending_mac`    | The period is paid. The reservation waits for an available Mac. If no Mac accepts it within 5 minutes, it becomes `failed` and we refund the charge.     |
| `active`         | A Mac holds the reservation. `period_end` tells you when the period ends.                                                                         |
| `ending`         | The period ended. We stop the sandboxes, save persistent disks and erase the Mac. See [when a reservation ends](#when-a-reservation-ends).           |
| `released`       | The reservation ended. `end_reason` tells you why, for example `expired`.                                                                     |
| `failed`         | The charge failed, or no Mac accepted the reservation (refunded).                                                                                     |

</Collapsible>

### Models and prices [#models-and-prices]

The production Macs are **Apple M4 Pro, 12 cores, 24 GB memory**. Always give this model when you reserve. Its model ID is in the table below. The price of one reservation of this model is **$2.5032 per Mac-hour, $60.08 per 24-hour period**.

| **Model**             | **Cores** | **Memory** | **Sandbox disk** | **Sandboxes at a time** |
| --------------------- | --------- | ---------- | ---------------- | ----------------------- |
| **`m4pro-12c-24g`**   | 12        | 24 GiB     | 220 GiB          | Up to 2                 |

The sizes of the sandboxes on one Mac must fit in its cores, memory and sandbox disk together. See [sizes](#sizes).

Read the current price and the number of available Macs from the Mac catalog. An available Mac is one that nobody has reserved. The number is a guide only: another purchase can reserve an available Mac before yours.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
catalog = daytona.v2.mac_models.catalog(region="macos-1")
for region in catalog.regions:
    for entry in region.models:
        price = entry.price
        print(
            entry.model.id,
            "available:", entry.available,
            "per period: $%.2f" % (price.period_cents / 100) if price else "no price",
        )

# Model specs only; the catalog adds prices and available Macs per region
for model in daytona.v2.mac_models.list():
    print(model.id, model.cores, model.memory_mib, model.max_sandboxes)
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
const catalog = await daytona.v2.macModels.catalog({ region: 'macos-1' })
for (const region of catalog.regions) {
  for (const entry of region.models) {
    const perPeriod = entry.price ? `$${(entry.price.periodCents / 100).toFixed(2)}` : 'no price'
    console.log(entry.model.id, 'available:', entry.available, 'per period:', perPeriod)
  }
}

// Model specs only; the catalog adds prices and available Macs per region
for (const model of await daytona.v2.macModels.list()) {
  console.log(model.id, model.cores, model.memoryMib, model.maxSandboxes)
}
```

</TabItem>
<TabItem label="CLI" icon="seti:shell">

```bash
daytona mac-model list --region macos-1
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
# Catalog: available Macs and prices per model; region_ids is a comma-separated list, all regions by default
curl 'https://api.us-macos.daytona.com/mac-catalog?region_ids=macos-1' \
  --header 'Authorization: Bearer YOUR_API_KEY'

# Model specs only; the catalog adds prices and available Macs per region
curl 'https://api.us-macos.daytona.com/mac-models' \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

| **Field**              | **Description**                                                                                     |
| ---------------------- | --------------------------------------------------------------------------------------------------- |
| `default_model`    | The model a reservation in the region gets when it names none.                                      |
| `available`        | The number of Macs of the model that nobody has reserved. A guide only; another purchase can reserve one first.     |
| `price`            | `period_cents`, `cents_per_hour`, `period_seconds` and `currency`. `null` where billing is not enabled. |
| `max_sandboxes`    | The most sandboxes a Mac of the model runs at once.                                                 |
| `sandbox_disk_mib` | The disk room for all sandboxes on one Mac, stopped sandboxes included.                             |

</TabItem>
</Tabs>

### Reserve a Mac [#reserve-a-mac]

Reserve one Mac of a model in a region. Each purchase has an idempotency key. If you retry with the same key, you get the same reservation and you do not pay again. If you do not give a key, the SDK and the CLI make a new key for each call, so a retry buys a second Mac. Give your own key when your code can retry.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
reservation = daytona.v2.reservations.create(
    region="macos-1",
    model="m4pro-12c-24g",
    idempotency_key="build-mac-2026-10-24",
)
print(reservation.id, reservation.status)
```

| **Parameter**     | **Type** | **Description**                                                                                                  |
| ----------------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
| `region`          | str      | Sets the region, `macos-1`. This parameter is required.                                                          |
| `model`           | str      | Sets the Mac [model](#models-and-prices), `m4pro-12c-24g`.                                                       |
| `idempotency_key` | str      | Sets the idempotency key. Defaults to a new key for each call.                                                   |
| `auto_renew`      | bool     | Sets [automatic renewal](#automatic-renewal). If you omit it, it follows your organization's auto-reserve setting, which is on by default. |

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
const reservation = await daytona.v2.reservations.create({
  region: 'macos-1',
  model: 'm4pro-12c-24g',
  idempotencyKey: 'build-mac-2026-10-24',
})
console.log(reservation.id, reservation.status)
```

| **Parameter**    | **Type** | **Description**                                                                                                  |
| ---------------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
| `region`         | string   | Sets the region, `macos-1`. This parameter is required.                                                          |
| `model`          | string   | Sets the Mac [model](#models-and-prices), `m4pro-12c-24g`.                                                       |
| `idempotencyKey` | string   | Sets the idempotency key. Defaults to a new key for each call.                                                   |
| `autoRenew`      | boolean  | Sets [automatic renewal](#automatic-renewal). If you omit it, it follows your organization's auto-reserve setting, which is on by default. |

</TabItem>
<TabItem label="CLI" icon="seti:shell">

```bash
daytona reservation create --region macos-1 --model m4pro-12c-24g \
  --idempotency-key build-mac-2026-10-24
```

| **Flag**            | **Type** | **Description**                                                                                                  |
| ------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
| `--region`          | string   | Sets the region, `macos-1`.                                                                                      |
| `--model`           | string   | Sets the Mac [model](#models-and-prices), `m4pro-12c-24g`.                                                       |
| `--idempotency-key` | string   | Sets the idempotency key. Defaults to a new key for each invocation.                                            |
| `--auto-renew`      | bool     | Sets [automatic renewal](#automatic-renewal). If you omit it, it follows your organization's auto-reserve setting, which is on by default. |

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.us-macos.daytona.com/reservations' \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Idempotency-Key: build-mac-2026-10-24' \
  --data '{ "region_id": "macos-1", "model": "m4pro-12c-24g" }'
```

| **Field**         | **Type**  | **Description**                                                                                                  |
| ----------------- | --------- | ---------------------------------------------------------------------------------------------------------------- |
| `Idempotency-Key` | header    | Sets the idempotency key. This header is required; the API does not generate a key.                             |
| `region_id`       | string  | Sets the region, `macos-1`. This field is required.                                                              |
| `model`           | string  | Sets the Mac [model](#models-and-prices), `m4pro-12c-24g`.                                                       |
| `auto_renew`      | boolean | Sets [automatic renewal](#automatic-renewal). If you omit it, it follows your organization's auto-reserve setting, which is on by default. |

</TabItem>
</Tabs>

### Reserve many Macs [#reserve-many-macs]

Reserve 1 to 100 Macs of one model in one request. Each Mac is a separate reservation with its own ID, period, charge and renewal.

By default, the request is all or nothing: if fewer Macs are available than you ask for, it reserves none and fails because no Mac is available. Set a minimum count to accept fewer: the request then reserves as many as are available, if that is at least the minimum.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
batch = daytona.v2.reservations.create_bulk(
    region="macos-1",
    model="m4pro-12c-24g",
    count=20,
    min_count=10,
    idempotency_key="ci-fleet-2026-10-24",
)
print(f"reserved {batch.reserved} of {batch.requested} Macs")
ids = [r.id for r in batch.reservations]
```

| **Parameter**     | **Type** | **Description**                                                         |
| ----------------- | -------- | ----------------------------------------------------------------------- |
| `count`           | int      | Sets how many Macs to reserve, 1 to 100.                                |
| `min_count`       | int      | Sets the fewest Macs to accept when fewer than the count are available. Defaults to the count, all or nothing. |
| `idempotency_key` | str      | Sets the idempotency key. Defaults to a new key for each call.          |

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
const batch = await daytona.v2.reservations.createBulk({
  region: 'macos-1',
  model: 'm4pro-12c-24g',
  count: 20,
  minCount: 10,
  idempotencyKey: 'ci-fleet-2026-10-24',
})
console.log(`reserved ${batch.reserved} of ${batch.requested} Macs`)
```

| **Parameter**    | **Type** | **Description**                                                         |
| ---------------- | -------- | ----------------------------------------------------------------------- |
| `count`          | number   | Sets how many Macs to reserve, 1 to 100.                                |
| `minCount`       | number   | Sets the fewest Macs to accept when fewer than the count are available. Defaults to the count, all or nothing. |
| `idempotencyKey` | string   | Sets the idempotency key. Defaults to a new key for each call.          |

</TabItem>
<TabItem label="CLI" icon="seti:shell">

A dry run shows the price and the available Macs and reserves nothing.

```bash
daytona reservation create --region macos-1 --model m4pro-12c-24g --count 20 --dry-run
daytona reservation create --region macos-1 --model m4pro-12c-24g --count 20 --min-count 10 \
  --idempotency-key ci-fleet-2026-10-24
```

| **Flag**            | **Type** | **Description**                                                         |
| ------------------- | -------- | ----------------------------------------------------------------------- |
| `--count`           | int      | Sets how many Macs to reserve, 1 to 100.                                |
| `--min-count`       | int      | Sets the fewest Macs to accept when fewer than the count are available. Defaults to the count, all or nothing. |
| `--idempotency-key` | string   | Sets the idempotency key. Defaults to a new key for each invocation.    |
| `--dry-run`         | bool     | Prints the price and the available Macs and reserves nothing.           |

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.us-macos.daytona.com/reservations/bulk' \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Idempotency-Key: ci-fleet-2026-10-24' \
  --data '{ "region_id": "macos-1", "model": "m4pro-12c-24g", "count": 20, "min_count": 10 }'
```

| **Field**         | **Type**  | **Description**                                                         |
| ----------------- | --------- | ----------------------------------------------------------------------- |
| `Idempotency-Key` | header    | Sets the idempotency key. This header is required; the API does not generate a key. |
| `count`           | integer | Sets how many Macs to reserve, 1 to 100.                                |
| `min_count`       | integer | Sets the fewest Macs to accept when fewer than the count are available. Defaults to the count, all or nothing. |

</TabItem>
</Tabs>

### List reservations [#list-reservations]

List the reservations of your organization, one page at a time. The list returns the open reservations and the reservations that ended in the last 30 days.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
cursor = None
while True:
    page = daytona.v2.reservations.list(limit=100, cursor=cursor)
    for r in page.items:
        print(r.id, r.status, r.model, r.period_end, r.auto_renew)
    cursor = page.next_cursor  # None on the last page
    if cursor is None:
        break
```

| **Parameter** | **Type** | **Description**                                                                    |
| ------------- | -------- | ---------------------------------------------------------------------------------- |
| `limit`       | int      | Sets the page size, from `1` to `200`. The default is `100`.        |
| `cursor`      | str      | Resumes the list after the previous page. Pass the `next_cursor` of that page.                            |

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
let cursor: string | undefined
do {
  const page = await daytona.v2.reservations.list({ limit: 100, cursor })
  for (const r of page.items) {
    console.log(r.id, r.status, r.model, r.periodEnd, r.autoRenew)
  }
  cursor = page.nextCursor // undefined on the last page
} while (cursor)
```

| **Parameter** | **Type** | **Description**                                                                    |
| ------------- | -------- | ---------------------------------------------------------------------------------- |
| `limit`       | number   | Sets the page size, from `1` to `200`. The default is `100`.        |
| `cursor`      | string   | Resumes the list after the previous page. Pass the `nextCursor` of that page.                             |

</TabItem>
<TabItem label="CLI" icon="seti:shell">

```bash
daytona reservation list
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
# Paginated; follow next_cursor for more rows
curl 'https://api.us-macos.daytona.com/reservations?limit=100' \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

| **Field**    | **Type**      | **Description**                                                                        |
| ------------ | ------------- | -------------------------------------------------------------------------------------- |
| `limit`  | integer | Sets the maximum number of items, from `1` to `200`. The default is `100`. |
| `cursor` | string  | Pass the `next_cursor` value of the previous response to read the next page.       |

</TabItem>
</Tabs>

#### Iterate over all reservations [#iterate-over-all-reservations]

Iterate over every reservation of your organization without handling page cursors. The iterator fetches the next page when the current one is exhausted and stops when the server returns no cursor.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
for r in daytona.v2.reservations.iter():
    print(r.id, r.status, r.model, r.period_end, r.auto_renew)
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
for await (const r of daytona.v2.reservations.iter()) {
  console.log(r.id, r.status, r.model, r.periodEnd, r.autoRenew)
}
```

</TabItem>
</Tabs>

### Get a reservation [#get-a-reservation]

Get one reservation of your organization by its ID. The response has the same fields as an item of the list, including its current status and the end of its period.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
r = daytona.v2.reservations.get("RESERVATION_ID")
print(r.id, r.status, r.period_end, r.auto_renew)
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
const r = await daytona.v2.reservations.get('RESERVATION_ID')
console.log(r.id, r.status, r.periodEnd, r.autoRenew)
```

</TabItem>
<TabItem label="CLI" icon="seti:shell">

```bash
daytona reservation get RESERVATION_ID
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.us-macos.daytona.com/reservations/RESERVATION_ID' \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

</TabItem>
</Tabs>

### Renew a reservation [#renew-a-reservation]

Renew a reservation to pay for the next 24-hour period on the same Mac. You keep the Mac, its sandboxes and their disks. Use a new key for each period you want to buy, and the same key when you retry.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
daytona.v2.reservations.renew(reservation.id, idempotency_key=f"renew-{reservation.id}-2")
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
await daytona.v2.reservations.renew(reservation.id, { idempotencyKey: `renew-${reservation.id}-2` })
```

</TabItem>
<TabItem label="CLI" icon="seti:shell">

```bash
daytona reservation renew RESERVATION_ID --idempotency-key renew-RESERVATION_ID-2
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.us-macos.daytona.com/reservations/RESERVATION_ID/renew' \
  --request POST \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Idempotency-Key: renew-RESERVATION_ID-2'
```

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- The `Idempotency-Key` header is required.
- The API does not generate an idempotency key.
</Callout>

</TabItem>
</Tabs>

### Automatic renewal [#automatic-renewal]

When automatic renewal is on, we renew the reservation in the last hour of its period, **but only while a sandbox on that Mac is active** (being created, starting or running). If all its sandboxes are stopped or paused, or it has none, the reservation does not renew and ends at the end of its period. If a renewal fails, the reservation records the reason and ends at the end of its period.

Turn automatic renewal on or off for one reservation.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
daytona.v2.reservations.update(reservation.id, auto_renew=False)
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
await daytona.v2.reservations.update(reservation.id, { autoRenew: false })
```

</TabItem>
<TabItem label="CLI" icon="seti:shell">

```bash
daytona reservation update RESERVATION_ID --auto-renew=false
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.us-macos.daytona.com/reservations/RESERVATION_ID' \
  --request PATCH \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --data '{ "auto_renew": false }'
```

</TabItem>
</Tabs>

### Auto-reserve [#auto-reserve]

Auto-reserve buys a Mac for you when you create a macOS sandbox and your organization holds no live reservation in the region. It buys the cheapest model that fits the sandbox, with automatic renewal on. If you want to control every purchase, turn auto-reserve off and reserve Macs yourself.

- It is **on by default**. It holds at most **2** reservations at a time. Above that, a create fails because the auto-reserve limit is reached.
- It does not buy a Mac when you have a reservation that is only full: no free slot, or too little disk left.
- An organization admin can turn it off, or change the limit, in the dashboard: Settings, **Auto-reserve Macs**, or through the [macOS settings](#macos-settings).

### macOS settings [#macos-settings]

The macOS settings control [auto-reserve](#auto-reserve) for macOS sandboxes. With auto-reserve on, a macOS create in a region where the organization holds no live Mac reservation buys a 24-hour reservation that renews while a sandbox runs on it. Auto-reserve is on by default, and the organization caps how many auto-reserved reservations it holds at once.

#### Get the settings [#get-the-settings]

Get the organization's macOS settings.

<Tabs syncKey="language">
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.daytona.com/orgs/ORGANIZATION_ID/macos-settings' \
  --header 'Authorization: Bearer YOUR_OIDC_TOKEN'
```

```json
{
  "macos_auto_reserve": true,
  "macos_auto_reserve_max_reservations": 2,
  "macos_auto_reserve_disabled_at": null,
  "macos_auto_reserve_disabled_by": null
}
```

The response has the following fields:

| **Field**                              | **Description**                                                                                     |
| -------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `macos_auto_reserve`                   | Shows whether auto-reserve is on.                                                                   |
| `macos_auto_reserve_max_reservations`  | The most auto-reserved reservations the organization holds at once.                                 |
| `macos_auto_reserve_disabled_at`       | Records when auto-reserve was last turned off. The value is `null` while auto-reserve is on.        |
| `macos_auto_reserve_disabled_by`       | Records who last turned auto-reserve off. The value is `null` while auto-reserve is on.             |

</TabItem>
</Tabs>

#### Set the settings [#set-the-settings]

Turn auto-reserve on or off, or change the cap on auto-reserved reservations. Fields left out of the request keep their stored values.

<Tabs syncKey="language">
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.daytona.com/orgs/ORGANIZATION_ID/macos-settings' \
  --request PATCH \
  --header 'Authorization: Bearer YOUR_OIDC_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{ "macos_auto_reserve": true, "macos_auto_reserve_max_reservations": 4 }'
```

| **Field**                              | **Type** | **Description**                                                                       |
| -------------------------------------- | -------- | ------------------------------------------------------------------------------------- |
| `macos_auto_reserve`                   | boolean  | Turns auto-reserve on or off. If omitted, the stored value stays unchanged. Turning auto-reserve off while it is already off keeps the original `macos_auto_reserve_disabled_at` and `macos_auto_reserve_disabled_by`. Turning auto-reserve on clears both fields. |
| `macos_auto_reserve_max_reservations`  | integer  | Sets the most auto-reserved reservations the organization holds at once. If omitted, the stored value stays unchanged. The value must be at least `1`. A cap below `1` returns `400`. |

<Callout type="note" title="Notes">
- The request requires the `write:organization` permission.
- Set at least one field in the request.
</Callout>

</TabItem>
</Tabs>

### When a reservation ends [#when-a-reservation-ends]

A reservation ends at the end of its period when it does not renew. There is no grace period, and we do not send a notice. Watch the end of the period, the automatic renewal setting and any renewal error on your reservations, or [renew by hand](#renew-a-reservation).

At the end, in this order:

1. The reservation enters the ending status. You cannot start, resume or create sandboxes on it.
2. **Persistent sandboxes** are shut down (paused ones are resumed first, then shut down). Each one that ran is **exported as an image**: an image without a name that records the sandbox's ID.
3. **Sandboxes that are not persistent** are deleted. Their data is lost.
4. When every export is saved or has failed, the Mac is **erased**: the sandbox disks, the console logs, the macOS user and its files. A failed export holds the erase for up to 24 hours. The disk of that sandbox is lost.
5. The Mac is checked and goes back to the pool for the next customer.
6. The reservation enters the released status. Every sandbox on it is deleted.

#### What you keep [#what-you-keep]

| **Data**                          | **After the end**                                                                                                                                  |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| Disk of a persistent sandbox      | Kept as an image without a name, unless its export fails. It is deleted when no sandbox is created from it for 30 days. Give it a name to keep it. |
| Snapshots you made                | Kept. Named ones until you delete them. Unnamed ones while a sandbox is created from them at least once every 7 days.                              |
| Usage records and bills           | Kept.                                                                                                                                              |
| Sandboxes that are not persistent | Lost.                                                                                                                                              |
| Memory of paused sandboxes        | Lost. The disk is exported.                                                                                                                        |
| Console logs                      | Lost.                                                                                                                                              |
| The Mac                           | Erased.                                                                                                                                            |

#### Continue after the end [#continue-after-the-end]

Find the exported image, then create a sandbox from it on a reservation. You can also give the image a name to keep it. The new sandbox needs a reservation with room for the disk: an existing one, a new one you buy, or one that auto-reserve buys. It gets a new machine identity.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
exports = daytona.v2.images.list(anonymous=True, sandbox_id="OLD_SANDBOX_ID")
image = exports.items[0]
daytona.v2.images.promote(image.id, "ios-builder-day-2")   # optional: keep it
sandbox = daytona.v2.create_macos(image.id, region="macos-1", persistent=True, wait_seconds=120)
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
const exports = await daytona.v2.images.list({ anonymous: true, sandboxId: 'OLD_SANDBOX_ID' })
const image = exports.items[0]
await daytona.v2.images.promote(image.id, 'ios-builder-day-2') // optional: keep it
const sandbox = await daytona.v2.createMacos(image.id, { region: 'macos-1', persistent: true, waitSeconds: 120 })
```

</TabItem>
<TabItem label="CLI" icon="seti:shell">

```bash
daytona image list --anonymous true --sandbox OLD_SANDBOX_ID
daytona image promote IMAGE_ID ios-builder-day-2
daytona sandbox create-macos --region macos-1 --image IMAGE_ID --persistent
```

</TabItem>
</Tabs>

## Filesystem snapshot [#filesystem-snapshot]

A filesystem snapshot saves the disk of a stopped persistent macOS sandbox as an image. You can create new sandboxes from it on any Mac of the region. Snapshots save the disk only, not the memory.

A new sandbox from an image gets a new machine identity. Apps that are bound to one machine (for example an iCloud sign-in or some licences) need you to sign in again.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
sandbox.stop()
image = sandbox.capture_image(name="ios-builder-ready")
image = daytona.v2.images.get(image.id, wait="ready", timeout=3600)
print(image.id, image.status)

copy = daytona.v2.create_macos("ios-builder-ready", region="macos-1", persistent=True)
```

<Callout type="note" title="Notes">
- The requests require the `write:images` and `write:sandboxes` permissions.
- The sandbox must be persistent and stopped.
- The sandbox must have an active reservation.
- The sandbox cannot start until the snapshot is `ready`.
</Callout>

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
await sandbox.stop()
const image = await sandbox.captureImage({ name: 'ios-builder-ready' })
const ready = await daytona.v2.images.get(image.id, { wait: 'ready', timeoutSeconds: 3600 })
const copy = await daytona.v2.createMacos('ios-builder-ready', { region: 'macos-1', persistent: true })
```

<Callout type="note" title="Notes">
- The requests require the `write:images` and `write:sandboxes` permissions.
- The sandbox must be persistent and stopped.
- The sandbox must have an active reservation.
- The sandbox cannot start until the snapshot is `ready`.
</Callout>

</TabItem>
<TabItem label="CLI" icon="seti:shell">

```bash
daytona sandbox stop SANDBOX_ID
daytona image capture SANDBOX_ID --name ios-builder-ready
daytona image list --name ios-builder-ready
daytona sandbox create-macos --region macos-1 --image ios-builder-ready --persistent
```

<Callout type="note" title="Notes">
- The requests require the `write:images` and `write:sandboxes` permissions.
- The sandbox must be persistent and stopped.
- The sandbox must have an active reservation.
- The sandbox cannot start until the snapshot is `ready`.
</Callout>

</TabItem>
</Tabs>

## Filesystem persistence [#filesystem-persistence]

A persistent macOS sandbox keeps its disk on its Mac across stop and start for as long as the reservation lives. To keep the disk after the reservation, make a [filesystem snapshot](#filesystem-snapshot), or let the end of the reservation save it (see [when a reservation ends](#when-a-reservation-ends)).

## Lifecycle [#lifecycle]

Every macOS sandbox moves through a lifecycle. Stop, start, pause and resume need a **persistent** sandbox. A sandbox that is not persistent can only run or be deleted.

If a Mac restarts, a running persistent sandbox comes back stopped with its disk. A running sandbox that is not persistent is lost.

| **Action** | **What happens**                                                                                                                                                                      |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Stop       | macOS shuts down. The disk stays on the same Mac and keeps its disk room on the reservation. The cores, memory and slot become available to other sandboxes.                                 |
| Start      | macOS boots from the same disk on the same Mac. The reservation needs an open slot and enough cores and memory for the sandbox, or start fails with `NO_FREE_SLOT`. Start never buys a reservation. |
| Pause      | The Mac saves the memory of the sandbox. It keeps its place on the reservation.                                                                                                       |
| Resume     | The sandbox continues from the saved memory. If the memory cannot be restored, macOS boots from the disk and `mac_boot_mode` is `cold_after_restore_failure`.                 |
| Delete     | The sandbox and its disk are erased. You cannot undo this.                                                                                                                            |

<Callout type="caution">
A stopped or paused sandbox **does not keep automatic renewal going**. If all sandboxes on a reservation are stopped or paused at the end of the period, the reservation ends. See [when a reservation ends](#when-a-reservation-ends).
</Callout>

### Stop / start [#stop--start]

Stop a persistent macOS sandbox to shut down macOS and keep its disk on the same Mac. Start boots macOS from the same disk on the same Mac.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
sandbox.stop()
sandbox.wait(until="stopped")
sandbox.start(timeout=120)

# Stop a sandbox by ID, without fetching it first
daytona.v2.stop("SANDBOX_ID")

# Start a stopped sandbox by ID
daytona.v2.start("SANDBOX_ID", timeout=120)
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
await sandbox.stop()
await sandbox.wait({ until: 'stopped' })
await sandbox.start({ timeoutSeconds: 120 })

// Stop a sandbox by ID, without fetching it first
await daytona.v2.stop('SANDBOX_ID')

// Start a stopped sandbox by ID
await daytona.v2.start('SANDBOX_ID', { timeoutSeconds: 120 })
```

</TabItem>
<TabItem label="CLI" icon="seti:shell">

```bash
daytona sandbox stop SANDBOX_ID
daytona sandbox start SANDBOX_ID
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.us-macos.daytona.com/sandboxes/{sandboxId}/stop' \
  --request POST \
  --header 'Authorization: Bearer YOUR_API_KEY'

# Long-poll until the sandbox is stopped
curl 'https://api.us-macos.daytona.com/sandboxes/{sandboxId}?wait=stopped&timeout_ms=60000' \
  --header 'Authorization: Bearer YOUR_API_KEY'

curl 'https://api.us-macos.daytona.com/sandboxes/{sandboxId}/start' \
  --request POST \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

</TabItem>
</Tabs>

### Pause / resume [#pause--resume]

Pause a persistent macOS sandbox to save its memory on the Mac. Resume continues the sandbox from the saved memory.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
sandbox.pause()
sandbox.resume()

# Pause a sandbox by ID, without fetching it first
daytona.v2.pause("SANDBOX_ID")

# Resume a paused sandbox by ID
daytona.v2.resume("SANDBOX_ID")
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
await sandbox.pause()
await sandbox.resume()

// Pause a sandbox by ID, without fetching it first
await daytona.v2.pause('SANDBOX_ID')

// Resume a paused sandbox by ID
await daytona.v2.resume('SANDBOX_ID')
```

</TabItem>
<TabItem label="CLI" icon="seti:shell">

```bash
daytona sandbox pause SANDBOX_ID
daytona sandbox resume SANDBOX_ID
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.us-macos.daytona.com/sandboxes/{sandboxId}/pause' \
  --request POST \
  --header 'Authorization: Bearer YOUR_API_KEY'

curl 'https://api.us-macos.daytona.com/sandboxes/{sandboxId}/resume' \
  --request POST \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

</TabItem>
</Tabs>

### Fork [#fork]

Fork a persistent macOS sandbox to copy its disk into a new sandbox. A fork does not copy memory, and a fork that asks for memory fails.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
twin = sandbox.fork(name="ios-builder-2")
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
const twin = await sandbox.fork({ name: 'ios-builder-2' })
```

</TabItem>
<TabItem label="CLI" icon="seti:shell">

```bash
daytona sandbox fork SANDBOX_ID --name ios-builder-2
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.us-macos.daytona.com/sandboxes/{sandboxId}/fork' \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --data '{ "name": "ios-builder-2" }'
```

| **Field** | **Type** | **Description**                                                  |
| --------- | -------- | ---------------------------------------------------------------- |
| `name`    | string   | Sets the display name of the fork. The default is `<source>-fork`. |

</TabItem>
</Tabs>

### Delete [#delete]

Delete a macOS sandbox to erase the sandbox and its disk. You cannot undo this.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
sandbox.delete()
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
await sandbox.delete()
```

</TabItem>
<TabItem label="CLI" icon="seti:shell">

```bash
daytona sandbox delete SANDBOX_ID
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.us-macos.daytona.com/sandboxes/{sandboxId}' \
  --request DELETE \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

</TabItem>
</Tabs>

## Run commands [#run-commands]

Run commands in a macOS sandbox, in the foreground or in the background. Files, git and PTYs work as on Linux sandboxes.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
result = sandbox.exec("sw_vers -productVersion && xcodebuild -version")
print(result.exit_code, result.output)

handle = sandbox.process.start("xcodebuild -scheme App build")  # runs in the background
print(handle.wait().exit_code)
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
const result = await sandbox.exec('sw_vers -productVersion && xcodebuild -version')
console.log(result.exitCode, result.output)
```

</TabItem>
<TabItem label="CLI" icon="seti:shell">

```bash
daytona sandbox exec SANDBOX_ID -- sw_vers -productVersion
daytona sandbox exec SANDBOX_ID --tty -- zsh        # an interactive shell
daytona sandbox cp ./App.zip SANDBOX_ID:/Users/lume/App.zip
```

</TabItem>
</Tabs>

## SSH [#ssh]

Connect to a macOS sandbox over SSH. The CLI opens SSH for you. With your own SSH client, make a short-lived sandbox token that allows SSH, and put the sandbox ID and the token in the user name, joined by a hash sign (#).

The gateway checks the token, not a password or an SSH key. A token works until it expires. Make a new one when you need it.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
token = sandbox.create_access_token(operations=["ssh"], ttl_seconds=3600)
print(f'ssh -p 2222 "{sandbox.id}#{token.access_token}@ssh.us-macos.daytona.com"')
```

<Callout type="caution">
Do not put a project API key in an SSH command. Use a sandbox token: it is limited to this sandbox and to SSH.
</Callout>

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
const token = await sandbox.createAccessToken({ operations: ['ssh'], ttlSeconds: 3600 })
console.log(`ssh -p 2222 "${sandbox.id}#${token.accessToken}@ssh.us-macos.daytona.com"`)
```

<Callout type="caution">
Do not put a project API key in an SSH command. Use a sandbox token: it is limited to this sandbox and to SSH.
</Callout>

</TabItem>
<TabItem label="CLI" icon="seti:shell">

```bash
daytona sandbox ssh SANDBOX_ID
daytona sandbox ssh SANDBOX_ID -- ls /Users/lume
```

</TabItem>
</Tabs>

## Desktop [#desktop]

macOS sandboxes have a desktop of 1280 × 800. Open the sandbox in the dashboard and choose the desktop view. The desktop takes mouse and keyboard input as soon as it connects.

### User and password [#user-and-password]

A macOS sandbox signs in to its macOS user when it starts, so the desktop opens without a login screen. macOS asks for the password of this user when you change a protected setting, for example when you allow an app under **Privacy & Security**, and when you unlock the screen. SSH and the desktop view do not use this password. Daytona checks your sandbox token instead.

The following table lists the user settings of the base image. A sandbox from a [filesystem snapshot](#filesystem-snapshot) keeps the user settings of the sandbox that the snapshot was taken from, including a changed password.

| **Property**     | **Value**                                                                                       |
| ---------------- | ----------------------------------------------------------------------------------------------- |
| User             | `lume`                                                                                          |
| Password         | `lume`                                                                                          |
| Administrator    | Yes                                                                                             |
| `sudo`           | Does not ask for a password                                                                     |
| Sign-in at start | Automatic                                                                                       |
| Screen lock      | Only when you lock the screen or the display sleeps. The sandbox does not sleep on its own.     |

### Computer use [#computer-use]

Take screenshots and send mouse and keyboard input inside the sandbox. Computer use can also read the accessibility tree, act on controls, and record the screen.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
import base64

cu = sandbox.computer_use
cu.start()
print(cu.status())

shot = cu.screen.screenshot()
with open("screen.png", "wb") as f:
    f.write(base64.b64decode(shot.screenshot))

cu.mouse.click(x=640, y=400)
cu.keyboard.type(text="hello from Daytona")
cu.keyboard.hotkey(keys="cmd+space")       # modifiers: cmd, ctrl, alt (option), shift
print(cu.display.windows())
```

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- `cu.a11y` reads the accessibility tree and acts on controls, and `cu.recording` records the screen.
</Callout>

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
const cu = sandbox.computerUse
await cu.start()
const shot = await cu.screen.screenshot()       // shot.screenshot is a base64 PNG
await cu.mouse.click({ x: 640, y: 400 })
await cu.keyboard.type({ text: 'hello from Daytona' })
await cu.keyboard.hotkey({ keys: 'cmd+space' })
```

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- `cu.a11y` reads the accessibility tree and acts on controls, and `cu.recording` records the screen.
</Callout>

</TabItem>
</Tabs>

### VNC client [#vnc-client]

Connect your own VNC client to the desktop. The desktop is also a VNC connection over a WebSocket. Make a token with the desktop operation and connect a noVNC client to the address below. The proxy does the VNC password step for you. Your client must support the DesktopSize encoding (noVNC does).

```text
wss://proxy.us-macos.daytona.app/vnc/SANDBOX_ID?daytona_token=TOKEN
```

```python
token = sandbox.create_access_token(operations=["desktop"], ttl_seconds=3600)
```

## Logs [#logs]

A macOS sandbox has a console log, and your own processes can keep their own logs. The console log is erased with the Mac when the reservation ends. Copy what you need before then.

| **Log**                   | **Description**                                                                                                                                                                       |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Console log**           | The macOS sandbox's console output and our lifecycle lines. The API route is `GET /sandboxes/SANDBOX_ID/console-logs`.                                                            |
| **Your own process logs** | Run a command with `process.start(...)` and read `handle.logs()` or `handle.stream_logs()` (TypeScript: `logs()`, `streamLogs()`), or write logs to a file and copy it out. |
| **Usage**                 | `daytona sandbox usage SANDBOX_ID`.                                                                                                                                               |

```bash
daytona sandbox logs SANDBOX_ID
daytona sandbox logs SANDBOX_ID --follow
```

## Errors [#errors]

The SDKs raise these errors with the codes in the following table. If something does not work as this page says, send us the sandbox ID or the reservation ID and the time. We read the server logs from those.

| **Code**                         | **Meaning**                                                                                                                                             | **What to do**                                                 |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| **`NO_ACTIVE_RESERVATION`**      | You have no live reservation in the region, and auto-reserve is off. Resume also returns it when the reservation of the paused sandbox is `ending`. | Reserve a Mac, or use `reservation="new"`.                 |
| **`NO_FREE_SLOT`**               | Your reservations have no room for this size.                                                                                                           | Use a smaller size, delete a sandbox, or reserve another Mac.  |
| **`MAC_DISK_FULL`**              | The Macs of the reservations with room for this size have too little disk space left. A smaller `disk` does not help.                              | Delete a macOS sandbox on one of them, or reserve another Mac. |
| **`NO_MAC_AVAILABLE`**           | No Mac of the model is available to reserve.                                                                                                                     | Try again later, or reserve fewer (`min_count`).           |
| **`AUTO_RESERVE_LIMIT_REACHED`** | Auto-reserve holds its maximum number of reservations.                                                                                                  | Reserve a Mac yourself, or ask an admin to raise the limit.    |
| **`IDEMPOTENCY_KEY_REUSED`**     | The key was used for a different purchase, or for a sandbox create with a different size.                                                               | Use a new key.                                                 |
| **`INSUFFICIENT_BALANCE`**       | Your balance does not cover the period of each Mac you reserve (402).                                                                                   | Add balance, or reserve fewer Macs.                            |
| **`CHARGE_FAILED`**              | The charge for the period failed (402).                                                                                                                 | Contact us.                                                    |
| **`SNAPSHOT_IN_PROGRESS`**       | A snapshot of this sandbox is in progress.                                                                                                              | Wait until the image is `ready`.                           |
| **`NOT_SUPPORTED_FOR_CLASS`**    | The action does not work on macOS, for example a fork with memory.                                                                                      | See the section for that action.                               |
| **`FEATURE_NOT_ENABLED`**        | macOS sandboxes are not on for your organization.                                                                                                       | Contact us.                                                    |

## Permissions [#permissions]

macOS sandboxes and Mac reservations use the sandbox permissions of their project, which you can grant to an [organization role](/docs/organizations#roles) or an [API key](/docs/api-keys). The macOS settings of the organization use organization permissions.

The [audit logs](/docs/audit-logs) record sandbox creates and deletes, and Mac reservations and renewals.

| **Permission**           | **Grants**                                                                                                                       |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| **`read:sandboxes`**     | Get sandboxes, and list and get Mac reservations                                                                                 |
| **`write:sandboxes`**    | Create, stop, start, pause, resume, and fork sandboxes, use the toolbox and SSH, and reserve, renew, and update Mac reservations |
| **`delete:sandboxes`**   | Delete sandboxes                                                                                                                 |
| **`write:images`**       | Capture filesystem snapshots as images                                                                                           |
| **`read:organization`**  | Get the macOS settings                                                                                                           |
| **`write:organization`** | Change the macOS settings                                                                                                        |
