Skip to content
Back to skills

Openhands Api

ASecurity

Reference skill for the OpenHands Cloud REST API (V1), including how to start additional cloud conversations for fresh-context or delegated work. Use when you need to automate common OpenHands Cloud actions; don't use for general sandbox/dev tasks unrelated to the OpenHands API.

  • 151 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added June 5, 2026
toolspythonbashdebugginggitapi

Works with

  • terminal
  • cli
  • api

Security analysis

A100/100

Pro scans all 8 files and shows the line behind each finding

Scanned June 5, 2026

npx -y skills add OpenHands/extensions --skill openhands-api --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Openhands Api?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Openhands Api
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/openhands-openhands-api/badge)](https://www.skillsdirectory.com/skills/openhands-openhands-api)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: openhands-api
description: Reference skill for the OpenHands Cloud REST API (V1), including how to start additional cloud conversations for fresh-context or delegated work. Use when you need to automate common OpenHands Cloud actions; don't use for general sandbox/dev tasks unrelated to the OpenHands API.
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) and provides small, easy-to-copy clients.

It is intentionally focused on common OpenHands Cloud 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.

## 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

## 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`:

1. 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>`
2. 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)
3. 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):

```python
# 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.

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

1. Write a **self-contained task description**. Do not assume the new conversation has any context from the current one.
2. Include the **repository**, branch, relevant file paths, constraints, and expected output.
3. Start the new conversation with `POST /api/v1/app-conversations`.
4. Poll the start-task until `status` is `READY` and you have an `app_conversation_id`.
5. Monitor the delegated conversation via `GET /api/v1/app-conversations?ids=...`.
6. Share or store the Cloud URL: `https://app.all-hands.dev/conversations/<app_conversation_id>`.

### Minimal cURL flow

```bash
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:

```bash
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:

```bash
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

```python
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()
```

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

```python
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:

1. **App server count (fastest when working)**
   - `GET /api/v1/conversation/{app_conversation_id}/events/count`
2. **Agent server count (reliable fallback)**
   - `GET {agent_server_url}/api/conversations/{app_conversation_id}/events/count`
3. **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](references/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):

```json
{
  "id": "<action-event-uuid>",
  "kind": "ActionEvent",
  "source": "agent",
  "tool_name": "terminal",
  "tool_call_id": "toolu_...",
  "action": {"command": "ls"}
}
```

```json
{
  "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

```bash
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

```bash
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)

```bash
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)

```python
# 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()
```

## CLI examples

Search conversations:

```bash
export OPENHANDS_CLOUD_API_KEY="..."
python skills/openhands-api/scripts/openhands_api.py search-conversations --limit 5
```

Start a conversation from a prompt file:

```bash
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 V1 docs and implementation:

- `OpenHands/docs/openhands/usage/cloud/cloud-api.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`

Files in this skill

  • .plugin/plugin.json643 B
  • README.md2.1 KB
  • SKILL.md14.4 KB
  • references/README.md1.1 KB
  • references/TROUBLESHOOTING.md3 KB
  • references/example_prompt.md371 B
  • scripts/openhands_api.py20.5 KB
  • scripts/openhands_api.ts7.9 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.