On this page
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. The SDKs
wrap the same calls.
Before you start
- Create an API key with the Cloud automation preset. It has every permission this page uses.
- 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:
export HAARNESS=https://haarness.ai
auth=(-H "Authorization: Bearer $HAARNESS_API_KEY")Start a workspace with a task
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 |
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 |
configuration and environment set the workspace's settings and secret environment values; the
OpenAPI document describes them.
Wait for the answer
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, 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:
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: the same calls reach its
chats, files, terminal and changes. Follow the agent live with curl -N "${ticket[@]}" "$GATEWAY/v1/events".
Send another message
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 .statusThen 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:
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 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 |
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 |
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-Keyheader with everyPOSTthat 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 withidempotency_mismatch. - Match errors on
code. Errors look like{"code": "insufficient_scope", "message": "…", "requestId": "…", "retryable": false}. Themessageis for people and can change. Retry only whenretryableistrue, and quote therequestIdwhen you contact support. - Page through lists. Lists return
{"items": […], "nextCursor": "…"}. Pass?cursor=with thenextCursorto get the next page, untilnextCursorisnull. - Respect
429. Wait for the number of seconds in theRetry-Afterheader, 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) 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 items |
| Publish marketplace items | Upload and submit Marketplace items for your account |
| Manage routers | Create routers 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.