Skip to content
DocsHaarness Cloud
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

  1. 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:

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 .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:

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-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) 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.