> Fetch the index of every Tonbo docs page as Markdown at https://tonbo.dev/llms.txt

# Management API: Machines

Last updated 2 November 2026

> Machines are not open yet. These pages describe Machines as they will open; until then, a create answers [`region_not_offered`](https://tonbo.dev/docs/machines/troubleshooting/manage.md#region_not_offered) (422), “Machines are not offered in any region yet.”

The account's Machines (contracts/machines/machine-v2.json). Exec and file actions on a Machine are not offered yet. [The overview](https://tonbo.dev/docs/reference/management.md) covers what every operation shares: authentication, roles, errors, pagination, idempotency and If-Match.

- [`GET /machines`](#list-machines) List the account's Machines
- [`POST /machines`](#create-machine) Create a Machine
- [`GET /machines/{name}`](#get-machine) Read a Machine
- [`PATCH /machines/{name}`](#update-machine) Change a Machine's spec
- [`DELETE /machines/{name}`](#delete-machine) Delete a Machine
- [`POST /machines/{name}/sleep`](#sleep-machine) Put a Machine to sleep now
- [`GET /machines/{name}/activations`](#list-machine-activations) List a Machine's activations
- [`GET /machine-shapes`](#list-machine-shapes) List the Machine shapes
- [Objects](#objects): [Machine](#schema-machine) and [MachineShape](#schema-machine-shape)

## List the account's Machines

`GET /v1/machines` · CLI: `tonbo machine list`

Needs [`account.read`](https://tonbo.dev/docs/reference/management.md#capability-account-read), which `owner`, `admin` and `member` hold. A narrowed key may call it.

The account's Machines by name, except those being deleted: managed Machines and, in a personal account, its devices (kind `byoc`, which `PUT /machines/self` registers) from every region, whatever `machines.open` says; `unavailable_regions` names the regions whose Machines on the page could not be read. A device answers `id`, `name`, `kind`, `region`, `byoc` (with its `key_state`, derived from its current login key), `created_at` and `updated_at`; its `byoc.hostname` and `byoc.device_id` are answered to the device's person only, never to an Agent key. A narrowed key never reaches a Machine. An organization holds managed Machines only (contracts/machines/machine-v2.json `kinds_by_account_kind`), so its list never answers a `byoc` Machine or its fields, and its pages and cursors are those of its managed Machines alone.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | string | no | Opaque cursor tied to the original query shape. At least 1 characters. |
| `limit` | query | integer | no | The most items one page holds, 1 to 200; 50 when omitted. A page may hold fewer; read on with `next_cursor` until it is null. Default `50`. 1 to 200. |

### Response

| Status | Answer |
| --- | --- |
| `200` | Machines |

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | array of [Machine](https://tonbo.dev/docs/reference/management/machines.md#schema-machine) | yes |  |
| `next_cursor` | string or null | yes | Opaque cursor of the next page; null on the last page. |
| `unavailable_regions` | array of string | yes |  |

| Header | Status | Description |
| --- | --- | --- |
| `WWW-Authenticate` | 401 | Authentication scheme required by this API. |
| `Retry-After` | 422, 429 | Seconds before the client should retry. |

**Example 200 body**

```json
{
  "data": [
    {
      "id": "<id>",
      "name": "<name>",
      "kind": "managed",
      "region": "<region>",
      "byoc": {
        "os": "darwin",
        "arch": "arm64",
        "client": "yield",
        "client_version": "<client_version>",
        "harnesses": [
          "claude-code"
        ],
        "last_seen": "2026-11-02T09:30:00Z",
        "key_state": "active",
        "hostname": "<hostname>",
        "device_id": "<device_id>"
      },
      "hostnames": {
        "https": "<https>",
        "api": "<api>"
      },
      "image": {
        "reference": "<reference>",
        "repository": "<repository>",
        "digest": "<digest>",
        "state": "pulling",
        "failure": {
          "code": "image_unavailable",
          "message": "<message>"
        }
      },
      "command": [
        "<command>"
      ],
      "shape": "<shape>",
      "ports": [
        {
          "port": 1,
          "access": "private",
          "default": false
        }
      ],
      "env": {},
      "sleep_after_seconds": 600,
      "idle_action": "suspend",
      "stop_grace_seconds": 10,
      "database": {
        "name": "<name>",
        "url": "<url>"
      },
      "logs": {
        "url": "<url>"
      },
      "state": "creating",
      "failure": {
        "code": "<code>",
        "message": "<message>"
      },
      "status": {
        "power": "asleep",
        "snapshot": null,
        "sleep": {
          "action": "<action>",
          "reason": "<reason>"
        },
        "conditions": [
          {
            "type": "wake_blocked",
            "reason": "payment_required",
            "since": "2026-11-02T09:30:00Z",
            "retry_at": "2026-11-02T09:30:00Z"
          }
        ],
        "awake": {
          "since": "2026-11-02T09:30:00Z",
          "connections": {
            "https": 0
          },
          "last_client_activity_at": "2026-11-02T09:30:00Z",
          "sleeps_at_earliest": "2026-11-02T09:30:00Z",
          "activation": "<activation>",
          "processes": {
            "running": 0,
            "detached": 0
          },
          "holds": [
            {
              "hold": "<hold>",
              "source": "member",
              "activation": "<activation>",
              "until": "2026-11-02T09:30:00Z",
              "actor": "<actor>",
              "created_at": "2026-11-02T09:30:00Z"
            }
          ],
          "max_awake_until": "2026-11-02T09:30:00Z"
        },
        "spec_current": false,
        "delete_at": "2026-11-02T09:30:00Z"
      },
      "activation": {
        "id": "<id>",
        "wake_gen": 1,
        "state": "admitted",
        "started_at": "2026-11-02T09:30:00Z",
        "revision": 1
      },
      "revision": 1,
      "created_by": "<created_by>",
      "created_at": "2026-11-02T09:30:00Z",
      "updated_at": "2026-11-02T09:30:00Z"
    }
  ],
  "next_cursor": "<next_cursor>",
  "unavailable_regions": [
    "<unavailable_regions>"
  ]
}
```

### Errors

| Status | Code | When | Send again | What to do |
| --- | --- | --- | --- | --- |
| `401` | [`exchange_required`](https://tonbo.dev/docs/reference/management.md#exchange_required) | The key acts as an Agent, and an Agent’s key is never sent itself. | No | Exchange the key for a token at the endpoint the answer’s `token_endpoint` names, and send the token as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#exchange_required)) |
| `401` | [`unauthenticated`](https://tonbo.dev/docs/reference/management.md#unauthenticated) | The request carries no key or token, or one that is invalid, expired or revoked. | No | Send a valid key, or a token that has not expired, as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#unauthenticated)) |
| `403` | [`forbidden`](https://tonbo.dev/docs/reference/management.md#forbidden) | The key’s role does not hold the capability the operation needs, the key is narrowed away from the resource, or the account already holds as many of the resource as it may. | No | Use a key whose role holds the capability, or one that is not narrowed; delete what you no longer need, or ask for a higher limit. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#forbidden)) |
| `422` | [`region_not_offered`](https://tonbo.dev/docs/reference/management.md#region_not_offered) | The resource is not offered in the region named, or in the account’s default region. Nothing was written. | No | Name a region where it is offered; `GET /v1/regions` lists the regions and what each offers. ([more](https://tonbo.dev/docs/machines/troubleshooting/manage.md#region_not_offered)) |
| `422` | [`validation_error`](https://tonbo.dev/docs/reference/management.md#validation_error) | A parameter, header or body field breaks its rule; `errors` names each field. | No | Correct the fields `errors` names; the operation’s reference gives each one’s rule. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#validation_error)) |
| `429` | [`rate_limited`](https://tonbo.dev/docs/reference/management.md#rate_limited) | The key or the account sent more requests than its rate allows. | After Retry-After | Wait for the seconds Retry-After names, then send the request again; spread requests out if it recurs. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#rate_limited)) |

### Retries

- A read: sending it again changes nothing.
- `422` and `429` carry `Retry-After`: wait that many seconds, then send the same request again.

**Request**

```shell
curl -sS "https://api.tonbo.dev/v1/machines" \
  -H "Authorization: Bearer $TONBO_API_KEY"
```

## Create a Machine

`POST /v1/machines` · CLI: `tonbo machine create`

Needs [`machines.write`](https://tonbo.dev/docs/reference/management.md#capability-machines-write), which `owner`, `admin` and `member` hold. A narrowed key may not.

Any member creates Machines (`machines.write`); a public port also needs `machines.public`, and a narrowed key never does. The image is the reserved platform reference `tonbo.dev/platform` (machine-v2.schema.json `$defs.platform_image_reference`), which resolves nothing, is stored with `repository` and `digest` null and reads as `state` `ready`, or a public registry reference, which resolves to its canonical `repository` and linux/arm64 `digest`, stored with the reference, and reads as `state` `pulling` until a node has pulled it (`state` is computed, never stored; see getMachine); a registry reference is refused before anything is written, with 422 `image_platform_unsupported` when it has no linux/arm64 manifest and `validation_error` when it is not a public reference the resolver can fetch under machine-v2.json `image.distribution` (a malformed reference, an unknown or unavailable image, a redirected manifest or an oversized document). A name another Machine of the account holds answers 409 `name_taken`. The name, the HTTPS label `<name>-<account>` and the SSH username are then reserved for the account, and the Machine is recorded in its region, which is fixed for its life; without `region` it goes to the account's default creation region. A region where Machines are not offered (`machines.open` in contracts/regions/cells-v1.json) answers the shared 422 `region_not_offered`, and a shape that is not offered 422 `shape_not_offered`, before anything is written. The answer is 202 while the Machine is being created and 201 in every other state (machine-v2.json `creation.create_status`). Both carry `Location`, the Machine's URL, and the 202 also `Retry-After`, the seconds to wait before reading the Machine there again (machine-v2.json `creation.poll_after_seconds`), until its `state` is `active` or `failed`. A replay with the same Idempotency-Key answers the same Machine as it is now, so a replay of a Machine whose creation failed answers 201 with its `failure`; the key reused for another request answers 409 `idempotency_conflict`. Refusals carry the codes and statuses of contracts/machines/machine-v2.json `errors`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | yes | A value you choose for this one creation, 1 to 255 characters, such as a new UUID. Send a new one for each resource you create, and the same one only to retry that request after a timeout or a lost answer: the retry answers what the first request created, in its current state, and never creates a second. A key reused for a different database, stream or Machine answers 409. 1 to 255 characters. |

### Request body

`application/json`, required. Unknown fields are refused.

The fields below are the contract’s, with their types and limits; [Manage Machines](https://tonbo.dev/docs/machines/manage.md) explains what each one does.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | 1 to 40 characters. Matches `^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$`. Never `"self"`. |
| `region` | string | no | Matches `^[a-z]{2}(-gov)?-[a-z]+-[0-9]+$`. |
| `image` | string | yes | 1 to 512 characters. |
| `command` | array of string or null | no | 1 to 64 items. |
| `shape` | string | yes | Matches `^[a-z]-[0-9]+$`. |
| `ports` | array of object | no | At most 8 items. |
| `ports[].port` | integer | yes | 1 to 65535. |
| `ports[].access` | one of `"private"` or `"public"` | yes | Default `"private"`. |
| `ports[].default` | boolean | yes |  |
| `env` | object of string | no | At most 128 entries. Each key matches `^[A-Za-z_][A-Za-z0-9_]{0,127}$`. |
| `sleep_after_seconds` | integer | no | Default `600`. 30 to 86400. |
| `idle_action` | one of `"suspend"` or `"release"` | no | Default `"suspend"`. |
| `stop_grace_seconds` | integer | no | Default `10`. 0 to 230. |

### Response

| Status | Answer |
| --- | --- |
| `201` | The Machine, no longer being created (a replay of a failed create carries its `failure`) |
| `202` | The Machine, still being created |

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | [Machine](https://tonbo.dev/docs/reference/management/machines.md#schema-machine) | yes | The Machine. |

| Header | Status | Description |
| --- | --- | --- |
| `ETag` | 201, 202 | The Machine's ETag, `"<id>.<revision>"` for a managed Machine and `"<id>"` for a device (contracts/machines/machine-v2.json `etag`), opaque to clients. An update names it in If-Match, and a delete or a sleep may. |
| `Location` | 201, 202 | The Machine's URL, `GET /v1/machines/{name}`. |
| `Retry-After` | 202, 403, 409, 422, 429 | The seconds to wait before reading the Machine at `Location` again (machine-v2.json `creation.poll_after_seconds`). |
| `WWW-Authenticate` | 401 | Authentication scheme required by this API. |

**Example 201 body**

```json
{
  "data": {
    "id": "<id>",
    "name": "<name>",
    "kind": "managed",
    "region": "<region>",
    "byoc": {
      "os": "darwin",
      "arch": "arm64",
      "client": "yield",
      "client_version": "<client_version>",
      "harnesses": [
        "claude-code"
      ],
      "last_seen": "2026-11-02T09:30:00Z",
      "key_state": "active",
      "hostname": "<hostname>",
      "device_id": "<device_id>"
    },
    "hostnames": {
      "https": "<https>",
      "api": "<api>"
    },
    "image": {
      "reference": "<reference>",
      "repository": "<repository>",
      "digest": "<digest>",
      "state": "pulling",
      "failure": {
        "code": "image_unavailable",
        "message": "<message>"
      }
    },
    "command": [
      "<command>"
    ],
    "shape": "<shape>",
    "ports": [
      {
        "port": 1,
        "access": "private",
        "default": false
      }
    ],
    "env": {},
    "sleep_after_seconds": 600,
    "idle_action": "suspend",
    "stop_grace_seconds": 10,
    "database": {
      "name": "<name>",
      "url": "<url>"
    },
    "logs": {
      "url": "<url>"
    },
    "state": "creating",
    "failure": {
      "code": "<code>",
      "message": "<message>"
    },
    "status": {
      "power": "asleep",
      "snapshot": null,
      "sleep": {
        "action": "<action>",
        "reason": "<reason>"
      },
      "conditions": [
        {
          "type": "wake_blocked",
          "reason": "payment_required",
          "since": "2026-11-02T09:30:00Z",
          "retry_at": "2026-11-02T09:30:00Z"
        }
      ],
      "awake": {
        "since": "2026-11-02T09:30:00Z",
        "connections": {
          "https": 0
        },
        "last_client_activity_at": "2026-11-02T09:30:00Z",
        "sleeps_at_earliest": "2026-11-02T09:30:00Z",
        "activation": "<activation>",
        "processes": {
          "running": 0,
          "detached": 0
        },
        "holds": [
          {
            "hold": "<hold>",
            "source": "member",
            "activation": "<activation>",
            "until": "2026-11-02T09:30:00Z",
            "actor": {
              "sub": "<sub>",
              "client_id": "<client_id>"
            },
            "created_at": "2026-11-02T09:30:00Z"
          }
        ],
        "max_awake_until": "2026-11-02T09:30:00Z"
      },
      "spec_current": false,
      "delete_at": "2026-11-02T09:30:00Z"
    },
    "activation": {
      "id": "<id>",
      "wake_gen": 1,
      "state": "admitted",
      "started_at": "2026-11-02T09:30:00Z",
      "revision": 1
    },
    "revision": 1,
    "created_by": "<created_by>",
    "created_at": "2026-11-02T09:30:00Z",
    "updated_at": "2026-11-02T09:30:00Z"
  }
}
```

### Errors

| Status | Code | When | Send again | What to do |
| --- | --- | --- | --- | --- |
| `401` | [`exchange_required`](https://tonbo.dev/docs/reference/management.md#exchange_required) | The key acts as an Agent, and an Agent’s key is never sent itself. | No | Exchange the key for a token at the endpoint the answer’s `token_endpoint` names, and send the token as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#exchange_required)) |
| `401` | [`unauthenticated`](https://tonbo.dev/docs/reference/management.md#unauthenticated) | The request carries no key or token, or one that is invalid, expired or revoked. | No | Send a valid key, or a token that has not expired, as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#unauthenticated)) |
| `403` | [`forbidden`](https://tonbo.dev/docs/reference/management.md#forbidden) | The key’s role does not hold the capability the operation needs, the key is narrowed away from the resource, or the account already holds as many of the resource as it may. | No | Use a key whose role holds the capability, or one that is not narrowed; delete what you no longer need, or ask for a higher limit. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#forbidden)) |
| `409` | [`name_taken`](https://tonbo.dev/docs/reference/management.md#name_taken) | another Machine of the account holds that name | No | Pick another name, or delete the Machine that holds it. ([more](https://tonbo.dev/docs/machines/troubleshooting/manage.md#name_taken)) |
| `409` | [`idempotency_conflict`](https://tonbo.dev/docs/reference/management.md#idempotency_conflict) | The Idempotency-Key was already sent with a different request. | No | Send the request with a new Idempotency-Key, or send the request the key was first used for. ([more](https://tonbo.dev/docs/machines/troubleshooting/manage.md#idempotency_conflict)) |
| `409` | [`conflict`](https://tonbo.dev/docs/reference/management.md#conflict) | The name is held by another live resource of the account, the Idempotency-Key already created another resource or one since deleted, or the resource’s current state does not allow the change. `detail` says which, and `errors` names the field. | No | Pick another name or a new Idempotency-Key, or read the resource again and act on its current state. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#conflict)) |
| `422` | [`image_platform_unsupported`](https://tonbo.dev/docs/reference/management.md#image_platform_unsupported) | the image has no linux/arm64 manifest; the message names the platforms found | No | Build the image for linux/arm64 and name it again. ([more](https://tonbo.dev/docs/machines/troubleshooting/manage.md#image_platform_unsupported)) |
| `422` | [`validation_error`](https://tonbo.dev/docs/reference/management.md#validation_error) | A parameter, header or body field breaks its rule; `errors` names each field. | No | Correct the fields `errors` names; the operation’s reference gives each one’s rule. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#validation_error)) |
| `422` | [`region_not_offered`](https://tonbo.dev/docs/reference/management.md#region_not_offered) | The resource is not offered in the region named, or in the account’s default region. Nothing was written. | No | Name a region where it is offered; `GET /v1/regions` lists the regions and what each offers. ([more](https://tonbo.dev/docs/machines/troubleshooting/manage.md#region_not_offered)) |
| `422` | [`shape_not_offered`](https://tonbo.dev/docs/reference/management.md#shape_not_offered) | the shape is not a shapes-v1 key or is not offered; like `volumes_not_offered` and the shared `region_not_offered`, nothing was fetched or written | No | Pick a shape `GET /v1/machine-shapes` offers. ([more](https://tonbo.dev/docs/machines/troubleshooting/manage.md#shape_not_offered)) |
| `429` | [`rate_limited`](https://tonbo.dev/docs/reference/management.md#rate_limited) | The key or the account sent more requests than its rate allows. | After Retry-After | Wait for the seconds Retry-After names, then send the request again; spread requests out if it recurs. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#rate_limited)) |

### Retries

- Send a new [`Idempotency-Key`](#create-machine-header-idempotency-key) with each request and the same one when you send it again: a repeat answers what the first request did, and the key reused for a different request answers 409 (see [Idempotency](https://tonbo.dev/docs/reference/management.md#idempotency)).
- `202`, `403`, `409`, `422` and `429` carry `Retry-After`: wait that many seconds, then send the same request again.

**Request**

```shell
curl -sS "https://api.tonbo.dev/v1/machines" \
  -H "Authorization: Bearer $TONBO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"name":"<name>","image":"<image>","shape":"<shape>"}'
```

## Read a Machine

`GET /v1/machines/{name}` · CLI: `tonbo machine show`

Needs [`account.read`](https://tonbo.dev/docs/reference/management.md#capability-account-read), which `owner`, `admin` and `member` hold. A narrowed key may call it.

A Machine of the account. A device (kind `byoc`, in a personal account only) is served whatever `machines.open` says and answers its `byoc` facts, `hostname` and `device_id` to its person only, with its id as its ETag (`"<id>"`), which a delete may name in If-Match. A managed Machine's ETag is `"<id>.<revision>"` (contracts/machines/machine-v2.json `etag`), opaque to clients: an update names it in If-Match, and a delete or a sleep may, so a tag read from one Machine never matches another that later takes its name. A Machine is read whatever `machines.open` says, so a closure never hides it; a region this server cannot reach answers 503 `region_unavailable`. Its `image` names the canonical `repository` a registry image is pulled from and a `state` computed from the pull: `pulling` until a node has pulled the digest while the Machine is creating or active, `ready` once one has (always for the platform reference `tonbo.dev/platform`, which is never pulled), `failed` with a `failure` whose code is one of contracts/machines/machine-v2.json `image.failures`, and `not_pulled` on a failed or deleting Machine that no node pulled, since nothing will. While the image is not ready, a request that would wake the Machine answers 503 `image_pulling`, or 409 `image_failed` once the pull failed. `status.conditions` says why a Machine is not doing what it should: `wake_blocked` when its last wake was refused and none has succeeded since, with the `reason`, `since` and, for a crash backoff, `retry_at`; `creation_delayed` once it has been creating for longer than contracts/machines/machine-v2.json `creation.alert_after_seconds`; `egress_blocked` while its outbound access is blocked; and `volume_purged` when the Volume its spec names was purged (in practice after 30 days of suspension past the owners' notice), so it boots without it until the spec names another Volume or none. `activation.revision` is the spec revision the live activation was admitted with. Every spec change advances the Machine's `revision`, but ports and `sleep_after_seconds` apply at once, so a lower `activation.revision` means a sleep is needed only when image, command, shape or env changed; `GET /machines/{name}/activations` says how each earlier activation ended. A managed Machine names its creator in `created_by`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `name` | path | string | yes | The Machine's name. 1 to 40 characters. Matches `^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$`. Never `"self"`. |

### Response

| Status | Answer |
| --- | --- |
| `200` | The Machine, with its ETag |

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | [Machine](https://tonbo.dev/docs/reference/management/machines.md#schema-machine) | yes | The Machine. |

| Header | Status | Description |
| --- | --- | --- |
| `ETag` | 200 | The Machine's ETag, `"<id>.<revision>"` for a managed Machine and `"<id>"` for a device (contracts/machines/machine-v2.json `etag`), opaque to clients. An update names it in If-Match, and a delete or a sleep may. |
| `WWW-Authenticate` | 401 | Authentication scheme required by this API. |
| `Retry-After` | 429, 503 | Seconds before the client should retry. |

**Example 200 body**

```json
{
  "data": {
    "id": "<id>",
    "name": "<name>",
    "kind": "managed",
    "region": "<region>",
    "byoc": {
      "os": "darwin",
      "arch": "arm64",
      "client": "yield",
      "client_version": "<client_version>",
      "harnesses": [
        "claude-code"
      ],
      "last_seen": "2026-11-02T09:30:00Z",
      "key_state": "active",
      "hostname": "<hostname>",
      "device_id": "<device_id>"
    },
    "hostnames": {
      "https": "<https>",
      "api": "<api>"
    },
    "image": {
      "reference": "<reference>",
      "repository": "<repository>",
      "digest": "<digest>",
      "state": "pulling",
      "failure": {
        "code": "image_unavailable",
        "message": "<message>"
      }
    },
    "command": [
      "<command>"
    ],
    "shape": "<shape>",
    "ports": [
      {
        "port": 1,
        "access": "private",
        "default": false
      }
    ],
    "env": {},
    "sleep_after_seconds": 600,
    "idle_action": "suspend",
    "stop_grace_seconds": 10,
    "database": {
      "name": "<name>",
      "url": "<url>"
    },
    "logs": {
      "url": "<url>"
    },
    "state": "creating",
    "failure": {
      "code": "<code>",
      "message": "<message>"
    },
    "status": {
      "power": "asleep",
      "snapshot": null,
      "sleep": {
        "action": "<action>",
        "reason": "<reason>"
      },
      "conditions": [
        {
          "type": "wake_blocked",
          "reason": "payment_required",
          "since": "2026-11-02T09:30:00Z",
          "retry_at": "2026-11-02T09:30:00Z"
        }
      ],
      "awake": {
        "since": "2026-11-02T09:30:00Z",
        "connections": {
          "https": 0
        },
        "last_client_activity_at": "2026-11-02T09:30:00Z",
        "sleeps_at_earliest": "2026-11-02T09:30:00Z",
        "activation": "<activation>",
        "processes": {
          "running": 0,
          "detached": 0
        },
        "holds": [
          {
            "hold": "<hold>",
            "source": "member",
            "activation": "<activation>",
            "until": "2026-11-02T09:30:00Z",
            "actor": {
              "sub": "<sub>",
              "client_id": "<client_id>"
            },
            "created_at": "2026-11-02T09:30:00Z"
          }
        ],
        "max_awake_until": "2026-11-02T09:30:00Z"
      },
      "spec_current": false,
      "delete_at": "2026-11-02T09:30:00Z"
    },
    "activation": {
      "id": "<id>",
      "wake_gen": 1,
      "state": "admitted",
      "started_at": "2026-11-02T09:30:00Z",
      "revision": 1
    },
    "revision": 1,
    "created_by": "<created_by>",
    "created_at": "2026-11-02T09:30:00Z",
    "updated_at": "2026-11-02T09:30:00Z"
  }
}
```

### Errors

| Status | Code | When | Send again | What to do |
| --- | --- | --- | --- | --- |
| `401` | [`exchange_required`](https://tonbo.dev/docs/reference/management.md#exchange_required) | The key acts as an Agent, and an Agent’s key is never sent itself. | No | Exchange the key for a token at the endpoint the answer’s `token_endpoint` names, and send the token as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#exchange_required)) |
| `401` | [`unauthenticated`](https://tonbo.dev/docs/reference/management.md#unauthenticated) | The request carries no key or token, or one that is invalid, expired or revoked. | No | Send a valid key, or a token that has not expired, as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#unauthenticated)) |
| `403` | [`forbidden`](https://tonbo.dev/docs/reference/management.md#forbidden) | The key’s role does not hold the capability the operation needs, the key is narrowed away from the resource, or the account already holds as many of the resource as it may. | No | Use a key whose role holds the capability, or one that is not narrowed; delete what you no longer need, or ask for a higher limit. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#forbidden)) |
| `404` | [`not_found`](https://tonbo.dev/docs/reference/management.md#not_found) | No live resource of the key’s account has that name or id, or the key is narrowed away from it. | No | Check the name, and that the key belongs to the account that holds it; list the resources to see what exists. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#not_found)) |
| `429` | [`rate_limited`](https://tonbo.dev/docs/reference/management.md#rate_limited) | The key or the account sent more requests than its rate allows. | After Retry-After | Wait for the seconds Retry-After names, then send the request again; spread requests out if it recurs. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#rate_limited)) |
| `503` | [`region_unavailable`](https://tonbo.dev/docs/reference/management.md#region_unavailable) | The region could not be reached or read, or it takes no new resource of this kind. Every answer carries Retry-After. | After Retry-After | Send the request again after Retry-After; if `detail` says the region takes no new resource of this kind, name another region. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#region_unavailable)) |

### Retries

- A read: sending it again changes nothing.
- `429` and `503` carry `Retry-After`: wait that many seconds, then send the same request again.

**Request**

```shell
curl -sS "https://api.tonbo.dev/v1/machines/<name>" \
  -H "Authorization: Bearer $TONBO_API_KEY"
```

## Change a Machine's spec

`PATCH /v1/machines/{name}` · CLI: `tonbo machine update`

Needs [`machines.write`](https://tonbo.dev/docs/reference/management.md#capability-machines-write), which `owner`, `admin` and `member` hold. A narrowed key may not.

A managed Machine only: a device answers 409 `machine_kind_mismatch`, whatever `machines.open` says. If-Match is required, because `env` and `ports` are replaced whole: the change is written only at the revision it names, so a missing one answers 428 `precondition_required`, a stale one 412 `precondition_failed`, and a concurrent change is never lost; an If-Match that is not this Machine's ETag, another Machine's that had the name included, also answers 412 `precondition_failed`, and so does a bare revision (`"<n>"`), whose `detail` says to upgrade @tonbo/cli to 0.9.0 or later (machine-v2.json `etag.bare_revision`). A Machine whose creation failed answers 409 `machine_failed` and does not change. A region where Machines are not offered answers 422 `region_not_offered`, and a shape that is not offered 422 `shape_not_offered`. A change to image, command, shape or env applies at the next activation; ports and `sleep_after_seconds` apply at once. A changed image follows the create's rule: the platform reference `tonbo.dev/platform` resolves nothing, and any other reference resolves to its canonical repository and linux/arm64 digest, or is refused before anything is written as the create refuses it (422 `image_platform_unsupported` or `validation_error`). Naming an image, the same one included, resolves it again and returns its `state` to `pulling`, so a failed pull is retried. Making a port public also needs `machines.public`. Refusals carry the codes and statuses of contracts/machines/machine-v2.json `errors`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `name` | path | string | yes | The Machine's name. 1 to 40 characters. Matches `^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$`. Never `"self"`. |
| `If-Match` | header | string | yes | The Machine's ETag as read, `"<id>.<revision>"` (contracts/machines/machine-v2.json `etag`); a Machine update is written only to that Machine at that revision. Required, because `env` and `ports` are replaced whole; a missing one answers 428 `precondition_required`. At most 500 characters. |

### Request body

`application/json`, required. Unknown fields are refused.

The fields below are the contract’s, with their types and limits; [Manage Machines](https://tonbo.dev/docs/machines/manage.md) explains what each one does.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `image` | string | no | 1 to 512 characters. |
| `command` | array of string or null | no | 1 to 64 items. |
| `shape` | string | no | Matches `^[a-z]-[0-9]+$`. |
| `ports` | array of object | no | At most 8 items. |
| `ports[].port` | integer | yes | 1 to 65535. |
| `ports[].access` | one of `"private"` or `"public"` | yes | Default `"private"`. |
| `ports[].default` | boolean | yes |  |
| `env` | object of string | no | At most 128 entries. Each key matches `^[A-Za-z_][A-Za-z0-9_]{0,127}$`. |
| `sleep_after_seconds` | integer | no | Default `600`. 30 to 86400. |
| `idle_action` | one of `"suspend"` or `"release"` | no | Default `"suspend"`. |
| `stop_grace_seconds` | integer | no | Default `10`. 0 to 230. |
| `discard_snapshot` | boolean | no | Default `false`. |

### Response

| Status | Answer |
| --- | --- |
| `200` | The Machine |

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | [Machine](https://tonbo.dev/docs/reference/management/machines.md#schema-machine) | yes | The Machine. |

| Header | Status | Description |
| --- | --- | --- |
| `ETag` | 200 | The Machine's ETag, `"<id>.<revision>"` for a managed Machine and `"<id>"` for a device (contracts/machines/machine-v2.json `etag`), opaque to clients. An update names it in If-Match, and a delete or a sleep may. |
| `WWW-Authenticate` | 401 | Authentication scheme required by this API. |
| `Retry-After` | 409, 422, 429, 503 | Seconds before the client should retry. |

**Example 200 body**

```json
{
  "data": {
    "id": "<id>",
    "name": "<name>",
    "kind": "managed",
    "region": "<region>",
    "byoc": {
      "os": "darwin",
      "arch": "arm64",
      "client": "yield",
      "client_version": "<client_version>",
      "harnesses": [
        "claude-code"
      ],
      "last_seen": "2026-11-02T09:30:00Z",
      "key_state": "active",
      "hostname": "<hostname>",
      "device_id": "<device_id>"
    },
    "hostnames": {
      "https": "<https>",
      "api": "<api>"
    },
    "image": {
      "reference": "<reference>",
      "repository": "<repository>",
      "digest": "<digest>",
      "state": "pulling",
      "failure": {
        "code": "image_unavailable",
        "message": "<message>"
      }
    },
    "command": [
      "<command>"
    ],
    "shape": "<shape>",
    "ports": [
      {
        "port": 1,
        "access": "private",
        "default": false
      }
    ],
    "env": {},
    "sleep_after_seconds": 600,
    "idle_action": "suspend",
    "stop_grace_seconds": 10,
    "database": {
      "name": "<name>",
      "url": "<url>"
    },
    "logs": {
      "url": "<url>"
    },
    "state": "creating",
    "failure": {
      "code": "<code>",
      "message": "<message>"
    },
    "status": {
      "power": "asleep",
      "snapshot": null,
      "sleep": {
        "action": "<action>",
        "reason": "<reason>"
      },
      "conditions": [
        {
          "type": "wake_blocked",
          "reason": "payment_required",
          "since": "2026-11-02T09:30:00Z",
          "retry_at": "2026-11-02T09:30:00Z"
        }
      ],
      "awake": {
        "since": "2026-11-02T09:30:00Z",
        "connections": {
          "https": 0
        },
        "last_client_activity_at": "2026-11-02T09:30:00Z",
        "sleeps_at_earliest": "2026-11-02T09:30:00Z",
        "activation": "<activation>",
        "processes": {
          "running": 0,
          "detached": 0
        },
        "holds": [
          {
            "hold": "<hold>",
            "source": "member",
            "activation": "<activation>",
            "until": "2026-11-02T09:30:00Z",
            "actor": {
              "sub": "<sub>",
              "client_id": "<client_id>"
            },
            "created_at": "2026-11-02T09:30:00Z"
          }
        ],
        "max_awake_until": "2026-11-02T09:30:00Z"
      },
      "spec_current": false,
      "delete_at": "2026-11-02T09:30:00Z"
    },
    "activation": {
      "id": "<id>",
      "wake_gen": 1,
      "state": "admitted",
      "started_at": "2026-11-02T09:30:00Z",
      "revision": 1
    },
    "revision": 1,
    "created_by": "<created_by>",
    "created_at": "2026-11-02T09:30:00Z",
    "updated_at": "2026-11-02T09:30:00Z"
  }
}
```

### Errors

| Status | Code | When | Send again | What to do |
| --- | --- | --- | --- | --- |
| `401` | [`exchange_required`](https://tonbo.dev/docs/reference/management.md#exchange_required) | The key acts as an Agent, and an Agent’s key is never sent itself. | No | Exchange the key for a token at the endpoint the answer’s `token_endpoint` names, and send the token as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#exchange_required)) |
| `401` | [`unauthenticated`](https://tonbo.dev/docs/reference/management.md#unauthenticated) | The request carries no key or token, or one that is invalid, expired or revoked. | No | Send a valid key, or a token that has not expired, as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#unauthenticated)) |
| `403` | [`forbidden`](https://tonbo.dev/docs/reference/management.md#forbidden) | The key’s role does not hold the capability the operation needs, the key is narrowed away from the resource, or the account already holds as many of the resource as it may. | No | Use a key whose role holds the capability, or one that is not narrowed; delete what you no longer need, or ask for a higher limit. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#forbidden)) |
| `404` | [`not_found`](https://tonbo.dev/docs/reference/management.md#not_found) | No live resource of the key’s account has that name or id, or the key is narrowed away from it. | No | Check the name, and that the key belongs to the account that holds it; list the resources to see what exists. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#not_found)) |
| `409` | [`machine_kind_mismatch`](https://tonbo.dev/docs/reference/management.md#machine_kind_mismatch) | the action's `kinds` does not include the named Machine's kind: a managed action named a `byoc` Machine | No | Name a Machine Tonbo runs: the operation does not apply to a personal device. ([more](https://tonbo.dev/docs/machines/troubleshooting/manage.md#machine_kind_mismatch)) |
| `409` | [`machine_failed`](https://tonbo.dev/docs/reference/management.md#machine_failed) | an update or a sleep names a Machine whose creation failed (`state` failed); the detail names its `failure.code`, and the Machine does not change: delete it and create it again | No | Delete the Machine and create it again. ([more](https://tonbo.dev/docs/machines/troubleshooting/manage.md#machine_failed)) |
| `409` | [`conflict`](https://tonbo.dev/docs/reference/management.md#conflict) | The name is held by another live resource of the account, the Idempotency-Key already created another resource or one since deleted, or the resource’s current state does not allow the change. `detail` says which, and `errors` names the field. | No | Pick another name or a new Idempotency-Key, or read the resource again and act on its current state. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#conflict)) |
| `412` | [`precondition_failed`](https://tonbo.dev/docs/reference/management.md#precondition_failed) | If-Match does not name the resource’s current ETag: it changed since you read it. Nothing changed. | No | Read the resource again, check that the change still applies, and send it with the new ETag. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#precondition_failed)) |
| `422` | [`region_not_offered`](https://tonbo.dev/docs/reference/management.md#region_not_offered) | The resource is not offered in the region named, or in the account’s default region. Nothing was written. | No | Name a region where it is offered; `GET /v1/regions` lists the regions and what each offers. ([more](https://tonbo.dev/docs/machines/troubleshooting/manage.md#region_not_offered)) |
| `422` | [`shape_not_offered`](https://tonbo.dev/docs/reference/management.md#shape_not_offered) | the shape is not a shapes-v1 key or is not offered; like `volumes_not_offered` and the shared `region_not_offered`, nothing was fetched or written | No | Pick a shape `GET /v1/machine-shapes` offers. ([more](https://tonbo.dev/docs/machines/troubleshooting/manage.md#shape_not_offered)) |
| `422` | [`image_platform_unsupported`](https://tonbo.dev/docs/reference/management.md#image_platform_unsupported) | the image has no linux/arm64 manifest; the message names the platforms found | No | Build the image for linux/arm64 and name it again. ([more](https://tonbo.dev/docs/machines/troubleshooting/manage.md#image_platform_unsupported)) |
| `422` | [`validation_error`](https://tonbo.dev/docs/reference/management.md#validation_error) | A parameter, header or body field breaks its rule; `errors` names each field. | No | Correct the fields `errors` names; the operation’s reference gives each one’s rule. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#validation_error)) |
| `428` | [`precondition_required`](https://tonbo.dev/docs/reference/management.md#precondition_required) | The operation requires If-Match and the request sent none. | No | Read the resource and send its ETag as If-Match. ([more](https://tonbo.dev/docs/machines/troubleshooting/manage.md#precondition_required)) |
| `429` | [`rate_limited`](https://tonbo.dev/docs/reference/management.md#rate_limited) | The key or the account sent more requests than its rate allows. | After Retry-After | Wait for the seconds Retry-After names, then send the request again; spread requests out if it recurs. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#rate_limited)) |
| `503` | [`region_unavailable`](https://tonbo.dev/docs/reference/management.md#region_unavailable) | The region could not be reached or read, or it takes no new resource of this kind. Every answer carries Retry-After. | After Retry-After | Send the request again after Retry-After; if `detail` says the region takes no new resource of this kind, name another region. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#region_unavailable)) |

### Retries

- It takes no Idempotency-Key; its description above says what sending it again does.
- [`If-Match`](#update-machine-header-if-match) is required: send the `ETag` you read, and a 412 `precondition_failed` means the resource changed since; read it again and decide (see [If-Match](https://tonbo.dev/docs/reference/management.md#if-match)).
- `409`, `422`, `429` and `503` carry `Retry-After`: wait that many seconds, then send the same request again.

**Request**

```shell
curl -sS -X PATCH "https://api.tonbo.dev/v1/machines/<name>" \
  -H "Authorization: Bearer $TONBO_API_KEY" \
  -H "If-Match: <if-match>" \
  -H "Content-Type: application/json" \
  -d '{}'
```

## Delete a Machine

`DELETE /v1/machines/{name}` · CLI: `tonbo machine delete`

Needs [`machines.write`](https://tonbo.dev/docs/reference/management.md#capability-machines-write), which `owner`, `admin` and `member` hold. A narrowed key may not.

A managed Machine stops and is purged with its database and logs; its name is free at once for the account's next Machine, while its hostnames stay reserved to the account. The answer is 202 with the Machine being deleted, after which it is absent from the API, or 204 with no body when the name's create never reached its region and only the name was released. A region where Machines are not offered answers 422 `region_not_offered`, and one this server cannot reach 503 `region_unavailable`. A device (kind `byoc`) is forgotten instead, whatever `machines.open` says (contracts/machines/machine-v2.json `kinds.byoc.forget`): it is hidden and leaves the per-person cap at once, and its name is free once the forget drains; the answer is 202 with the device as it was, or 204 when its region held no row for it; a 204 names the kind in `X-Tonbo-Machine-Kind`. Forgetting a device that is not the caller's own (an Agent key's) also needs `api_keys.others` (`actions` `delete` `byoc_requires_also`), 403 otherwise. A device's key is not revoked: a device whose key is still active registers again at its next start. If-Match is optional; when sent it must be the Machine's current ETag (a device's is its id), or the answer is 412 `precondition_failed` and nothing is deleted, so a delete retried after the name was reused never deletes the new Machine. A name whose create never reached its region has no ETag to match.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `name` | path | string | yes | The Machine's name. 1 to 40 characters. Matches `^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$`. Never `"self"`. |
| `If-Match` | header | string | no | The Machine's ETag as read (contracts/machines/machine-v2.json `etag`); when sent, the request acts only on that Machine at that revision, and otherwise answers 412 `precondition_failed`. At most 500 characters. |

### Response

| Status | Answer |
| --- | --- |
| `202` | The Machine, being deleted, or the device, forgotten |
| `204` | The name was released, no Machine having been recorded under it, or the device was forgotten, its region holding no row for it |

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | [Machine](https://tonbo.dev/docs/reference/management/machines.md#schema-machine) | yes | The Machine. |

| Header | Status | Description |
| --- | --- | --- |
| `X-Tonbo-Machine-Kind` | 204 | The kind of the Machine the name held, so that a client words a released name (`managed`) and a forgotten device (`byoc`) apart. |
| `WWW-Authenticate` | 401 | Authentication scheme required by this API. |
| `Retry-After` | 422, 429, 503 | Seconds before the client should retry. |

**Example 202 body**

```json
{
  "data": {
    "id": "<id>",
    "name": "<name>",
    "kind": "managed",
    "region": "<region>",
    "byoc": {
      "os": "darwin",
      "arch": "arm64",
      "client": "yield",
      "client_version": "<client_version>",
      "harnesses": [
        "claude-code"
      ],
      "last_seen": "2026-11-02T09:30:00Z",
      "key_state": "active",
      "hostname": "<hostname>",
      "device_id": "<device_id>"
    },
    "hostnames": {
      "https": "<https>",
      "api": "<api>"
    },
    "image": {
      "reference": "<reference>",
      "repository": "<repository>",
      "digest": "<digest>",
      "state": "pulling",
      "failure": {
        "code": "image_unavailable",
        "message": "<message>"
      }
    },
    "command": [
      "<command>"
    ],
    "shape": "<shape>",
    "ports": [
      {
        "port": 1,
        "access": "private",
        "default": false
      }
    ],
    "env": {},
    "sleep_after_seconds": 600,
    "idle_action": "suspend",
    "stop_grace_seconds": 10,
    "database": {
      "name": "<name>",
      "url": "<url>"
    },
    "logs": {
      "url": "<url>"
    },
    "state": "creating",
    "failure": {
      "code": "<code>",
      "message": "<message>"
    },
    "status": {
      "power": "asleep",
      "snapshot": null,
      "sleep": {
        "action": "<action>",
        "reason": "<reason>"
      },
      "conditions": [
        {
          "type": "wake_blocked",
          "reason": "payment_required",
          "since": "2026-11-02T09:30:00Z",
          "retry_at": "2026-11-02T09:30:00Z"
        }
      ],
      "awake": {
        "since": "2026-11-02T09:30:00Z",
        "connections": {
          "https": 0
        },
        "last_client_activity_at": "2026-11-02T09:30:00Z",
        "sleeps_at_earliest": "2026-11-02T09:30:00Z",
        "activation": "<activation>",
        "processes": {
          "running": 0,
          "detached": 0
        },
        "holds": [
          {
            "hold": "<hold>",
            "source": "member",
            "activation": "<activation>",
            "until": "2026-11-02T09:30:00Z",
            "actor": {
              "sub": "<sub>",
              "client_id": "<client_id>"
            },
            "created_at": "2026-11-02T09:30:00Z"
          }
        ],
        "max_awake_until": "2026-11-02T09:30:00Z"
      },
      "spec_current": false,
      "delete_at": "2026-11-02T09:30:00Z"
    },
    "activation": {
      "id": "<id>",
      "wake_gen": 1,
      "state": "admitted",
      "started_at": "2026-11-02T09:30:00Z",
      "revision": 1
    },
    "revision": 1,
    "created_by": "<created_by>",
    "created_at": "2026-11-02T09:30:00Z",
    "updated_at": "2026-11-02T09:30:00Z"
  }
}
```

### Errors

| Status | Code | When | Send again | What to do |
| --- | --- | --- | --- | --- |
| `401` | [`exchange_required`](https://tonbo.dev/docs/reference/management.md#exchange_required) | The key acts as an Agent, and an Agent’s key is never sent itself. | No | Exchange the key for a token at the endpoint the answer’s `token_endpoint` names, and send the token as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#exchange_required)) |
| `401` | [`unauthenticated`](https://tonbo.dev/docs/reference/management.md#unauthenticated) | The request carries no key or token, or one that is invalid, expired or revoked. | No | Send a valid key, or a token that has not expired, as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#unauthenticated)) |
| `403` | [`forbidden`](https://tonbo.dev/docs/reference/management.md#forbidden) | The key’s role does not hold the capability the operation needs, the key is narrowed away from the resource, or the account already holds as many of the resource as it may. | No | Use a key whose role holds the capability, or one that is not narrowed; delete what you no longer need, or ask for a higher limit. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#forbidden)) |
| `404` | [`not_found`](https://tonbo.dev/docs/reference/management.md#not_found) | No live resource of the key’s account has that name or id, or the key is narrowed away from it. | No | Check the name, and that the key belongs to the account that holds it; list the resources to see what exists. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#not_found)) |
| `412` | [`precondition_failed`](https://tonbo.dev/docs/reference/management.md#precondition_failed) | If-Match does not name the resource’s current ETag: it changed since you read it. Nothing changed. | No | Read the resource again, check that the change still applies, and send it with the new ETag. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#precondition_failed)) |
| `422` | [`region_not_offered`](https://tonbo.dev/docs/reference/management.md#region_not_offered) | The resource is not offered in the region named, or in the account’s default region. Nothing was written. | No | Name a region where it is offered; `GET /v1/regions` lists the regions and what each offers. ([more](https://tonbo.dev/docs/machines/troubleshooting/manage.md#region_not_offered)) |
| `422` | [`validation_error`](https://tonbo.dev/docs/reference/management.md#validation_error) | A parameter, header or body field breaks its rule; `errors` names each field. | No | Correct the fields `errors` names; the operation’s reference gives each one’s rule. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#validation_error)) |
| `429` | [`rate_limited`](https://tonbo.dev/docs/reference/management.md#rate_limited) | The key or the account sent more requests than its rate allows. | After Retry-After | Wait for the seconds Retry-After names, then send the request again; spread requests out if it recurs. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#rate_limited)) |
| `503` | [`region_unavailable`](https://tonbo.dev/docs/reference/management.md#region_unavailable) | The region could not be reached or read, or it takes no new resource of this kind. Every answer carries Retry-After. | After Retry-After | Send the request again after Retry-After; if `detail` says the region takes no new resource of this kind, name another region. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#region_unavailable)) |

### Retries

- It takes no Idempotency-Key; its description above says what sending it again does.
- [`If-Match`](#delete-machine-header-if-match) is optional: send the `ETag` you read, and a 412 `precondition_failed` means the resource changed since; read it again and decide (see [If-Match](https://tonbo.dev/docs/reference/management.md#if-match)).
- `422`, `429` and `503` carry `Retry-After`: wait that many seconds, then send the same request again.

**Request**

```shell
curl -sS -X DELETE "https://api.tonbo.dev/v1/machines/<name>" \
  -H "Authorization: Bearer $TONBO_API_KEY"
```

## Put a Machine to sleep now

`POST /v1/machines/{name}/sleep` · CLI: `tonbo machine sleep`

Needs [`machines.write`](https://tonbo.dev/docs/reference/management.md#capability-machines-write), which `owner`, `admin` and `member` hold. A narrowed key may not.

machine-controller applies the Machine's effective idle action (`status.sleep`). An asleep Machine wakes on its next connection; there is no wake operation. The answer is 202 while an activation runs, with `Retry-After`, the seconds to wait before reading the Machine again (machine-v2.json `sleep.poll_after_seconds`), until its `status.power` is `asleep`; and 200 with the Machine unchanged when there is nothing to do: it is asleep, or still being created. A Machine whose creation failed answers 409 `machine_failed`, and a device 409 `machine_kind_mismatch`, whatever `machines.open` says. A region where Machines are not offered answers 422 `region_not_offered`. If-Match is optional; when sent it must be the Machine's current ETag, or the answer is 412 `precondition_failed` and nothing changes.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `name` | path | string | yes | The Machine's name. 1 to 40 characters. Matches `^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$`. Never `"self"`. |
| `If-Match` | header | string | no | The Machine's ETag as read (contracts/machines/machine-v2.json `etag`); when sent, the request acts only on that Machine at that revision, and otherwise answers 412 `precondition_failed`. At most 500 characters. |

### Response

| Status | Answer |
| --- | --- |
| `200` | The Machine, unchanged, with nothing to do |
| `202` | The Machine, going to sleep |

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | [Machine](https://tonbo.dev/docs/reference/management/machines.md#schema-machine) | yes | The Machine. |

| Header | Status | Description |
| --- | --- | --- |
| `Retry-After` | 202, 409, 422, 429, 503 | The seconds to wait before reading the Machine (getMachine) again, until its `status.power` is `asleep` (machine-v2.json `sleep.poll_after_seconds`). |
| `WWW-Authenticate` | 401 | Authentication scheme required by this API. |

**Example 200 body**

```json
{
  "data": {
    "id": "<id>",
    "name": "<name>",
    "kind": "managed",
    "region": "<region>",
    "byoc": {
      "os": "darwin",
      "arch": "arm64",
      "client": "yield",
      "client_version": "<client_version>",
      "harnesses": [
        "claude-code"
      ],
      "last_seen": "2026-11-02T09:30:00Z",
      "key_state": "active",
      "hostname": "<hostname>",
      "device_id": "<device_id>"
    },
    "hostnames": {
      "https": "<https>",
      "api": "<api>"
    },
    "image": {
      "reference": "<reference>",
      "repository": "<repository>",
      "digest": "<digest>",
      "state": "pulling",
      "failure": {
        "code": "image_unavailable",
        "message": "<message>"
      }
    },
    "command": [
      "<command>"
    ],
    "shape": "<shape>",
    "ports": [
      {
        "port": 1,
        "access": "private",
        "default": false
      }
    ],
    "env": {},
    "sleep_after_seconds": 600,
    "idle_action": "suspend",
    "stop_grace_seconds": 10,
    "database": {
      "name": "<name>",
      "url": "<url>"
    },
    "logs": {
      "url": "<url>"
    },
    "state": "creating",
    "failure": {
      "code": "<code>",
      "message": "<message>"
    },
    "status": {
      "power": "asleep",
      "snapshot": null,
      "sleep": {
        "action": "<action>",
        "reason": "<reason>"
      },
      "conditions": [
        {
          "type": "wake_blocked",
          "reason": "payment_required",
          "since": "2026-11-02T09:30:00Z",
          "retry_at": "2026-11-02T09:30:00Z"
        }
      ],
      "awake": {
        "since": "2026-11-02T09:30:00Z",
        "connections": {
          "https": 0
        },
        "last_client_activity_at": "2026-11-02T09:30:00Z",
        "sleeps_at_earliest": "2026-11-02T09:30:00Z",
        "activation": "<activation>",
        "processes": {
          "running": 0,
          "detached": 0
        },
        "holds": [
          {
            "hold": "<hold>",
            "source": "member",
            "activation": "<activation>",
            "until": "2026-11-02T09:30:00Z",
            "actor": {
              "sub": "<sub>",
              "client_id": "<client_id>"
            },
            "created_at": "2026-11-02T09:30:00Z"
          }
        ],
        "max_awake_until": "2026-11-02T09:30:00Z"
      },
      "spec_current": false,
      "delete_at": "2026-11-02T09:30:00Z"
    },
    "activation": {
      "id": "<id>",
      "wake_gen": 1,
      "state": "admitted",
      "started_at": "2026-11-02T09:30:00Z",
      "revision": 1
    },
    "revision": 1,
    "created_by": "<created_by>",
    "created_at": "2026-11-02T09:30:00Z",
    "updated_at": "2026-11-02T09:30:00Z"
  }
}
```

### Errors

| Status | Code | When | Send again | What to do |
| --- | --- | --- | --- | --- |
| `401` | [`exchange_required`](https://tonbo.dev/docs/reference/management.md#exchange_required) | The key acts as an Agent, and an Agent’s key is never sent itself. | No | Exchange the key for a token at the endpoint the answer’s `token_endpoint` names, and send the token as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#exchange_required)) |
| `401` | [`unauthenticated`](https://tonbo.dev/docs/reference/management.md#unauthenticated) | The request carries no key or token, or one that is invalid, expired or revoked. | No | Send a valid key, or a token that has not expired, as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#unauthenticated)) |
| `403` | [`forbidden`](https://tonbo.dev/docs/reference/management.md#forbidden) | The key’s role does not hold the capability the operation needs, the key is narrowed away from the resource, or the account already holds as many of the resource as it may. | No | Use a key whose role holds the capability, or one that is not narrowed; delete what you no longer need, or ask for a higher limit. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#forbidden)) |
| `404` | [`not_found`](https://tonbo.dev/docs/reference/management.md#not_found) | No live resource of the key’s account has that name or id, or the key is narrowed away from it. | No | Check the name, and that the key belongs to the account that holds it; list the resources to see what exists. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#not_found)) |
| `409` | [`machine_failed`](https://tonbo.dev/docs/reference/management.md#machine_failed) | an update or a sleep names a Machine whose creation failed (`state` failed); the detail names its `failure.code`, and the Machine does not change: delete it and create it again | No | Delete the Machine and create it again. ([more](https://tonbo.dev/docs/machines/troubleshooting/manage.md#machine_failed)) |
| `409` | [`machine_kind_mismatch`](https://tonbo.dev/docs/reference/management.md#machine_kind_mismatch) | the action's `kinds` does not include the named Machine's kind: a managed action named a `byoc` Machine | No | Name a Machine Tonbo runs: the operation does not apply to a personal device. ([more](https://tonbo.dev/docs/machines/troubleshooting/manage.md#machine_kind_mismatch)) |
| `409` | [`conflict`](https://tonbo.dev/docs/reference/management.md#conflict) | The name is held by another live resource of the account, the Idempotency-Key already created another resource or one since deleted, or the resource’s current state does not allow the change. `detail` says which, and `errors` names the field. | No | Pick another name or a new Idempotency-Key, or read the resource again and act on its current state. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#conflict)) |
| `412` | [`precondition_failed`](https://tonbo.dev/docs/reference/management.md#precondition_failed) | If-Match does not name the resource’s current ETag: it changed since you read it. Nothing changed. | No | Read the resource again, check that the change still applies, and send it with the new ETag. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#precondition_failed)) |
| `422` | [`region_not_offered`](https://tonbo.dev/docs/reference/management.md#region_not_offered) | The resource is not offered in the region named, or in the account’s default region. Nothing was written. | No | Name a region where it is offered; `GET /v1/regions` lists the regions and what each offers. ([more](https://tonbo.dev/docs/machines/troubleshooting/manage.md#region_not_offered)) |
| `422` | [`validation_error`](https://tonbo.dev/docs/reference/management.md#validation_error) | A parameter, header or body field breaks its rule; `errors` names each field. | No | Correct the fields `errors` names; the operation’s reference gives each one’s rule. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#validation_error)) |
| `429` | [`rate_limited`](https://tonbo.dev/docs/reference/management.md#rate_limited) | The key or the account sent more requests than its rate allows. | After Retry-After | Wait for the seconds Retry-After names, then send the request again; spread requests out if it recurs. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#rate_limited)) |
| `503` | [`region_unavailable`](https://tonbo.dev/docs/reference/management.md#region_unavailable) | The region could not be reached or read, or it takes no new resource of this kind. Every answer carries Retry-After. | After Retry-After | Send the request again after Retry-After; if `detail` says the region takes no new resource of this kind, name another region. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#region_unavailable)) |

### Retries

- No Idempotency-Key: a repeat requests the same sleep of the same Machine
- [`If-Match`](#sleep-machine-header-if-match) is optional: send the `ETag` you read, and a 412 `precondition_failed` means the resource changed since; read it again and decide (see [If-Match](https://tonbo.dev/docs/reference/management.md#if-match)).
- `202`, `409`, `422`, `429` and `503` carry `Retry-After`: wait that many seconds, then send the same request again.

**Request**

```shell
curl -sS -X POST "https://api.tonbo.dev/v1/machines/<name>/sleep" \
  -H "Authorization: Bearer $TONBO_API_KEY"
```

## List a Machine's activations

`GET /v1/machines/{name}/activations` · CLI: `tonbo machine activations`

Needs [`account.read`](https://tonbo.dev/docs/reference/management.md#capability-account-read), which `owner`, `admin` and `member` hold. A narrowed key may call it.

A managed Machine's activations, newest first: each boot or refused wake, with what woke it, the spec `revision` and `shape` it was admitted with, when, and its `end` (machine-v2.schema.json `$defs.activation_end`), null while it is live. An end's `reason` is why it ended: a refusal such as `payment_required` or `crash_backoff`, a stop such as `idle_early`, `sleep_after`, `workload_exited` (with the `exit_code` or `signal`) or `funding_stop`, or the node's. Any member reads it (`account.read`), whatever `machines.open` says; a narrowed key never does. A device answers 409 `machine_kind_mismatch`, and a region this server cannot reach 503 `region_unavailable`. The cursor is tied to the Machine, so it never pages another Machine that later takes the name.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `name` | path | string | yes | The Machine's name. 1 to 40 characters. Matches `^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$`. Never `"self"`. |
| `cursor` | query | string | no | Opaque cursor tied to the original query shape. At least 1 characters. |
| `limit` | query | integer | no | The most items one page holds, 1 to 200; 50 when omitted. A page may hold fewer; read on with `next_cursor` until it is null. Default `50`. 1 to 200. |

### Response

| Status | Answer |
| --- | --- |
| `200` | Activations, newest first |

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | array of object | yes |  |
| `data[].id` | string | yes | Matches `^[0-9a-f]{12}7[0-9a-f]{3}[89ab][0-9a-f]{15}$`. |
| `data[].wake_gen` | integer | yes | 1 to 9007199254740991. |
| `data[].revision` | integer | yes | At least 1. |
| `data[].shape` | string | yes | Matches `^[a-z]-[0-9]+$`. |
| `data[].started_at` | string (date-time) | yes | Matches `Z$`. |
| `data[].end` | object or null | yes |  |
| `data[].end.state` | one of `"released"`, `"failed"`, `"expired"`, `"lost"`, `"refused"` or `"suspended"` | yes |  |
| `data[].end.reason` | string | yes | Matches `^[a-z][a-z0-9_]{0,63}$`. |
| `data[].end.exit_code` | integer or null | yes | 0 to 255. |
| `data[].end.signal` | integer or null | yes | 1 to 64. |
| `data[].end.at` | string (date-time) | yes | Matches `Z$`. |
| `data[].secrets_generation` | integer | no | At least 0. |
| `next_cursor` | string or null | yes | Opaque cursor of the next page; null on the last page. |

| Header | Status | Description |
| --- | --- | --- |
| `WWW-Authenticate` | 401 | Authentication scheme required by this API. |
| `Retry-After` | 409, 422, 429, 503 | Seconds before the client should retry. |

**Example 200 body**

```json
{
  "data": [
    {
      "id": "<id>",
      "wake_gen": 1,
      "revision": 1,
      "shape": "<shape>",
      "started_at": "2026-11-02T09:30:00Z",
      "end": {
        "state": "released",
        "reason": "<reason>",
        "exit_code": 0,
        "signal": 1,
        "at": "2026-11-02T09:30:00Z"
      },
      "secrets_generation": 0
    }
  ],
  "next_cursor": "<next_cursor>"
}
```

### Errors

| Status | Code | When | Send again | What to do |
| --- | --- | --- | --- | --- |
| `401` | [`exchange_required`](https://tonbo.dev/docs/reference/management.md#exchange_required) | The key acts as an Agent, and an Agent’s key is never sent itself. | No | Exchange the key for a token at the endpoint the answer’s `token_endpoint` names, and send the token as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#exchange_required)) |
| `401` | [`unauthenticated`](https://tonbo.dev/docs/reference/management.md#unauthenticated) | The request carries no key or token, or one that is invalid, expired or revoked. | No | Send a valid key, or a token that has not expired, as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#unauthenticated)) |
| `403` | [`forbidden`](https://tonbo.dev/docs/reference/management.md#forbidden) | The key’s role does not hold the capability the operation needs, the key is narrowed away from the resource, or the account already holds as many of the resource as it may. | No | Use a key whose role holds the capability, or one that is not narrowed; delete what you no longer need, or ask for a higher limit. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#forbidden)) |
| `404` | [`not_found`](https://tonbo.dev/docs/reference/management.md#not_found) | No live resource of the key’s account has that name or id, or the key is narrowed away from it. | No | Check the name, and that the key belongs to the account that holds it; list the resources to see what exists. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#not_found)) |
| `409` | [`machine_kind_mismatch`](https://tonbo.dev/docs/reference/management.md#machine_kind_mismatch) | the action's `kinds` does not include the named Machine's kind: a managed action named a `byoc` Machine | No | Name a Machine Tonbo runs: the operation does not apply to a personal device. ([more](https://tonbo.dev/docs/machines/troubleshooting/manage.md#machine_kind_mismatch)) |
| `409` | [`conflict`](https://tonbo.dev/docs/reference/management.md#conflict) | The name is held by another live resource of the account, the Idempotency-Key already created another resource or one since deleted, or the resource’s current state does not allow the change. `detail` says which, and `errors` names the field. | No | Pick another name or a new Idempotency-Key, or read the resource again and act on its current state. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#conflict)) |
| `422` | [`region_not_offered`](https://tonbo.dev/docs/reference/management.md#region_not_offered) | The resource is not offered in the region named, or in the account’s default region. Nothing was written. | No | Name a region where it is offered; `GET /v1/regions` lists the regions and what each offers. ([more](https://tonbo.dev/docs/machines/troubleshooting/manage.md#region_not_offered)) |
| `422` | [`validation_error`](https://tonbo.dev/docs/reference/management.md#validation_error) | A parameter, header or body field breaks its rule; `errors` names each field. | No | Correct the fields `errors` names; the operation’s reference gives each one’s rule. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#validation_error)) |
| `429` | [`rate_limited`](https://tonbo.dev/docs/reference/management.md#rate_limited) | The key or the account sent more requests than its rate allows. | After Retry-After | Wait for the seconds Retry-After names, then send the request again; spread requests out if it recurs. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#rate_limited)) |
| `503` | [`region_unavailable`](https://tonbo.dev/docs/reference/management.md#region_unavailable) | The region could not be reached or read, or it takes no new resource of this kind. Every answer carries Retry-After. | After Retry-After | Send the request again after Retry-After; if `detail` says the region takes no new resource of this kind, name another region. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#region_unavailable)) |

### Retries

- A read: sending it again changes nothing.
- `409`, `422`, `429` and `503` carry `Retry-After`: wait that many seconds, then send the same request again.

**Request**

```shell
curl -sS "https://api.tonbo.dev/v1/machines/<name>/activations" \
  -H "Authorization: Bearer $TONBO_API_KEY"
```

## List the Machine shapes

`GET /v1/machine-shapes` · CLI: `tonbo machine shapes`

Needs [`account.read`](https://tonbo.dev/docs/reference/management.md#capability-account-read), which `owner`, `admin` and `member` hold. A narrowed key may call it.

Every shape of contracts/machines/shapes-v1.json, with whether a Machine may name it now and its list price for an hour at full use, and the shape a client proposes by default. Served to any member whatever `machines.open` says; a narrowed key never reaches it.

### Response

| Status | Answer |
| --- | --- |
| `200` | Shapes |

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | array of [MachineShape](https://tonbo.dev/docs/reference/management/machines.md#schema-machine-shape) | yes |  |
| `default` | string | yes | shapes-v1.json `default`. |

| Header | Status | Description |
| --- | --- | --- |
| `WWW-Authenticate` | 401 | Authentication scheme required by this API. |
| `Retry-After` | 429 | Seconds before the client should retry. |

**Example 200 body**

```json
{
  "data": [
    {
      "shape": "m-8",
      "vcpus": 1,
      "memory_gib": 1,
      "scratch_gib": 1,
      "offered": false,
      "suspendable": false,
      "ceiling_usd_micros_per_hour": 0
    }
  ],
  "default": "<default>"
}
```

### Errors

| Status | Code | When | Send again | What to do |
| --- | --- | --- | --- | --- |
| `401` | [`exchange_required`](https://tonbo.dev/docs/reference/management.md#exchange_required) | The key acts as an Agent, and an Agent’s key is never sent itself. | No | Exchange the key for a token at the endpoint the answer’s `token_endpoint` names, and send the token as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#exchange_required)) |
| `401` | [`unauthenticated`](https://tonbo.dev/docs/reference/management.md#unauthenticated) | The request carries no key or token, or one that is invalid, expired or revoked. | No | Send a valid key, or a token that has not expired, as `Authorization: Bearer`. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#unauthenticated)) |
| `403` | [`forbidden`](https://tonbo.dev/docs/reference/management.md#forbidden) | The key’s role does not hold the capability the operation needs, the key is narrowed away from the resource, or the account already holds as many of the resource as it may. | No | Use a key whose role holds the capability, or one that is not narrowed; delete what you no longer need, or ask for a higher limit. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#forbidden)) |
| `429` | [`rate_limited`](https://tonbo.dev/docs/reference/management.md#rate_limited) | The key or the account sent more requests than its rate allows. | After Retry-After | Wait for the seconds Retry-After names, then send the request again; spread requests out if it recurs. ([more](https://tonbo.dev/docs/databases/troubleshooting/manage.md#rate_limited)) |

### Retries

- A read: sending it again changes nothing.
- `429` carry `Retry-After`: wait that many seconds, then send the same request again.

**Request**

```shell
curl -sS "https://api.tonbo.dev/v1/machine-shapes" \
  -H "Authorization: Bearer $TONBO_API_KEY"
```

## Objects

### Machine

A Machine as contracts/machines/machine-v2.schema.json defines it.

The fields below are the contract’s, with their types and limits; [Manage Machines](https://tonbo.dev/docs/machines/manage.md) explains what each one does.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Matches `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`. |
| `name` | string | yes | 1 to 40 characters. Matches `^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$`. Never `"self"`. |
| `kind` | one of `"managed"` or `"byoc"` | yes |  |
| `region` | string | yes | Matches `^[a-z]{2}(-gov)?-[a-z]+-[0-9]+$`. |
| `byoc` | object | no |  |
| `byoc.os` | one of `"darwin"` or `"linux"` | yes |  |
| `byoc.arch` | one of `"arm64"` or `"amd64"` | yes |  |
| `byoc.client` | one of `"yield"` | yes |  |
| `byoc.client_version` | string | yes | At most 64 characters. Matches `^(0\|[1-9][0-9]*)\.(0\|[1-9][0-9]*)\.(0\|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$`. |
| `byoc.harnesses` | array of one of `"claude-code"` or `"codex"` | yes | No duplicates. |
| `byoc.last_seen` | string (date-time) | yes | Matches `Z$`. |
| `byoc.key_state` | one of `"active"`, `"revoked"` or `"expired"` | yes |  |
| `byoc.hostname` | string | no | 1 to 253 characters. |
| `byoc.device_id` | string | no | Matches `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`. |
| `hostnames` | object | no |  |
| `hostnames.https` | string | yes | Matches `^https://[a-z][a-z0-9]*(?:-[a-z0-9]+)*-[a-z][a-z0-9]*(?:-[a-z0-9]+)*\.tonbo\.sh$`. |
| `hostnames.api` | string | no | Matches `^https://api\.[a-z0-9-]+\.tonbo\.sh$`. |
| `image` | object | no |  |
| `image.reference` | string | yes | 1 to 512 characters. |
| `image.repository` | string or null | yes | At most 512 characters. Matches `^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+/[a-z0-9]+(?:(?:[._]\|__\|-+)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]\|__\|-+)[a-z0-9]+)*)*$`. |
| `image.digest` | string or null | yes | Matches `^sha256:[0-9a-f]{64}$`. |
| `image.state` | one of `"pulling"`, `"ready"`, `"failed"` or `"not_pulled"` | yes |  |
| `image.failure` | object or null | yes |  |
| `image.failure.code` | one of `"image_unavailable"`, `"image_platform_unsupported"`, `"image_too_large"` or `"image_unsupported"` | yes |  |
| `image.failure.message` | string | yes | 1 to 512 characters. Matches `^[^\u0000-\u001f\u007f-\u009f]+$`. |
| `command` | array of string or null | no | 1 to 64 items. |
| `shape` | string | no | Matches `^[a-z]-[0-9]+$`. |
| `ports` | array of object | no | At most 8 items. |
| `ports[].port` | integer | yes | 1 to 65535. |
| `ports[].access` | one of `"private"` or `"public"` | yes | Default `"private"`. |
| `ports[].default` | boolean | yes |  |
| `env` | object of string | no | At most 128 entries. Each key matches `^[A-Za-z_][A-Za-z0-9_]{0,127}$`. |
| `sleep_after_seconds` | integer | no | Default `600`. 30 to 86400. |
| `idle_action` | one of `"suspend"` or `"release"` | no | Default `"suspend"`. |
| `stop_grace_seconds` | integer | no | Default `10`. 0 to 230. |
| `database` | object | no |  |
| `database.name` | string | yes | Matches `^m\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`. |
| `database.url` | string | yes | Matches `^https://`. |
| `logs` | object | no |  |
| `logs.url` | string | yes | Matches `^https://`. |
| `state` | one of `"creating"`, `"active"`, `"deleting"` or `"failed"` | no |  |
| `failure` | object or null | no |  |
| `failure.code` | string | yes | Matches `^[a-z][a-z0-9_]*$`. |
| `failure.message` | string | yes | At least 1 characters. |
| `status` | object | no |  |
| `status.power` | one of `"asleep"`, `"waking"`, `"awake"` or `"sleeping"` | yes |  |
| `status.snapshot` | null | yes |  |
| `status.sleep` | object | yes |  |
| `status.sleep.action` | any | yes |  |
| `status.sleep.reason` | any | yes |  |
| `status.conditions` | array of object, object, object, object or object | yes |  |
| `status.awake` | object or null | yes |  |
| `status.awake.since` | string (date-time) | yes | Matches `Z$`. |
| `status.awake.connections` | object | yes |  |
| `status.awake.connections.https` | integer | yes | At least 0. |
| `status.awake.last_client_activity_at` | string (date-time) or null | yes | Matches `Z$`. |
| `status.awake.sleeps_at_earliest` | string (date-time) or null | yes | Matches `Z$`. |
| `status.awake.activation` | string | no | Matches `^[0-9a-f]{12}7[0-9a-f]{3}[89ab][0-9a-f]{15}$`. |
| `status.awake.processes` | object | no |  |
| `status.awake.processes.running` | integer | yes | At least 0. |
| `status.awake.processes.detached` | integer | yes | At least 0. |
| `status.awake.holds` | array of object | no |  |
| `status.awake.holds[].hold` | string | yes | Matches `^[a-z0-9._-]{1,64}$`. |
| `status.awake.holds[].source` | one of `"member"` or `"guest"` | yes |  |
| `status.awake.holds[].activation` | string | yes | Matches `^[0-9a-f]{12}7[0-9a-f]{3}[89ab][0-9a-f]{15}$`. |
| `status.awake.holds[].until` | string (date-time) | yes | Matches `Z$`. |
| `status.awake.holds[].actor` | object or null | yes |  |
| `status.awake.holds[].actor.sub` | string | yes | At least 1 characters. |
| `status.awake.holds[].actor.client_id` | string | yes | At least 1 characters. |
| `status.awake.holds[].created_at` | string (date-time) | yes | Matches `Z$`. |
| `status.awake.max_awake_until` | string (date-time) or null | no | Matches `Z$`. |
| `status.spec_current` | boolean | no |  |
| `status.delete_at` | string (date-time) or null | no | Matches `Z$`. |
| `activation` | object or null | no |  |
| `activation.id` | string | yes | Matches `^[0-9a-f]{12}7[0-9a-f]{3}[89ab][0-9a-f]{15}$`. |
| `activation.wake_gen` | integer | yes | 1 to 9007199254740991. |
| `activation.state` | one of `"admitted"`, `"funded"`, `"dispatched"`, `"starting"`, `"failing"`, `"running"`, `"stopping"` or `"suspending"` | yes |  |
| `activation.started_at` | string (date-time) | yes | Matches `Z$`. |
| `activation.revision` | integer | yes | At least 1. |
| `revision` | integer | no | At least 1. |
| `created_by` | string | no | Matches `^[a-z0-9._-]{1,64}$`. |
| `created_at` | string (date-time) | yes | Matches `Z$`. |
| `updated_at` | string (date-time) | yes | Matches `Z$`. |

**Example Machine**

```json
{
  "id": "<id>",
  "name": "<name>",
  "kind": "managed",
  "region": "<region>",
  "byoc": {
    "os": "darwin",
    "arch": "arm64",
    "client": "yield",
    "client_version": "<client_version>",
    "harnesses": [
      "claude-code"
    ],
    "last_seen": "2026-11-02T09:30:00Z",
    "key_state": "active",
    "hostname": "<hostname>",
    "device_id": "<device_id>"
  },
  "hostnames": {
    "https": "<https>",
    "api": "<api>"
  },
  "image": {
    "reference": "<reference>",
    "repository": "<repository>",
    "digest": "<digest>",
    "state": "pulling",
    "failure": {
      "code": "image_unavailable",
      "message": "<message>"
    }
  },
  "command": [
    "<command>"
  ],
  "shape": "<shape>",
  "ports": [
    {
      "port": 1,
      "access": "private",
      "default": false
    }
  ],
  "env": {},
  "sleep_after_seconds": 600,
  "idle_action": "suspend",
  "stop_grace_seconds": 10,
  "database": {
    "name": "<name>",
    "url": "<url>"
  },
  "logs": {
    "url": "<url>"
  },
  "state": "creating",
  "failure": {
    "code": "<code>",
    "message": "<message>"
  },
  "status": {
    "power": "asleep",
    "snapshot": null,
    "sleep": {
      "action": "<action>",
      "reason": "<reason>"
    },
    "conditions": [
      {
        "type": "wake_blocked",
        "reason": "payment_required",
        "since": "2026-11-02T09:30:00Z",
        "retry_at": "2026-11-02T09:30:00Z"
      }
    ],
    "awake": {
      "since": "2026-11-02T09:30:00Z",
      "connections": {
        "https": 0
      },
      "last_client_activity_at": "2026-11-02T09:30:00Z",
      "sleeps_at_earliest": "2026-11-02T09:30:00Z",
      "activation": "<activation>",
      "processes": {
        "running": 0,
        "detached": 0
      },
      "holds": [
        {
          "hold": "<hold>",
          "source": "member",
          "activation": "<activation>",
          "until": "2026-11-02T09:30:00Z",
          "actor": {
            "sub": "<sub>",
            "client_id": "<client_id>"
          },
          "created_at": "2026-11-02T09:30:00Z"
        }
      ],
      "max_awake_until": "2026-11-02T09:30:00Z"
    },
    "spec_current": false,
    "delete_at": "2026-11-02T09:30:00Z"
  },
  "activation": {
    "id": "<id>",
    "wake_gen": 1,
    "state": "admitted",
    "started_at": "2026-11-02T09:30:00Z",
    "revision": 1
  },
  "revision": 1,
  "created_by": "<created_by>",
  "created_at": "2026-11-02T09:30:00Z",
  "updated_at": "2026-11-02T09:30:00Z"
}
```

### MachineShape

A shape of contracts/machines/shapes-v1.json `shapes`, whether a Machine may name it now, and its list price.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `shape` | string | yes | The shape's name, which a Machine's `shape` names. Matches `^[a-z]-[0-9]+$`. |
| `vcpus` | integer | yes | The most vCPUs a Machine of this shape uses. At least 1. |
| `memory_gib` | number | yes | The most memory a Machine of this shape uses, in GiB. Greater than 0. |
| `scratch_gib` | number | yes | The most scratch disk a Machine of this shape uses, in GiB. Greater than 0. |
| `offered` | boolean | yes | Whether a Machine may name this shape now; a shape not offered answers 422 `shape_not_offered`. |
| `suspendable` | boolean | yes | Whether an idle Machine of this shape can suspend, keeping its memory to resume from. False while suspend is not offered, when an idle Machine is released instead. |
| `ceiling_usd_micros_per_hour` | integer | yes | The shape's list price for an hour at full use, in micros of USD: its vCPUs, memory and scratch disk at contracts/billing/machine-prices-v1.json's hourly prices. Billing charges what a Machine uses, never more than this. At least 0. |

**Example MachineShape**

```json
{
  "shape": "m-8",
  "vcpus": 1,
  "memory_gib": 1,
  "scratch_gib": 1,
  "offered": false,
  "suspendable": false,
  "ceiling_usd_micros_per_hour": 0
}
```
