From openhands-skills
Automates OpenHands Cloud REST API V1 for starting conversations, monitoring status, and delegating to fresh-context cloud sessions. Use for OpenHands Cloud workflows.
npx claudepluginhub openhands/extensionsThis skill uses the workspace's default tool permissions.
This skill documents the **OpenHands Cloud API** (V1) and provides small, easy-to-copy clients.
Provides curl examples to create, list, and manage Telnyx Missions—automated workflows and tasks for AI-driven telecom operations.
Manages terminal sessions for AI coding agents: create/start/stop/fork sessions, attach/detach MCPs, git worktree support, launch sub-agents via CLI/TUI commands.
Share bugs, ideas, or general feedback.
This skill documents the OpenHands Cloud API (V1) and provides small, easy-to-copy clients.
It is intentionally focused on common OpenHands Cloud workflows:
https://app.all-hands.dev)./api/v1/....X-Session-API-Key.Use this skill when you need to:
Use Bearer auth:
Authorization: Bearer <OPENHANDS_CLOUD_API_KEY>OPENHANDS_CLOUD_API_KEYOPENHANDS_API_KEYUse session auth:
X-Session-API-Key: <session_api_key>How to obtain agent_server_url and session_api_key:
POST /api/v1/app-conversationsGET /api/v1/app-conversations?ids=<conversation_id>agent_server_url (or similar)session_api_key (or similar){agent_server_url}/api/...X-Session-API-Key: <session_api_key>Example (common field names; adjust to your deployment):
# using the minimal Python client (`OpenHandsAPI`)
conv = api.app_conversation_get(app_conversation_id)
session_api_key = conv.get("session_api_key")
conversation_url = conv.get("conversation_url", "")
# `conversation_url` often looks like: https://<runtime-host>/api/conversations/<id>
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.
The following are the main endpoints implemented in the minimal client:
GET /api/v1/users/me — validate auth and inspect current accountGET /api/v1/app-conversations/search?limit=... — list recent conversationsGET /api/v1/app-conversations?ids=... — fetch conversation records by id (batch)GET /api/v1/app-conversations/count — count conversationsPOST /api/v1/app-conversations — start a new conversation (creates a sandbox)GET /api/v1/app-conversations/start-tasks?ids=... — check async start-task statusGET /api/v1/conversation/{app_conversation_id}/events/search?limit=... — read conversation eventsGET /api/v1/conversation/{app_conversation_id}/events/count — count eventsGET /api/v1/sandboxes/search?limit=... — list sandboxesPOST /api/v1/sandboxes/{sandbox_id}/pause / .../resume — manage sandbox lifecycleGET /api/v1/app-conversations/{app_conversation_id}/download — download trajectory zipUse the Cloud API when you want a separate OpenHands conversation with its own fresh context window. This is useful for:
When you start a delegated Cloud conversation:
POST /api/v1/app-conversations.status is READY and you have an app_conversation_id.GET /api/v1/app-conversations?ids=....https://app.all-hands.dev/conversations/<app_conversation_id>.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}"
from openhands_api import OpenHandsAPI
api = OpenHandsAPI() # prefers OPENHANDS_CLOUD_API_KEY
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()
execution_status == "running".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.")
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_idapp_conversation_id is the id you should use for conversation operations like:
GET /api/v1/app-conversations/{app_conversation_id}/downloadGET /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.
These run against agent_server_url (not the app server):
POST {agent_server_url}/api/bash/execute_bash_commandGET {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/searchGET {agent_server_url}/api/conversations/{conversation_id}/events/countIf you need to know how many events a conversation has, you can:
GET /api/v1/conversation/{app_conversation_id}/events/countGET {agent_server_url}/api/conversations/{app_conversation_id}/events/countGET /api/v1/app-conversations/{app_conversation_id}/downloadevent_*.json filesDo 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).
For common issues and solutions, see TROUBLESHOOTING.md.
Events returned by:
GET /api/v1/conversation/{id}/events/searchGET {agent_server_url}/api/conversations/{id}/events/search…share the same high-level shape.
Each event typically includes:
id (UUID)timestampkindsourceCommon 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_idObservationEvent.action_id == ActionEvent.idExample (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": "..."}
}
These assume you're querying the app server endpoint. For agent-server queries, swap the URL base + use X-Session-API-Key.
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
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
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
# Copy `skills/openhands-api/scripts/openhands_api.py` into your project (e.g. as `openhands_api.py`),
# then import it normally:
from openhands_api import OpenHandsAPI
api = OpenHandsAPI() # prefers OPENHANDS_CLOUD_API_KEY
me = api.users_me()
print(me)
recent = api.app_conversations_search(limit=5)
print(recent)
api.close()
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
.../search endpoints with a small limit.X-Session-API-Key.See also:
skills/openhands-api/scripts/openhands_api.pyenyst/llm-playground → openhands-api-client-v1/scripts/cloud_api_v1.pyhttps://github.com/jpshackelford/.openhands/tree/main/skills/openhands-cloud-api