> ## Documentation index
> Fetch the complete documentation index at: https://haarness.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Cloud API

Everything you do in the Cloud app is also available over HTTP at `https://haarness.ai/api/v1`. This page walks
through a script that starts a workspace with a task, reads the answer, then schedules the task for every morning.
The full reference is the [OpenAPI document](https://haarness.ai/openapi.json). The [SDKs](https://haarness.ai/docs/cloud/cli-and-sdk.md#script-a-workspace-in-typescript-or-python)
wrap the same calls.

## Before you start

1. [Create an API key](https://haarness.ai/docs/cloud/cli-and-sdk.md#create-an-api-key) with the **Cloud automation** preset. It has
   every permission this page uses.
2. Put it in your environment as `HAARNESS_API_KEY`. Never put it in a URL or a log.

The examples use `curl`, `jq` and `uuidgen` in bash or zsh:

```sh
export HAARNESS=https://haarness.ai
auth=(-H "Authorization: Bearer $HAARNESS_API_KEY")
```

## Start a workspace with a task

```sh
CREATED=$(curl -sf "${auth[@]}" -X POST "$HAARNESS/api/v1/workspaces" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
  -d '{"title": "Nightly review", "initialPrompt": {"text": "Summarise the commits from yesterday."}}')
WORKSPACE=$(jq -r .workspace.id <<<"$CREATED")
CHAT=$(jq -r .workspace.launch.chatId <<<"$CREATED")
```

The answer (`202`) holds the new `workspace` and the `operation` that starts it. The workspace starts straight
away. Cloud sends the message to a new chat as soon as the workspace is ready, even if your script has already
exited. `$CHAT` is that chat.

| Field | Does |
| --- | --- |
| `title` | Names the workspace |
| `cloudAgentId` | Starts from a [Cloud agent](https://haarness.ai/docs/cloud/agents.md) |
| `start` | `false` creates the workspace stopped; it cannot take an `initialPrompt` then |
| `initialPrompt.text` | The first message, up to 32 KB |
| `initialPrompt.chatTitle` | The chat's name; by default, the start of the message |
| `initialPrompt.reasoningEffort` | Such as `low` or `high`; by default, the workspace's own |
| `initialPrompt.approvalMode` | `"auto"`, `"review"` or `"yolo"`; left out, the chat starts in Auto. See [permissions](https://haarness.ai/docs/security/permissions.md#review-auto-or-yolo-how-much-does-a-chat-ask) |

`configuration` and `environment` set the workspace's settings and secret environment values; the
[OpenAPI document](https://haarness.ai/openapi.json) describes them.

## Wait for the answer

```sh
while :; do
  STATUS=$(curl -sf "${auth[@]}" "$HAARNESS/api/v1/workspaces/$WORKSPACE" | jq -r .launch.status)
  case $STATUS in pending|running) sleep 10 ;; *) break ;; esac
done
echo "$STATUS"
```

| `launch.status` | Means |
| --- | --- |
| `pending` | The workspace is starting |
| `running` | The agent is working on it |
| `needs_attention` | The agent waits for your approval or an answer |
| `completed` | Done: read the answer |
| `failed`, `interrupted`, `cancelled` | It stopped; `launch.error` says why |

On `needs_attention`, open the chat in the Cloud app and answer there, or answer over the API as on a
[local server](https://haarness.ai/docs/server/api.md#approve-a-tool-call), using the ticket below.

The workspace's own state is in `observedState`: `starting`, `running`, `stopped`, `failed` and so on.

## Read the answer

A workspace's chats are reached through its own address, with a short-lived ticket:

```sh
ATTACH=$(curl -sf "${auth[@]}" -X POST "$HAARNESS/api/v1/workspaces/$WORKSPACE/attach")
GATEWAY=$(jq -r .gatewayUrl <<<"$ATTACH")
ticket=(-H "Authorization: Bearer $(jq -r .ticket <<<"$ATTACH")")

curl -sf "${ticket[@]}" "$GATEWAY/v1/threads/$CHAT/items" \
  | jq -r '.[] | select(.data.type == "assistant_message") | .data.text'
```

Attaching works while the workspace is running. If it has gone to sleep or was stopped, `POST` to its `/start` and
wait for `running` first. A ticket lasts up to eight hours (`expiresAt`) and stops working when the workspace
restarts or the key is rotated or revoked. Attach again whenever you need a new one.

With the ticket, the workspace answers like a [local Haarness server](https://haarness.ai/docs/server/api.md): the same calls reach its
chats, files, terminal and changes. Follow the agent live with `curl -N "${ticket[@]}" "$GATEWAY/v1/events"`.

## Send another message

```sh
TURN=$(curl -sf "${ticket[@]}" -X POST "$GATEWAY/v1/threads/$CHAT/turns" \
  -H "Content-Type: application/json" \
  -d "{\"text\": \"Which change is the riskiest?\", \"requestId\": \"$(uuidgen)\"}" | jq -r .id)
curl -sf "${ticket[@]}" "$GATEWAY/v1/turns/$TURN/wait?timeout_ms=120000" | jq -r .status
```

Then read the items again. A wait gives up after five minutes at most; call it again if the turn is still
running.

To open another chat in the same workspace, `POST "$GATEWAY/v1/threads"` with `{"title": "…"}` and use the `id` it
returns. A workspace's chats share its files.

For a workspace you started without `initialPrompt`, wait until `observedState` is `running`, then attach
and create a chat this way.

## Schedule it

Run the same prompt in that chat every weekday at 08:00, Paris time:

```sh
curl -sf "${auth[@]}" -X POST "$HAARNESS/api/v1/schedules" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
  -d "{\"workspaceId\": \"$WORKSPACE\", \"chatId\": \"$CHAT\", \"name\": \"Morning review\",
       \"prompt\": \"Summarise yesterday's commits.\", \"cron\": \"0 8 * * 1-5\", \"timezone\": \"Europe/Paris\"}"
```

| To run | Send |
| --- | --- |
| Once | `"at": "2026-11-02T09:00:00+01:00"` |
| Every few minutes or hours | `"everySeconds": 3600`, from 60 seconds to one year |
| On a calendar | `"cron": "0 8 * * 1-5"` and `"timezone": "Europe/Paris"` (UTC if you leave it out) |

To check a timing before you save it, `GET /api/v1/schedules/preview?cron=0%208%20*%20*%201-5&timezone=Europe/Paris`
returns the next runs (`count` up to 20). A run marked `"shifted": true` falls on a night when the clocks go
forward. See [Schedule prompts](https://haarness.ai/docs/cloud/schedules.md#time-zones-and-daylight-saving) for how time zones work.

| Endpoint | Does |
| --- | --- |
| `GET /api/v1/schedules` | Lists schedules; `?workspaceId=` for one workspace's |
| `GET /api/v1/schedules/{id}` | One schedule, with its next and last run |
| `PATCH /api/v1/schedules/{id}` | Pauses (`"enabled": false`), renames, changes the `prompt` or the `timing`; send the current `version` |
| `POST /api/v1/schedules/{id}/run` | Runs it now |
| `GET /api/v1/schedules/{id}/runs` | Run history, with each run's status and chat |
| `DELETE /api/v1/schedules/{id}` | Deletes it |

## Manage workspaces and Cloud agents

| Endpoint | Does |
| --- | --- |
| `GET /api/v1/workspaces` | Lists workspaces, newest first; every workspace is listed until it is deleted |
| `POST /api/v1/workspaces` | Creates one, as [above](https://haarness.ai/docs/cloud/api.md#start-a-workspace-with-a-task) |
| `GET /api/v1/workspaces/{id}` | One workspace, with its state and `launch` |
| `POST /api/v1/workspaces/{id}/stop` | Puts it to sleep; its files and chats are kept |
| `POST /api/v1/workspaces/{id}/start` | Wakes it up |
| `PATCH /api/v1/workspaces/{id}` | Renames it (`title`) or changes its settings |
| `DELETE /api/v1/workspaces/{id}` | Deletes the workspace with its files and chats, and frees the storage it used |
| `POST /api/v1/workspaces/{id}/save-agent` | Saves its settings as a Cloud agent (`name`, `version`) |
| `GET`, `POST /api/v1/workspaces/{id}/snapshots` | Lists its disk snapshots, or takes one |
| `POST /api/v1/workspaces/{id}/restore` | Puts a snapshot (`snapshotId`) back on a sleeping workspace's disk |
| `GET /api/v1/workspaces/{id}/operations` | Its starts, stops, snapshots and other operations, with their status |
| `GET /api/v1/cloud-agents` | Lists [Cloud agents](https://haarness.ai/docs/cloud/agents.md) |

A workspace keeps its files, and the storage they take, until you delete it; sleeping does not free storage.
Workspaces have no archived state. Changing a workspace's `configuration` or `environment` with `PATCH` needs
its current `version` and a sleeping workspace. Cloud agents are created, changed and deleted with `POST /api/v1/cloud-agents`, `PUT` and `DELETE /api/v1/cloud-agents/{id}`.

## Handle errors, retries and rate limits

- **Retry safely.** Send an `Idempotency-Key` header with every `POST` that creates or runs something. If a
  request times out, repeat it with the same key and body: you get the first answer instead of a second workspace.
  The same key with a different body fails with `idempotency_mismatch`.
- **Match errors on `code`.** Errors look like
  `{"code": "insufficient_scope", "message": "…", "requestId": "…", "retryable": false}`. The `message` is for
  people and can change. Retry only when `retryable` is `true`, and quote the `requestId` when you contact support.
- **Page through lists.** Lists return `{"items": […], "nextCursor": "…"}`. Pass `?cursor=` with the
  `nextCursor` to get the next page, until `nextCursor` is `null`.
- **Respect `429`.** Wait for the number of seconds in the `Retry-After` header, then try again.

| `code` | Status | Means |
| --- | --- | --- |
| `unauthenticated` | 401 | The key is wrong, expired or revoked |
| `insufficient_scope` | 403 | The key lacks the permission for this call |
| `not_found` | 404 | No such workspace, schedule or agent in this key's account |
| `invalid_state`, `conflict` | 409 | Not possible right now, such as attaching to a sleeping workspace or using a computer that is offline, or it changed since you read its `version` |
| `bad_gateway` | 502 | Your connected computer answered but could not do it; check Haarness on that computer |
| `insufficient_credits`, `budget_exceeded` | 402 | Out of credits, or over a spending limit |
| `rate_limited` | 429 | Too many requests |

## Permissions

Pick only what your script needs when you create the key:

| Permission | Lets the key |
| --- | --- |
| **Read workspaces** | List workspaces and Cloud agents and follow their progress |
| **Manage workspaces** | Create, start, stop, change and delete workspaces and Cloud agents |
| **Use chats and files** | Attach to a workspace: its chats, files, terminal and runtime settings |
| **Read schedules** | See schedules and their runs |
| **Manage schedules** | Create, change, run and delete schedules |
| **Browse models** | List models and their prices |
| **Call models** | Send model requests, within the key's **Model limits** |
| **Read remote hosts** | List connected computers ([remote hosts](https://haarness.ai/docs/cloud/remote-hosts.md)) and published chats |
| **Connect a remote host** | Publish a computer or selected chats through Cloud |
| **Control remote hosts** | Use their chats, files and terminals; rename or revoke remote access |
| **Read marketplace** | Read your account's private [Marketplace](https://haarness.ai/docs/cloud/marketplace.md) items |
| **Publish marketplace items** | Upload and submit Marketplace items for your account |
| **Manage routers** | Create [routers](https://haarness.ai/docs/cloud/models-and-routers.md) and manage their provider keys |
| **Read billing** | See balances, usage and invoices; it cannot change payments |

| Preset | Selects |
| --- | --- |
| **Models only** | **Browse models**, **Call models** |
| **Cloud automation** | **Read workspaces**, **Manage workspaces**, **Use chats and files**, **Read schedules**, **Manage schedules** |
| **Models and Cloud** | Both of the above |


A key belongs to one account. It stops working when it expires, when it is revoked, or when the person who
created it leaves the account or becomes a **Viewer**. Whatever its permissions, a key cannot create or change
API keys, change payment settings or manage members. An account can have up to 100 active keys.
