| name | openhands-api |
| description | Reference skill for the OpenHands Cloud REST API (V1) and agent-server APIs, including how to start additional cloud or local backend conversations for fresh-context or delegated work. |
| triggers | ["openhands-api","openhands-api-v1","openhands-cloud","openhands-cloud-api-v1","oh-api-v1","oh-cloud-api-v1"] |
This skill documents the OpenHands Cloud API (V1), commonly used agent-server APIs, and small, easy-to-copy clients.
It is intentionally focused on common OpenHands API workflows:
- Defaults to OpenHands Cloud (
https://app.all-hands.dev).
- Targets the V1 app server REST API under
/api/v1/....
- Includes a few agent server endpoints (inside a sandbox) that use
X-Session-API-Key.
- Covers the multi-conversation delegation pattern: start separate Cloud conversations when you want fresh context windows or background work.
- Covers local Agent Canvas backend conversations: start or inspect conversations by calling a local agent server directly.
When to use this skill
Use this skill when you need to:
- start or inspect OpenHands Cloud conversations from code
- monitor async startup via start-task polling
- monitor execution status for long-running jobs
- create separate Cloud conversations for parallel or background work
- access sandbox agent-server endpoints once a conversation is running
- start or inspect conversations on a local Agent Canvas backend or local agent server
Auth
App server (Cloud)
Use Bearer auth:
- Header:
Authorization: Bearer <OPENHANDS_CLOUD_API_KEY>
- Preferred env var:
OPENHANDS_CLOUD_API_KEY
- Backward-compatible env var:
OPENHANDS_API_KEY
Agent server (inside a sandbox)
Use session auth:
- Header:
X-Session-API-Key: <session_api_key>
How to obtain agent_server_url and session_api_key:
- Start or fetch an app conversation via the app server (Bearer auth), e.g.:
POST /api/v1/app-conversations
- or
GET /api/v1/app-conversations?ids=<conversation_id>
- In the returned JSON, look for sandbox/runtime connection fields (names vary slightly by deployment/version). Common patterns:
- a sandbox object containing
agent_server_url (or similar)
- a session key such as
session_api_key (or similar)
- Use those values to call the agent server directly:
- Base:
{agent_server_url}/api/...
- Header:
X-Session-API-Key: <session_api_key>
Example (common field names; adjust to your deployment):
conv = api.app_conversation_get(app_conversation_id)
session_api_key = conv.get("session_api_key")
conversation_url = conv.get("conversation_url", "")
agent_server_url = conversation_url.rsplit("/api/conversations", 1)[0]
If those fields are not present on the conversation record, list/search sandboxes (GET /api/v1/sandboxes/search) and use the sandbox referenced by the conversation to locate the agent server URL + session key.
Local Agent Canvas backend
Use the local backend flow only for local Agent Canvas / agent-server development, such as agent-canvas, agent-canvas --backend-only, or npm run dev with ingress at http://localhost:8000. This calls the agent server directly with X-Session-API-Key. It is not an automation, and it is different from OpenHands Cloud delegation through POST /api/v1/app-conversations, which uses Bearer auth against the Cloud app API and may return asynchronous start-task records.
When Agent Canvas runs locally, the launcher uses LOCAL_BACKEND_API_KEY when it is set. Otherwise it generates and persists the session API key at ~/.openhands/agent-canvas/api-key.txt. Set OH_SESSION_API_KEY_PATH to override the persisted key path. Never print, log, or paste the actual key; use command substitution or an environment variable in examples and scripts.
LOCAL_AGENT_SERVER_URL="${LOCAL_AGENT_SERVER_URL:-http://localhost:8000}"
SESSION_API_KEY="${LOCAL_BACKEND_API_KEY:-$(cat "${OH_SESSION_API_KEY_PATH:-$HOME/.openhands/agent-canvas/api-key.txt}")}"
Check the local server before creating a backend conversation:
curl -sS "${LOCAL_AGENT_SERVER_URL}/server_info" \
-H "X-Session-API-Key: ${SESSION_API_KEY}"
Start a backend conversation with POST /api/conversations. Include the agent settings and workspace expected by that backend. Local agent-server calls use an explicit workspace such as {"kind": "LocalWorkspace", "working_dir": "/workspace"}; Cloud app-conversation delegation instead uses app-server fields such as selected_repository and selected_branch. If you are starting the conversation from an existing Agent Canvas session, pass through the current configured settings or encrypted settings rather than hard-coding secrets into scripts.
CONVERSATION_JSON=$(curl -sS -X POST "${LOCAL_AGENT_SERVER_URL}/api/conversations" \
-H "X-Session-API-Key: ${SESSION_API_KEY}" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"agent": {
"kind": "Agent",
"llm": {
"model": "your-model-provider/your-model-name",
"api_key": "**********"
},
"tools": [
{"name": "terminal"},
{"name": "file_editor"},
{"name": "task_tracker"}
]
},
"workspace": {"kind": "LocalWorkspace", "working_dir": "/workspace"},
"initial_message": {
"content": [{"text": "Summarize the current workspace."}],
"run": true
}
}
JSON
)
CONVERSATION_ID=$(python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' <<<"${CONVERSATION_JSON}")
printf 'Conversation: %s/api/conversations/%s\n' "${LOCAL_AGENT_SERVER_URL}" "${CONVERSATION_ID}"
Poll status and inspect recent events:
curl -sS "${LOCAL_AGENT_SERVER_URL}/api/conversations/${CONVERSATION_ID}" \
-H "X-Session-API-Key: ${SESSION_API_KEY}"
curl -sS "${LOCAL_AGENT_SERVER_URL}/api/conversations/${CONVERSATION_ID}/events/search?limit=20&sort_order=TIMESTAMP_DESC" \
-H "X-Session-API-Key: ${SESSION_API_KEY}"
If the same base URL serves the Agent Canvas UI, the browser route is:
printf '%s/conversations/%s\n' "${LOCAL_AGENT_SERVER_URL}" "${CONVERSATION_ID}"
Common V1 app server endpoints
The following are the main endpoints implemented in the minimal client:
GET /api/v1/users/me — validate auth and inspect current account
GET /api/v1/app-conversations/search?limit=... — list recent conversations
GET /api/v1/app-conversations?ids=... — fetch conversation records by id (batch)
GET /api/v1/app-conversations/count — count conversations
POST /api/v1/app-conversations — start a new conversation (creates a sandbox)
GET /api/v1/app-conversations/start-tasks?ids=... — check async start-task status
GET /api/v1/conversation/{app_conversation_id}/events/search?limit=... — read conversation events
GET /api/v1/conversation/{app_conversation_id}/events/count — count events
GET /api/v1/sandboxes/search?limit=... — list sandboxes
POST /api/v1/sandboxes/{sandbox_id}/pause / .../resume — manage sandbox lifecycle
GET /api/v1/app-conversations/{app_conversation_id}/download — download trajectory zip
Delegating work with additional Cloud conversations
Use the Cloud API when you want a separate OpenHands conversation with its own fresh context window.
This is useful for:
- background jobs that can run independently
- parallel investigations or implementation tasks
- long-running work where you want to keep the current conversation focused
- task-specific contexts, such as one conversation building a component while another runs tests
Delegation checklist
When you start a delegated Cloud conversation:
- Write a self-contained task description. Do not assume the new conversation has any context from the current one.
- Include the repository, branch, relevant file paths, constraints, and expected output.
- Start the new conversation with
POST /api/v1/app-conversations.
- Poll the start-task until
status is READY and you have an app_conversation_id.
- Monitor the delegated conversation via
GET /api/v1/app-conversations?ids=....
- Share or store the Cloud URL:
https://app.all-hands.dev/conversations/<app_conversation_id>.
Minimal cURL flow
curl -X POST "https://app.all-hands.dev/api/v1/app-conversations" \
-H "Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"initial_message": {
"content": [{"type": "text", "text": "Investigate flaky tests in tests/test_api.py. Report the root cause and propose a fix."}]
},
"selected_repository": "owner/repo"
}'
If the response does not already include app_conversation_id, poll the start-task:
curl -s "https://app.all-hands.dev/api/v1/app-conversations/start-tasks?ids=${START_TASK_ID}" \
-H "Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY}"
Then check execution status:
curl -s "https://app.all-hands.dev/api/v1/app-conversations?ids=${APP_CONVERSATION_ID}" \
-H "Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY}"
Minimal Python flow
from openhands_api import OpenHandsAPI
api = OpenHandsAPI()
start = api.app_conversation_start(
initial_message=(
"Implement the requested dashboard component in src/dashboard.tsx. "
"Update any related tests and summarize the changes."
),
selected_repository="owner/repo",
selected_branch="main",
title="Dashboard component task",
)
ready = start
if not ready.get("app_conversation_id"):
ready = api.poll_start_task_until_ready(start["id"])
conversation_id = ready["app_conversation_id"]
print(f"Delegated conversation: {api.base_url}/conversations/{conversation_id}")
status = api.app_conversation_get(conversation_id)
print(status.get("sandbox_status"), status.get("execution_status"))
api.close()
Parallelism guidance
- Prefer 5 or fewer concurrently running delegated conversations.
- Before starting more, check recent conversations and count how many are still
execution_status == "running".
- Batch specific conversation lookups with
GET /api/v1/app-conversations?ids=... when you already know their ids.
Example:
items = api.app_conversations_search(limit=50).get("items", [])
running = [item for item in items if item.get("execution_status") == "running"]
if len(running) >= 5:
print("Wait for some delegated conversations to finish before starting more.")
Start-task vs app_conversation_id (common pitfall)
In many deployments, POST /api/v1/app-conversations is asynchronous and returns a start-task object:
id is the start_task_id
app_conversation_id is the id you should use for conversation operations like:
GET /api/v1/app-conversations/{app_conversation_id}/download
GET /api/v1/conversation/{app_conversation_id}/events/...
If app_conversation_id is not present in the initial response, fetch it via:
GET /api/v1/app-conversations/start-tasks?ids=<start_task_id>
If you pass a start_task_id to /download, you will get 404 Not Found.
Common agent server endpoints
These run against agent_server_url (not the app server):
POST {agent_server_url}/api/bash/execute_bash_command
GET {agent_server_url}/api/file/download/<absolute_path>
POST {agent_server_url}/api/file/upload/<absolute_path> (multipart)
GET {agent_server_url}/api/conversations/{conversation_id}/events/search
GET {agent_server_url}/api/conversations/{conversation_id}/events/count
Counting events (recommended approach)
If you need to know how many events a conversation has, you can:
- App server count (fastest when working)
GET /api/v1/conversation/{app_conversation_id}/events/count
- Agent server count (reliable fallback)
GET {agent_server_url}/api/conversations/{app_conversation_id}/events/count
- Trajectory zip fallback (heavier, but still one call + gives full payloads)
GET /api/v1/app-conversations/{app_conversation_id}/download
- Unzip and count
event_*.json files
Do not rely on the last event id to infer the total number of events.
In the agent-server API, event IDs are UUIDs (not monotonically increasing integers).
Troubleshooting
For common issues and solutions, see TROUBLESHOOTING.md.
Event structure (for debugging)
Events returned by:
- app server:
GET /api/v1/conversation/{id}/events/search
- agent server:
GET {agent_server_url}/api/conversations/{id}/events/search
…share the same high-level shape.
Each event typically includes:
id (UUID)
timestamp
kind
source
Common kind values:
| kind | source (typical) | key fields (common) | purpose |
|---|
ActionEvent | agent | tool_name, tool_call_id, action | tool call requested by the agent |
ObservationEvent | environment | tool_name, tool_call_id, action_id, observation | tool result produced by the sandbox/environment |
MessageEvent | user / assistant | message (or similar) | user/assistant chat messages |
ConversationStateUpdateEvent | environment | key, value | state transitions/metadata |
Linking tool calls:
ActionEvent.tool_call_id == ObservationEvent.tool_call_id
ObservationEvent.action_id == ActionEvent.id
Example (simplified):
{
"id": "<action-event-uuid>",
"kind": "ActionEvent",
"source": "agent",
"tool_name": "terminal",
"tool_call_id": "toolu_...",
"action": {"command": "ls"}
}
{
"id": "<observation-event-uuid>",
"kind": "ObservationEvent",
"source": "environment",
"tool_name": "terminal",
"tool_call_id": "toolu_...",
"action_id": "<action-event-uuid>",
"observation": {"exit_code": 0, "stdout": "..."}
}
Debugging one-liners (events)
These assume you're querying the app server endpoint. For agent-server queries, swap the URL base + use X-Session-API-Key.
Print a quick timeline
curl -s "${BASE_URL:-https://app.all-hands.dev}/api/v1/conversation/${APP_CONVERSATION_ID}/events/search?limit=100" \
-H "Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY:-$OPENHANDS_API_KEY}" \
-H "Accept: application/json" | \
python3 - <<'PY'
import json, sys
items = (json.load(sys.stdin) or {}).get("items", [])
for i, e in enumerate(items):
print(f"{i:04d} {e.get('timestamp','')} {e.get('source','')} {e.get('kind','')}")
PY
Find error-like events
curl -s "${BASE_URL:-https://app.all-hands.dev}/api/v1/conversation/${APP_CONVERSATION_ID}/events/search?limit=200" \
-H "Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY:-$OPENHANDS_API_KEY}" \
-H "Accept: application/json" | \
python3 - <<'PY'
import json, sys
items = (json.load(sys.stdin) or {}).get("items", [])
for i, e in enumerate(items):
if e.get("kind") == "ErrorEvent" or ("code" in e and "detail" in e):
print(i, e.get("kind"), e.get("code"), str(e.get("detail", ""))[:400])
PY
Check tool-call matching (unmatched actions / duplicate observations)
curl -s "${BASE_URL:-https://app.all-hands.dev}/api/v1/conversation/${APP_CONVERSATION_ID}/events/search?limit=200" \
-H "Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY:-$OPENHANDS_API_KEY}" \
-H "Accept: application/json" | \
python3 - <<'PY'
import json, sys
from collections import Counter
items = (json.load(sys.stdin) or {}).get("items", [])
action_ids = {e.get("id") for e in items if e.get("kind") == "ActionEvent"}
obs_action_ids = [e.get("action_id") for e in items if e.get("kind") == "ObservationEvent" and e.get("action_id")]
observed = set(obs_action_ids)
print("actions:", len(action_ids))
print("observations:", len(observed))
unmatched = action_ids - observed
print("unmatched actions:", list(unmatched)[:20] if unmatched else "none")
dups = [aid for aid, c in Counter(obs_action_ids).items() if c > 1]
print("duplicate observation action_ids:", list(dups)[:20] if dups else "none")
PY
Quick start (Python)
from openhands_api import OpenHandsAPI
api = OpenHandsAPI()
me = api.users_me()
print(me)
recent = api.app_conversations_search(limit=5)
print(recent)
api.close()
CLI examples
Search conversations:
export OPENHANDS_CLOUD_API_KEY="..."
python skills/openhands-api/scripts/openhands_api.py search-conversations --limit 5
Start a conversation from a prompt file:
python skills/openhands-api/scripts/openhands_api.py start-conversation \
--prompt-file skills/openhands-api/references/example_prompt.md \
--repo owner/repo \
--branch main
Notes for AI agents extending this client
- Prefer
.../search endpoints with a small limit.
- Avoid loops that could generate many API calls.
- Start conversations only when asked: it may create sandboxes and cost money.
- For sandbox file operations and command execution, use the agent server endpoints with
X-Session-API-Key.
See also:
skills/openhands-api/scripts/openhands_api.py
- The original inspiration client:
enyst/llm-playground → openhands-api-client-v1/scripts/cloud_api_v1.py
- Troubleshooting content and real-world usage feedback →
https://github.com/jpshackelford/.openhands/tree/main/skills/openhands-cloud-api
Source of truth
This skill is aligned against the current OpenHands API docs and implementation:
OpenHands/docs/openhands/usage/cloud/cloud-api.mdx
OpenHands/docs/openhands/usage/agent-canvas/backend-setup/local.mdx
OpenHands/docs/sdk/arch/agent-server.mdx
OpenHands/docs/openhands/usage/api/v1.mdx
OpenHands/OpenHands/openhands/app_server/v1_router.py
OpenHands/OpenHands/openhands/app_server/app_conversation/app_conversation_router.py
OpenHands/OpenHands/openhands/app_server/app_conversation/app_conversation_models.py