diff --git a/docs.json b/docs.json
index 8233742c5..ad36c4128 100644
--- a/docs.json
+++ b/docs.json
@@ -94,7 +94,8 @@
"group": "Cloud API",
"pages": [
"openhands/usage/cloud/cloud-api",
- "openhands/usage/api/v1"
+ "openhands/usage/api/v1",
+ "openhands/usage/api/migration"
]
}
]
diff --git a/openhands/usage/api/migration.mdx b/openhands/usage/api/migration.mdx
new file mode 100644
index 000000000..4efb7cc14
--- /dev/null
+++ b/openhands/usage/api/migration.mdx
@@ -0,0 +1,698 @@
+---
+title: V0 to V1 Migration Guide
+description: Complete guide for migrating from OpenHands V0 APIs to V1 APIs
+---
+
+
+ The V0 API (`/api/conversations`) is deprecated and scheduled for removal on **April 1, 2026**.
+ Please migrate to the V1 API as soon as possible.
+
+
+## Overview
+
+This guide walks you through migrating from OpenHands V0 APIs to the new V1 APIs. We'll take you step-by-step through a typical migration, showing exactly what changes and why.
+
+### What's Different in V1?
+
+The most important change: **Conversations and Sandboxes are now decoupled**.
+
+In V0, creating a conversation automatically created and managed the underlying sandbox (runtime environment). In V1, sandboxes are independent resources you can:
+- Pause and resume without losing state
+- Reuse across multiple conversations
+- Manage with explicit lifecycle controls
+
+V1 also introduces two levels of API:
+- **App Server API** (`app.all-hands.dev`) - Manage conversations and sandboxes (this guide's focus)
+- **Agent Server API** (per-sandbox URL) - Direct access to workspace and runtime operations
+
+
+
+| Theme | Key Result |
+|-------|------------|
+| **Reliability** | 61% reduction in system failures — eliminated entire classes of infrastructure errors |
+| **Performance** | Lighter-weight runtime with co-located execution — removes inter-pod communication overhead |
+| **Developer Experience** | Modern API patterns — dedicated search, filtering, batch operations, webhooks, and flexible sandbox controls |
+
+**Reliability Details**
+- 61% reduction in system-attributable failures (78.0 → 30.0 errors per 1k conversations)
+- Eliminated infrastructure errors from inter-pod communication
+- Event-sourced state enables replay-based recovery
+
+**Performance Details**
+- Lighter-weight agent server reduces resource overhead
+- Independent sandbox lifecycle for better resource control
+
+**Developer Experience Details**
+- Pause/resume sandboxes, explicit status tracking (`STARTING`, `RUNNING`, `PAUSED`, `ERROR`)
+- Dedicated search and count endpoints
+- Batch operations and enhanced filtering
+- Webhook support for lifecycle events
+- Stream conversation updates from creation
+
+
+
+---
+
+## Migration Walkthrough
+
+Follow along as we migrate a typical V0 integration to V1. Each step shows your existing V0 code and the V1 equivalent.
+
+### Step 1: Create and Start a Conversation
+
+**V0 Approach**
+
+In V0, you called a single endpoint and got a conversation ID immediately:
+
+```bash
+curl -X POST "https://app.all-hands.dev/api/conversations" \
+ -H "Authorization: Bearer YOUR_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "initial_user_msg": "Check the README.md file",
+ "repository": "yourusername/your-repo"
+ }'
+```
+
+Response:
+```json
+{
+ "status": "ok",
+ "conversation_id": "abc1234"
+}
+```
+
+**V1 Approach**
+
+In V1, conversation startup is asynchronous—you get a start task that tracks initialization:
+
+```bash
+curl -X POST "https://app.all-hands.dev/api/v1/app-conversations" \
+ -H "Authorization: Bearer YOUR_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "initial_message": {
+ "content": [{"type": "text", "text": "Check the README.md file"}]
+ },
+ "selected_repository": "yourusername/your-repo"
+ }'
+```
+
+Response:
+```json
+{
+ "id": "start-task-uuid",
+ "status": "STARTING",
+ "conversation_id": null,
+ "sandbox_id": null,
+ "created_at": "2025-01-15T10:30:00Z"
+}
+```
+
+
+**Key Changes:**
+- Endpoint: `/api/conversations` → `/api/v1/app-conversations`
+- Field: `initial_user_msg` (string) → `initial_message` (object with content array)
+- Field: `repository` → `selected_repository`
+- The conversation ID isn't immediately available—see Step 2
+
+
+---
+
+### Step 2: Track Conversation Startup
+
+In V0, your conversation was ready immediately. In V1, you need to wait for initialization to complete. You have two options:
+
+**Option A: Poll for Readiness**
+
+```bash
+curl "https://app.all-hands.dev/api/v1/app-conversations/start-tasks?id={start_task_id}" \
+ -H "Authorization: Bearer YOUR_API_KEY"
+```
+
+Response when ready:
+```json
+{
+ "id": "start-task-uuid",
+ "status": "READY",
+ "conversation_id": "550e8400-e29b-41d4-a716-446655440000",
+ "sandbox_id": "sandbox-abc123",
+ "created_at": "2025-01-15T10:30:00Z"
+}
+```
+
+The `status` field tells you where things stand:
+- `STARTING` → Still initializing, keep polling
+- `READY` → Conversation is ready, use `conversation_id` for subsequent calls
+- `ERROR` → Something went wrong, check the `error` field
+
+**Option B: Stream from the Start**
+
+For real-time updates (recommended for user-facing apps), use the streaming endpoint:
+
+```bash
+curl -N "https://app.all-hands.dev/api/v1/app-conversations/stream-start" \
+ -H "Authorization: Bearer YOUR_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "initial_message": {
+ "content": [{"type": "text", "text": "Check the README.md file"}]
+ },
+ "selected_repository": "yourusername/your-repo"
+ }'
+```
+
+This streams server-sent events as the conversation initializes and begins processing. You'll receive status updates and can show progress to users immediately.
+
+
+Streaming is recommended for user-facing applications where you want to show progress immediately rather than waiting for initialization to complete.
+
+
+---
+
+### Step 3: Work with Events and Messages
+
+Once your conversation is ready, here's how to send messages and retrieve events.
+
+**Sending Messages**
+
+
+**Big improvement in V1:** In V0, sending messages to a running conversation typically requires establishing a WebSocket (Socket.IO) connection. V1 provides a simple REST endpoint on the Agent Server, making programmatic message sending much easier.
+
+
+
+
+ V0 requires a WebSocket connection to send messages to a running conversation. While a REST endpoint exists, the standard approach uses Socket.IO:
+
+ ```javascript
+ // V0 requires Socket.IO WebSocket connection
+ const socket = io(serverUrl, {
+ query: { conversation_id: id, latest_event_id: -1 }
+ });
+ socket.emit("oh_user_action", {
+ action: "message",
+ args: { content: "Now check the package.json" }
+ });
+ ```
+
+
+ V1 uses a simple REST call to the Agent Server (see [Step 5](#step-5-access-the-agent-server-when-needed) for how to get the Agent Server URL):
+
+ ```bash
+ curl -X POST "{agent_server_url}/api/conversations/{id}/events" \
+ -H "X-Session-API-Key: {session_api_key}" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "content": [{"type": "text", "text": "Now check the package.json"}],
+ "run": true
+ }'
+ ```
+
+ The `run: true` option automatically starts the agent loop to process your message.
+
+
+
+**Retrieving Events**
+
+V1 introduces dedicated search and count endpoints with enhanced filtering:
+
+
+
+ ```bash
+ # Get all events
+ curl "https://app.all-hands.dev/api/conversations/{id}/events" \
+ -H "Authorization: Bearer YOUR_API_KEY"
+ ```
+
+
+ ```bash
+ # Search events with filtering
+ curl "https://app.all-hands.dev/api/v1/conversation/{id}/events/search?limit=50" \
+ -H "Authorization: Bearer YOUR_API_KEY"
+
+ # Get event count (useful for pagination)
+ curl "https://app.all-hands.dev/api/v1/conversation/{id}/events/count" \
+ -H "Authorization: Bearer YOUR_API_KEY"
+
+ # Batch get specific events by ID
+ curl "https://app.all-hands.dev/api/v1/conversation/{id}/events?event_id=evt1&event_id=evt2" \
+ -H "Authorization: Bearer YOUR_API_KEY"
+ ```
+
+
+
+---
+
+### Step 4: Manage Sandbox Lifecycle
+
+This is entirely new in V1. You now have explicit control over sandbox resources.
+
+**Search for Your Sandboxes**
+
+```bash
+curl "https://app.all-hands.dev/api/v1/sandboxes/search?limit=10" \
+ -H "Authorization: Bearer YOUR_API_KEY"
+```
+
+**Pause a Sandbox** (stop billing, preserve state)
+
+```bash
+curl -X POST "https://app.all-hands.dev/api/v1/sandboxes/{sandbox_id}/pause" \
+ -H "Authorization: Bearer YOUR_API_KEY"
+```
+
+**Resume a Sandbox** (continue where you left off)
+
+```bash
+curl -X POST "https://app.all-hands.dev/api/v1/sandboxes/{sandbox_id}/resume" \
+ -H "Authorization: Bearer YOUR_API_KEY"
+```
+
+**Delete a Sandbox** (clean up resources)
+
+```bash
+curl -X DELETE "https://app.all-hands.dev/api/v1/sandboxes/{sandbox_id}" \
+ -H "Authorization: Bearer YOUR_API_KEY"
+```
+
+
+**Sandbox Status Values:**
+- `STARTING` - Sandbox is initializing
+- `RUNNING` - Sandbox is active and ready
+- `PAUSED` - Sandbox is suspended (no billing)
+- `ERROR` - Something went wrong
+- `MISSING` - Sandbox no longer exists
+
+
+---
+
+### Step 5: Access the Agent Server (When Needed)
+
+For direct workspace operations (file uploads, bash commands, git operations), you'll use the Agent Server API. This runs on each sandbox and requires a different authentication method.
+
+**Getting the Agent Server URL**
+
+First, get your sandbox details:
+
+```bash
+curl "https://app.all-hands.dev/api/v1/sandboxes?id={sandbox_id}" \
+ -H "Authorization: Bearer YOUR_API_KEY"
+```
+
+Response:
+```json
+{
+ "id": "sandbox-abc123",
+ "status": "RUNNING",
+ "session_api_key": "session-key-xyz",
+ "exposed_urls": {
+ "AGENT_SERVER": "https://sandbox-abc123.runtime.all-hands.dev",
+ "VSCODE": "https://vscode-abc123.runtime.all-hands.dev"
+ }
+}
+```
+
+**Making Agent Server Calls**
+
+Use the `session_api_key` with the `X-Session-API-Key` header:
+
+```bash
+# Execute a bash command
+curl -X POST "https://sandbox-abc123.runtime.all-hands.dev/api/bash/execute_bash_command" \
+ -H "X-Session-API-Key: session-key-xyz" \
+ -H "Content-Type: application/json" \
+ -d '{"command": "ls -la"}'
+
+# Download a file
+curl "https://sandbox-abc123.runtime.all-hands.dev/api/file/download/workspace/README.md" \
+ -H "X-Session-API-Key: session-key-xyz"
+
+# Upload a file
+curl -X POST "https://sandbox-abc123.runtime.all-hands.dev/api/file/upload/workspace/config.json" \
+ -H "X-Session-API-Key: session-key-xyz" \
+ -F "file=@local-config.json"
+```
+
+
+The Agent Server's OpenAPI spec is available at `{agent_server_url}/openapi.json` for complete endpoint documentation.
+
+
+---
+
+### Step 6: Handle Files and Workspace
+
+File operations have moved from the App Server to the Agent Server in V1.
+
+| V0 Operation | V1 Equivalent |
+|--------------|---------------|
+| `GET /api/conversations/{id}/list-files` | Agent Server: `POST /api/bash/execute_bash_command` with `ls` |
+| `GET /api/conversations/{id}/select-file` | Agent Server: `GET /api/file/download/{path}` |
+| `POST /api/conversations/{id}/upload-files` | Agent Server: `POST /api/file/upload/{path}` |
+| `GET /api/conversations/{id}/zip-directory` | Agent Server: `GET /api/file/download/{path}` for individual files |
+
+**Example: List Files in Workspace**
+
+```bash
+# Get Agent Server URL first (see Step 5), then:
+curl -X POST "https://sandbox-abc123.runtime.all-hands.dev/api/bash/execute_bash_command" \
+ -H "X-Session-API-Key: session-key-xyz" \
+ -H "Content-Type: application/json" \
+ -d '{"command": "find /workspace -type f -name \"*.py\" | head -20"}'
+```
+
+**Alternative: Use the Agent SDK**
+
+The [OpenHands Agent SDK](https://docs.openhands.dev/sdk) provides convenient methods that work with both V0 and V1:
+
+```python
+from openhands import Workspace
+
+workspace = Workspace(conversation_id="your-conversation-id")
+files = workspace.list_files()
+content = workspace.read_file("README.md")
+workspace.write_file("output.txt", "Hello, world!")
+```
+
+---
+
+### Step 7: Monitor Conversations and Fetch Events
+
+If you're building custom dashboards, CI/CD integrations, or alternative visualizations, you'll need to fetch conversation events. V1 provides a much more flexible approach than V0.
+
+**V0 Approach: Trajectory Download**
+
+In V0, you fetched all events at once using the trajectory endpoint:
+
+```bash
+curl "https://app.all-hands.dev/api/conversations/{id}/trajectory" \
+ -H "Authorization: Bearer YOUR_API_KEY"
+```
+
+This returned the entire conversation history in a single payload—simple, but inefficient for large conversations or real-time monitoring.
+
+**V1 Approach: Events Search API**
+
+V1 introduces a powerful events search endpoint with filtering, pagination, and sorting:
+
+```bash
+curl "https://app.all-hands.dev/api/v1/conversation/{id}/events/search?limit=100&sort_order=TIMESTAMP_DESC" \
+ -H "Authorization: Bearer YOUR_API_KEY"
+```
+
+Response:
+```json
+{
+ "items": [
+ {"id": "evt-123", "kind": "MessageEvent", "timestamp": "2025-01-15T10:30:05Z", ...},
+ {"id": "evt-122", "kind": "ActionEvent", "timestamp": "2025-01-15T10:30:03Z", ...}
+ ],
+ "next_page_id": "page_xyz"
+}
+```
+
+**Available filters:**
+- `limit` - Max results per page (up to 100)
+- `sort_order` - `TIMESTAMP` (oldest first) or `TIMESTAMP_DESC` (newest first)
+- `timestamp__gte` - Events at or after this time
+- `timestamp__lt` - Events before this time
+- `kind__eq` - Filter by event type (`MessageEvent`, `ActionEvent`, `ObservationEvent`, etc.)
+- `page_id` - For pagination through large result sets
+
+**Example: Fetch All Events (Bulk)**
+
+To fetch all events in a conversation (equivalent to V0 trajectory):
+
+```python
+import requests
+
+def fetch_all_events(conversation_id: str, api_key: str) -> list:
+ """Fetch all events from a conversation."""
+ base_url = "https://app.all-hands.dev"
+ headers = {"Authorization": f"Bearer {api_key}"}
+ all_events = []
+ page_id = None
+
+ while True:
+ params = {"limit": 100, "sort_order": "TIMESTAMP"}
+ if page_id:
+ params["page_id"] = page_id
+
+ response = requests.get(
+ f"{base_url}/api/v1/conversation/{conversation_id}/events/search",
+ params=params,
+ headers=headers
+ )
+ data = response.json()
+ all_events.extend(data["items"])
+
+ page_id = data.get("next_page_id")
+ if not page_id:
+ break
+
+ return all_events
+```
+
+**Example: Efficient Polling for Real-Time Monitoring**
+
+For dashboards or live displays, poll for only new events since your last fetch:
+
+```python
+import requests
+import time
+from datetime import datetime, timedelta
+
+def monitor_conversation(conversation_id: str, api_key: str, poll_interval: float = 1.0):
+ """Poll for new events in real-time."""
+ base_url = "https://app.all-hands.dev"
+ headers = {"Authorization": f"Bearer {api_key}"}
+ last_event_id = None
+ last_timestamp = None
+
+ while True:
+ params = {"limit": 100, "sort_order": "TIMESTAMP"}
+ if last_timestamp:
+ params["timestamp__gte"] = last_timestamp
+
+ response = requests.get(
+ f"{base_url}/api/v1/conversation/{conversation_id}/events/search",
+ params=params,
+ headers=headers
+ )
+
+ events = response.json()["items"]
+
+ for event in events:
+ # Skip if we've already seen this event
+ if event["id"] == last_event_id:
+ continue
+
+ # Process the new event (your visualization logic here)
+ print(f"[{event['kind']}] {event.get('timestamp')}")
+
+ last_event_id = event["id"]
+ last_timestamp = event["timestamp"]
+
+ time.sleep(poll_interval)
+```
+
+
+**Why V1 is better for monitoring:**
+- **Incremental fetches** - Only get new events using `timestamp__gte`, not the entire history
+- **Flexible filtering** - Filter by event type, time range, or paginate through results
+- **Lower latency** - Smaller payloads mean faster responses
+- **Works without active sandbox** - App Server events persist even when sandbox is paused
+
+
+---
+
+## Quick Reference: Endpoint Mapping
+
+Use this section to look up specific endpoint mappings when you need them.
+
+### Conversation Lifecycle
+
+| Operation | V0 Endpoint | V1 Endpoint |
+|-----------|-------------|-------------|
+| Create conversation | `POST /api/conversations` | `POST /api/v1/app-conversations` |
+| Start (streaming) | `POST /api/conversations/{id}/start` | `POST /api/v1/app-conversations/stream-start` |
+| Search conversations | `GET /api/conversations` | `GET /api/v1/app-conversations/search` |
+| Get by ID | `GET /api/conversations/{id}` | `GET /api/v1/app-conversations?id={id}` |
+| Get count | N/A | `GET /api/v1/app-conversations/count` |
+| Update | `PATCH /api/conversations/{id}` | `PATCH /api/v1/app-conversations/{id}` |
+| Delete | `DELETE /api/conversations/{id}` | `DELETE /api/v1/app-conversations/{id}` |
+
+### Events & Messages
+
+| Operation | V0 Endpoint | V1 Endpoint |
+|-----------|-------------|-------------|
+| Send message | WebSocket (Socket.IO) required | Agent Server: `POST /api/conversations/{id}/events` |
+| Get trajectory | `GET /api/conversations/{id}/trajectory` | `GET /api/v1/conversation/{id}/events/search` (paginated) |
+| Search events | `GET /api/conversations/{id}/events` | `GET /api/v1/conversation/{id}/events/search` |
+| Get event count | N/A | `GET /api/v1/conversation/{id}/events/count` |
+| Batch get events | N/A | `GET /api/v1/conversation/{id}/events` |
+
+### Sandbox Management (New in V1)
+
+| Operation | V1 Endpoint |
+|-----------|-------------|
+| Create sandbox | `POST /api/v1/sandboxes` |
+| Search sandboxes | `GET /api/v1/sandboxes/search` |
+| Get sandboxes | `GET /api/v1/sandboxes` |
+| Pause | `POST /api/v1/sandboxes/{id}/pause` |
+| Resume | `POST /api/v1/sandboxes/{id}/resume` |
+| Delete | `DELETE /api/v1/sandboxes/{id}` |
+
+### Development Tools
+
+| Operation | V0 Endpoint | V1 Equivalent |
+|-----------|-------------|---------------|
+| Get VSCode URL | `GET /api/conversations/{id}/vscode-url` | Get sandbox, find `VSCODE` in `exposed_urls` |
+| Get web hosts | `GET /api/conversations/{id}/web-hosts` | Get sandbox, find `AGENT_SERVER` in `exposed_urls` |
+
+### Health & Status (Unchanged)
+
+| Operation | Endpoint |
+|-----------|----------|
+| Health check | `GET /health` |
+| Alive check | `GET /alive` |
+| Ready check | `GET /ready` |
+
+---
+
+## Agent Server API Reference
+
+The Agent Server runs on each sandbox and provides direct access to workspace operations.
+
+### Authentication
+
+Use the `session_api_key` from your sandbox info with the `X-Session-API-Key` header.
+
+### Available Endpoints
+
+
+
+| Endpoint | Description |
+|----------|-------------|
+| `POST /api/file/upload/{path}` | Upload files to workspace |
+| `GET /api/file/download/{path}` | Download individual files |
+| `GET /api/file/download-trajectory/{conversation_id}` | Download conversation trajectory |
+
+
+
+
+
+| Endpoint | Description |
+|----------|-------------|
+| `GET /api/git/changes/{path}` | Get git changes |
+| `GET /api/git/diff/{path}` | Get git diff |
+
+
+
+
+
+| Endpoint | Description |
+|----------|-------------|
+| `POST /api/bash/execute_bash_command` | Execute bash command (blocking) |
+| `POST /api/bash/start_bash_command` | Start bash command (async) |
+| `GET /api/bash/bash_events/search` | Search bash events |
+
+
+
+
+
+| Endpoint | Description |
+|----------|-------------|
+| `POST /api/conversations/{id}/events` | Send user message |
+| `GET /api/conversations/{id}/events/search` | Search events |
+| `GET /api/conversations/{id}/events/count` | Count events |
+| `POST /api/conversations/{id}/pause` | Pause conversation |
+| `POST /api/conversations/{id}/run` | Resume/run conversation |
+| `DELETE /api/conversations/{id}` | Delete conversation |
+
+
+
+
+
+| Endpoint | Description |
+|----------|-------------|
+| `GET /api/vscode/url` | Get VSCode URL |
+| `GET /api/vscode/status` | Check VSCode status |
+| `GET /api/desktop/url` | Get desktop URL |
+| `GET /api/tools/` | List available tools |
+
+
+
+
+The Agent Server requires an active (running) sandbox. For operations on paused or inactive sandboxes, use the App Server API.
+
+
+### Accessing Agent Server API Documentation
+
+The Agent Server provides its own Swagger UI and OpenAPI specification, but you need a running sandbox to access them.
+
+**Step 1: Get the Agent Server URL**
+
+First, retrieve your sandbox info to get the Agent Server URL (see [Step 5](#step-5-access-the-agent-server-directly) above):
+
+```bash
+curl "https://app.all-hands.dev/api/v1/sandboxes?id={sandbox_id}" \
+ -H "Authorization: Bearer YOUR_API_KEY"
+```
+
+From the response, extract the `AGENT_SERVER` URL from `exposed_urls`.
+
+**Step 2: Access the Documentation**
+
+Once you have the Agent Server URL, you can access:
+
+| Resource | URL |
+|----------|-----|
+| Swagger UI | `{agent_server_url}/docs` |
+| OpenAPI JSON | `{agent_server_url}/openapi.json` |
+
+For example, if your Agent Server URL is `https://sandbox-abc123.runtime.all-hands.dev`:
+
+```bash
+# View the OpenAPI spec
+curl "https://sandbox-abc123.runtime.all-hands.dev/openapi.json" \
+ -H "X-Session-API-Key: session-key-xyz"
+
+# Or open in browser for interactive Swagger UI:
+# https://sandbox-abc123.runtime.all-hands.dev/docs
+```
+
+
+The Agent Server's Swagger UI doesn't require authentication to view, but you'll need the `X-Session-API-Key` header to execute requests.
+
+
+---
+
+## Known Gaps and Workarounds
+
+Some V0 capabilities don't have direct V1 App Server equivalents yet:
+
+| Gap | V0 Endpoint | Workaround |
+|-----|-------------|------------|
+| Download workspace as zip | `GET /api/conversations/{id}/zip-directory` | Use Agent Server to download individual files, or Agent SDK `workspace.get_workspace_zip()` |
+| Get trajectory (inactive runtime) | `GET /api/conversations/{id}/trajectory` | Trajectory available while sandbox is active via Agent Server |
+| List workspace files | `GET /api/conversations/{id}/list-files` | Use Agent Server `POST /api/bash/execute_bash_command` with `ls`, or Agent SDK `workspace.list_files()` |
+
+---
+
+## Authentication Summary
+
+| API | Authentication |
+|-----|----------------|
+| **App Server** (both V0 and V1) | API key via `Authorization: Bearer YOUR_API_KEY` header |
+| **Agent Server** | Session API key via `X-Session-API-Key: {session_api_key}` header |
+
+API keys are created via the settings page or `/api/keys` endpoint and work for both V0 and V1 App Server APIs.
+
+---
+
+## Additional Resources
+
+- [V1 REST API Overview](/openhands/usage/api/v1)
+- [Cloud API Guide](/openhands/usage/cloud/cloud-api)
+- [OpenHands Agent SDK](https://docs.openhands.dev/sdk) — Python SDK with `workspace.read_file()`, `workspace.write_file()`, `workspace.list_files()`, and more
+- [V1 API Swagger Docs](https://app.all-hands.dev/docs) — Interactive API documentation
+- [V1 API OpenAPI Spec](https://app.all-hands.dev/openapi.json) — OpenAPI JSON specification for the App Server