# Orgo: full documentation > Launch cloud computers that AI agents can control and interact with. Every Orgo computer is a full desktop environment with a browser and the standard userland, reachable over HTTP and VNC. Pick the operating system at create time: Linux and Windows are available today, with more to come. A new computer is online in seconds and runs continuously until you stop it. Every page on `https://docs.orgo.ai`, in navigation order. The index is [llms.txt](https://docs.orgo.ai/llms.txt). REST base URL `https://www.orgo.ai/api`, spec version 2.0.0, 74 operations. Send `Authorization: Bearer $ORGO_API_KEY` on every request. --- # Introduction Source: https://docs.orgo.ai/introduction Summary: Cloud computers for AI agents. What Orgo is, what it is not, and the three ways to reach a computer. ## What Orgo is Launch cloud computers that AI agents can control and interact with. Every Orgo computer is a full desktop environment with a browser and the standard userland, reachable over HTTP and VNC. Pick the operating system at create time: Linux and Windows are available today, with more to come. A new computer is online in seconds and runs continuously until you stop it. People use Orgo computers in three common ways: - **Drive a computer with a model provider.** Wire an Anthropic, OpenAI, Google, or any OpenAI-compatible model to an Orgo computer and let it screenshot, click, type, and run commands. Useful for automating data entry, combining several tools into one interface, or driving software that doesn't expose an API. - **Install an agent inside the computer.** Run [OpenClaw](https://docs.orgo.ai/guides/openclaw), [Hermes Agent](https://docs.orgo.ai/guides/hermes), or any other CLI agent there so it has a persistent 24/7 home instead of a laptop. - **Run developer CLIs continuously.** Run Claude Code, Codex, or similar agentic CLIs on an Orgo computer so they stay online across sessions and can be reached from any device. A new computer in about fifteen seconds. The clip shows the dashboard flow. The same thing happens when an agent calls `POST /computers` from your code. **Quickstart** Launch a computer and control it in under five minutes. **Get an API key** Create an account and grab a key. ## What Orgo is not **Not a browser.** Tools like Browserbase give an agent a browser tab. Orgo gives it a full computer. That one computer can browse the web, save files, run code, and install desktop applications. Use a browser tool if you only need the browser. Use Orgo if you need a computer. **Not an AI agent.** Orgo provides the computer, not the agent. Bring Claude Computer Use, OpenAI's CUA, Hermes Agent, OpenClaw, or your own loop. Orgo is what they run on. **Not a general cloud platform.** Orgo runs one thing: persistent computers for AI agents. That focus shapes the rest. You get computers online in seconds, defaults tuned for computer use, and an API designed around what agents actually do. ## How it works There are three ways to reach a computer, and they all drive the same thing: - **[API](https://docs.orgo.ai/api-reference/introduction).** A plain HTTP API. Any language with an HTTP client works, and the Python and TypeScript SDKs wrap it. - **[CLI](https://docs.orgo.ai/guides/cli).** `orgo` from your terminal, for creating computers and driving them by hand. - **[MCP](https://docs.orgo.ai/guides/mcp).** An [MCP](https://modelcontextprotocol.io) server, so any MCP client controls your computers directly. The example below uses the API. It creates a computer, then hands it to an agent through the OpenAI-compatible endpoint. With no `model`, the computer runs Computer Use (`orgo-agent`). ```bash cURL # 1. Create a computer (one HTTP call). Set $WORKSPACE_ID to your workspace ID. curl -X POST https://www.orgo.ai/api/computers \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"workspace_id": "'"$WORKSPACE_ID"'", "name": "agent-1"}' # 2. Let an agent drive it (OpenAI-compatible, any SDK works). # Set $COMPUTER_ID to the "id" returned by step 1. curl https://www.orgo.ai/api/v1/chat/completions \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "computer_id": "'"$COMPUTER_ID"'", "messages": [ {"role": "user", "content": "Find a funny cat image and save it to the desktop"} ] }' ``` ```python Python import os from openai import OpenAI client = OpenAI(base_url="https://www.orgo.ai/api/v1", api_key=os.environ["ORGO_API_KEY"]) response = client.chat.completions.create( model="orgo-agent", messages=[{"role": "user", "content": "Find a funny cat image and save it to the desktop"}], extra_body={"computer_id": os.environ["COMPUTER_ID"]}, ) print(response.choices[0].message.content) ``` ```typescript TypeScript import OpenAI from "openai"; import type { ChatCompletionCreateParamsNonStreaming } from "openai/resources/chat/completions"; const client = new OpenAI({ baseURL: "https://www.orgo.ai/api/v1", apiKey: process.env.ORGO_API_KEY, }); // computer_id is an Orgo field. The SDK sends it as-is; add it to the type. const params: ChatCompletionCreateParamsNonStreaming & { computer_id: string } = { model: "orgo-agent", messages: [{ role: "user", content: "Find a funny cat image and save it to the desktop" }], computer_id: process.env.COMPUTER_ID!, }; const response = await client.chat.completions.create(params); console.log(response.choices[0].message.content); ``` The agent takes screenshots, clicks, types, and runs commands until the task is done. You can also call individual actions directly. See the [API Reference](https://docs.orgo.ai/api-reference/introduction) for every endpoint. The Python and TypeScript SDKs wrap this same surface. ## Agent loop Computer use agents operate in a loop: ```mermaid flowchart LR A["See"] --> B["Decide"] B --> C["Act"] C --> A ``` 1. **See.** Capture a screenshot. 2. **Decide.** The model analyzes the screen and chooses an action. 3. **Act.** Perform a mouse, keyboard, or shell action. 4. **Repeat.** Continue until the task is complete. ## Key capabilities **Fast boot** A new computer is online in seconds. **Full control** Mouse, keyboard, shell, and code execution. **Persistent state** Files, installed software, and configuration on disk survive stops and restarts. **Bring any model** Claude, GPT, Gemini, Hermes, or any OpenAI-compatible model. ## Get started **Quickstart** Launch your first computer. **For agents** Let Claude, Codex, or any agent use Orgo. **Use any model** Claude, GPT, Gemini, Hermes, and more. **Templates** Reproducible computers, defined once and launched in seconds. **Embed computers** Drop a live computer into your own app. **API Reference** Every endpoint, every field. --- # Quickstart Source: https://docs.orgo.ai/quickstart Summary: Your first computer in five minutes. Create it, then screenshot, click, type, and run commands over HTTP. This guide uses the **raw HTTP API**, the same surface the [CLI](https://docs.orgo.ai/guides/cli) wraps. Point `curl`, `requests`, or `fetch` at `https://www.orgo.ai/api` and you're done. **Info:** Prefer the terminal? The [Orgo CLI](https://docs.orgo.ai/guides/cli) wraps this same API with `orgo` commands and a live `orgo ssh` shell. ## Watch the getting started tutorial Watch the walkthrough to create your account and launch your first computer. [Watch Getting started with Orgo on YouTube](https://www.youtube.com/watch?v=CMn502tB8rI). **Video transcript** Hi, I'm Spencer, and this video is a quick introduction to getting set up with Orgo from the web dashboard. If you go to orgo.ai, you can explore the website and learn more. What I'm going to do is click the Start building button, which brings us to the dashboard. I logged in ahead of time, so I'm brought to a screen like this. To orient ourselves: on the top right there's a search bar, which lets you search your account resources and run quick actions in the app. There's also a profile dropdown, which gives you billing and account settings, and lets you sign out or switch accounts. On the top left there's a workspace selector. It lets you group and organize your computers and isolate access. There are also tabs for Home and Templates, and at the bottom left there's a New computer button and a link to the developer documentation. To get started, I'm going to click the Launch computer button to see what a computer looks like and some of the things you can do. As soon as I click it, our new remote virtual computer on Orgo pops up. If you click the three dots, there are a few common actions: settings, restart, duplicate, stop, and delete. If I click into the computer, you get a more complete view of it. There's Files, and you can click over to Terminal to run any terminal commands. By default, Orgo launches a Linux Ubuntu computer, but we also have other operating systems. You can launch Linux, Mac, or Windows computers on Orgo. Then there's the computer itself. One of the primary things you can do from the website is fully use and interact with this computer from the browser. If you click Open in new tab, the arrow at the top right, you can use the computer in full screen. Same with the terminal. For example, if you're setting up Hermes Agent, you can do it from a full screen terminal view, which makes it a lot easier. At the top right there's also the invite area. You can send a link to get someone into your workspace to use this computer. They need an Orgo account, but they don't need a paid subscription. If you pay for the computer, they only need a free account to access it. There's also some context, MCP, and CLI information here, as well as a mode to toggle the screen view. I'll show you a few more things before we wrap up. Click Home to go back to the view of your whole workspace. If I click Search, it shows you things you can do, and you can click New computer as a quick action to create more computers. You can also create a workspace. I'll call this one my new workspace. Workspaces separate out your computers. You can swap back and forth between them, and when you create a workspace, you can invite people and give them access to everything within it. You might do this if you're working on multiple coding projects, or you have multiple clients and want to separate access. One customer per workspace is a common way to set this up. There's also a Templates menu. You can build your own templates for these computers, or use a few that we provide out of the box. For example, this Hermes Agent template is a computer that comes bundled with all the software packages and the Hermes Agent installation. You get a full computer that boots in a few seconds with Hermes Agent and everything you need to run it already installed. All you do past that point is connect your API keys for the AI models inside the computer. Before we conclude: if you click the three dots on a computer, you get a lot more settings. You can show the latency to see your real-time connection, get audio, change the resolution, see which operating system you're running, get a full set of system metrics, change the hardware, view backups, and get SSH access. Hardware is something that makes Orgo really nice. You can live change the CPU, memory, storage, bandwidth, or other settings on the computer. For example, you might start a computer, get set up, and later decide you need more resources. You can scale up the hardware later on. That's a quick overview of the dashboard, and I hope it helps you get started. We'll create more videos that go through different parts of the platform. One other thing I forgot: we also have developer documentation. Everything you do from the dashboard, such as creating and managing computers, you can also do through the API. If you're building Orgo into your applications and want the full API to manage the lifecycle of these computers, you can do all of it through code instead of a user interface. That's all listed out in the docs. We'll create more videos covering the developer setup and the CLI. This was a quick video introducing you to the Orgo web dashboard. ## 1. Get your API key Create an account at [orgo.ai/start](https://www.orgo.ai/start), then create a key in [Settings → Credentials](https://www.orgo.ai/settings/credentials). **Note:** Signing up, creating workspaces, and creating API keys are free. Creating a computer in your own workspace requires a paid plan. See [pricing](https://www.orgo.ai/pricing). ```bash export ORGO_API_KEY=sk_live_... ``` Every request takes this as a bearer token: ```http Authorization: Bearer $ORGO_API_KEY ``` ## 2. Create a workspace Workspaces group related computers. ```bash cURL curl -X POST https://www.orgo.ai/api/workspaces \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "quickstart"}' ``` ```python Python import os, requests r = requests.post( "https://www.orgo.ai/api/workspaces", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, json={"name": "quickstart"}, ) workspace = r.json() print(workspace["id"]) ``` ```typescript TypeScript const r = await fetch("https://www.orgo.ai/api/workspaces", { method: "POST", headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ name: "quickstart" }), }); const workspace = await r.json(); console.log(workspace.id); ``` Response (abridged): ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "quickstart", "status": "active", "created_at": "2026-04-22T10:00:00Z" } ``` Save the `id`. You'll pass it as `workspace_id` when creating a computer. ## 3. Create a computer ```bash cURL curl -X POST https://www.orgo.ai/api/computers \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "workspace_id": "'"$WORKSPACE_ID"'", "name": "agent-1", "ram": 4, "cpu": 1 }' ``` ```python Python r = requests.post( "https://www.orgo.ai/api/computers", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, json={ "workspace_id": workspace["id"], "name": "agent-1", "ram": 4, "cpu": 1, }, ) computer = r.json() print(computer["id"], computer["status"]) ``` ```typescript TypeScript const r = await fetch("https://www.orgo.ai/api/computers", { method: "POST", headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ workspace_id: workspace.id, name: "agent-1", ram: 4, cpu: 1, }), }); const computer = await r.json(); console.log(computer.id, computer.status); ``` Set `$WORKSPACE_ID` to the `id` from step 2. Response (abridged): ```json { "id": "a3bb189e-8bf9-3888-9912-ace4e6543002", "name": "agent-1", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "os": "linux", "cpu": 1, "ram": 4, "resolution": "1280x720x24", "status": "running", "instance_id": "a3881618" } ``` The request returns once the computer is running, so it accepts commands right away. Save the `id` as `$COMPUTER_ID` for the cURL examples below. ## 4. Take a screenshot ```bash cURL curl "https://www.orgo.ai/api/computers/$COMPUTER_ID/screenshot?response_format=binary&format=png" \ -H "Authorization: Bearer $ORGO_API_KEY" \ -o screenshot.png ``` ```python Python # response_format=binary returns the image bytes directly. r = requests.get( f"https://www.orgo.ai/api/computers/{computer['id']}/screenshot", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, params={"response_format": "binary", "format": "png"}, ) with open("screenshot.png", "wb") as f: f.write(r.content) ``` ```typescript TypeScript // response_format=binary returns the image bytes directly. const r = await fetch( `https://www.orgo.ai/api/computers/${computer.id}/screenshot?response_format=binary&format=png`, { headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` } }, ); const png = Buffer.from(await r.arrayBuffer()); ``` Without `response_format`, the response is JSON whose `image` is a stored path that needs the same bearer token to download. See [Screenshot](https://docs.orgo.ai/api-reference/computers/screenshot). ## 5. Click, type, and run shell commands ```bash cURL # Click at (100, 200) curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/click \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"x": 100, "y": 200}' # Type text curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/type \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "Hello, world!"}' # Press Return (keys use X keysym names) curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/key \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"key": "Return"}' # Run a shell command curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/bash \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command": "ls -la"}' ``` ```python Python cid = computer["id"] base = f"https://www.orgo.ai/api/computers/{cid}" hdr = {"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"} requests.post(f"{base}/click", headers=hdr, json={"x": 100, "y": 200}) requests.post(f"{base}/type", headers=hdr, json={"text": "Hello, world!"}) requests.post(f"{base}/key", headers=hdr, json={"key": "Return"}) r = requests.post(f"{base}/bash", headers=hdr, json={"command": "ls -la"}) print(r.json()["output"]) ``` ```typescript TypeScript const cid = computer.id; const base = `https://www.orgo.ai/api/computers/${cid}`; const hdr = { Authorization: `Bearer ${process.env.ORGO_API_KEY}`, "Content-Type": "application/json", }; await fetch(`${base}/click`, { method: "POST", headers: hdr, body: JSON.stringify({ x: 100, y: 200 }) }); await fetch(`${base}/type`, { method: "POST", headers: hdr, body: JSON.stringify({ text: "Hello, world!" }) }); await fetch(`${base}/key`, { method: "POST", headers: hdr, body: JSON.stringify({ key: "Return" }) }); const bash = await fetch(`${base}/bash`, { method: "POST", headers: hdr, body: JSON.stringify({ command: "ls -la" }), }).then((x) => x.json()); console.log(bash.output); ``` Full list of actions: [Mouse](https://docs.orgo.ai/api-reference/computers/click) · [Keyboard](https://docs.orgo.ai/api-reference/computers/type) · [Scroll](https://docs.orgo.ai/api-reference/computers/scroll) · [Bash](https://docs.orgo.ai/api-reference/computers/bash) · [Python exec](https://docs.orgo.ai/api-reference/computers/exec) · [Wait](https://docs.orgo.ai/api-reference/computers/wait) · [Drag](https://docs.orgo.ai/api-reference/computers/drag). ## 6. Let an AI drive it Orgo has an OpenAI-compatible endpoint at `/api/v1/chat/completions`. Point any OpenAI SDK at `https://www.orgo.ai/api/v1` and pass `computer_id`. The agent screenshots, clicks, and types on its own until your instruction is done. Leave out `model` and the computer runs Computer Use (`orgo-agent`). The OpenAI SDKs require `model`, so the SDK examples name it. ```bash cURL curl https://www.orgo.ai/api/v1/chat/completions \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "computer_id": "'"$COMPUTER_ID"'", "messages": [ {"role": "user", "content": "Open Chrome and search for AI news"} ] }' ``` ```python Python from openai import OpenAI client = OpenAI( base_url="https://www.orgo.ai/api/v1", api_key=os.environ["ORGO_API_KEY"], ) response = client.chat.completions.create( model="orgo-agent", messages=[{"role": "user", "content": "Open Chrome and search for AI news"}], extra_body={"computer_id": computer["id"]}, ) print(response.choices[0].message.content) ``` ```typescript TypeScript import OpenAI from "openai"; import type { ChatCompletionCreateParamsNonStreaming } from "openai/resources/chat/completions"; const client = new OpenAI({ baseURL: "https://www.orgo.ai/api/v1", apiKey: process.env.ORGO_API_KEY, }); // computer_id is an Orgo field. The SDK sends it as-is; add it to the type. const params: ChatCompletionCreateParamsNonStreaming & { computer_id: string } = { model: "orgo-agent", messages: [{ role: "user", content: "Open Chrome and search for AI news" }], computer_id: computer.id, }; const response = await client.chat.completions.create(params); console.log(response.choices[0].message.content); ``` Stream the agent's text as it works with `"stream": true`. See [Create chat completion](https://docs.orgo.ai/api-reference/chat/completions) for the full reference: models, images, step limits, threads, your own Anthropic key, and errors. ## 7. Lifecycle ```bash # Stop curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/stop \ -H "Authorization: Bearer $ORGO_API_KEY" # Start a stopped computer curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/start \ -H "Authorization: Bearer $ORGO_API_KEY" # Restart (reboot) curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/restart \ -H "Authorization: Bearer $ORGO_API_KEY" # Delete permanently curl -X DELETE https://www.orgo.ai/api/computers/$COMPUTER_ID \ -H "Authorization: Bearer $ORGO_API_KEY" ``` Stopped computers cold-boot from their saved disk on the next `start`. **Warning:** In-memory state is not preserved across a stop. A computer that is stopped and started again can land on a different host with a new address and ports. ## Next steps **For agents** Let Claude, Codex, or any agent use Orgo. **API Reference** Every endpoint, every field. **Use any model** Claude, GPT, Gemini, Hermes. Any OpenAI-compatible model. **Embed computers** Drop a live computer into your own app via VNC. --- # For agents Source: https://docs.orgo.ai/for-agents Summary: Give an AI agent access in one approval. How Claude, Codex, or any agent connects to Orgo, creates a computer, and uses it. An AI agent needs three things: access, a computer, and a way to use it. This page covers each in the fewest steps. **Tip:** Give your agent [orgo.ai/orgo.md](https://www.orgo.ai/orgo.md). It is this whole flow as one markdown file. For the full docs as text, use [llms.txt](https://docs.orgo.ai/llms.txt) or [llms-full.txt](https://docs.orgo.ai/llms-full.txt). ## Get access Pick one. Each needs a person to approve once. | Way | Use it when | The person | | --- | --- | --- | | [MCP](#connect-over-mcp) | The agent runs in an MCP client: Claude Code, Claude Desktop, Cursor, Codex | Signs in and picks a workspace | | [Device approval](#get-an-api-key-with-device-approval) | The agent can make HTTP calls or run a shell | Approves a short code | | [Settings](https://docs.orgo.ai/api-reference/authentication) | A server or CI job needs a key | Creates and copies a key | ### Connect over MCP Add the hosted server. No key to copy. ```bash claude mcp add --transport http orgo https://www.orgo.ai/mcp ``` In any other client, add `https://www.orgo.ai/mcp` as a remote MCP server. The client opens Orgo in a browser. The person signs in, chooses a workspace, and approves. The connection can create and use computers in that workspace. See [MCP](https://docs.orgo.ai/guides/mcp) for the tools and for local setups. ### Get an API key with device approval The agent starts a request, the person approves it in a browser, and the agent collects an account-wide API key. No key is ever pasted into a chat. **1. Start.** No auth needed. ```bash curl -X POST https://www.orgo.ai/api/cli/auth/start \ -H "Content-Type: application/json" \ -d '{"client": "my-agent", "hostname": "laptop"}' ``` ```json { "device_code": "c893fbd6-60c8-449b-8298-6557ea306fa5", "user_code": "6K2-UY4-2Q6", "verification_uri": "https://www.orgo.ai/cli/approve", "verification_uri_complete": "https://www.orgo.ai/cli/approve?code=6K2-UY4-2Q6", "interval_seconds": 2, "expires_in_seconds": 600 } ``` **2. Ask the person to approve.** Send them `verification_uri_complete`. They sign in and approve within 10 minutes. **3. Poll** every `interval_seconds` with the `device_code`. ```bash curl -X POST https://www.orgo.ai/api/cli/auth/poll \ -H "Content-Type: application/json" \ -d '{"device_code": "c893fbd6-60c8-449b-8298-6557ea306fa5"}' ``` `status` is `pending` until the person acts, then one of: | `status` | Next step | | --- | --- | | `approved` | Read `api_key` and keep it secret. It is returned once. | | `denied` | Stop. The person said no. | | `expired` | Start again. | ```json { "status": "approved", "api_key": "sk_live_...", "key_id": "...", "user": { "id": "...", "email": "person@example.com" } } ``` The key is named "CLI on" plus the `hostname` you sent. The person can revoke it in [Settings → Credentials](https://www.orgo.ai/settings/credentials). **The whole flow as one script** ```bash start=$(curl -s -X POST https://www.orgo.ai/api/cli/auth/start \ -H "Content-Type: application/json" \ -d "{\"client\": \"my-agent\", \"hostname\": \"$(hostname)\"}") echo "Approve here: $(echo "$start" | jq -r .verification_uri_complete)" device_code=$(echo "$start" | jq -r .device_code) while :; do sleep 2 poll=$(curl -s -X POST https://www.orgo.ai/api/cli/auth/poll \ -H "Content-Type: application/json" \ -d "{\"device_code\": \"$device_code\"}") status=$(echo "$poll" | jq -r .status) [ "$status" != "pending" ] && break done [ "$status" = "approved" ] || { echo "Not approved: $status"; exit 1; } export ORGO_API_KEY=$(echo "$poll" | jq -r .api_key) ``` An agent that can run the [CLI](https://docs.orgo.ai/guides/cli) gets the same flow from `orgo login`. ## Make sure the account has a plan Accounts are free. Creating a computer needs a paid plan. If the account has none, the agent can get a checkout link for the person: ```bash curl -X POST https://www.orgo.ai/api/checkout \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"plan": "hacker_v2"}' ``` `plan` is `hacker_v2` (Starter), `startup_v2` (Growth), or `scale_v2` (Scale). The response has a Stripe Checkout `url`. Send it to the person to pay. Prices are in [pricing.md](https://www.orgo.ai/pricing.md). **Warning:** Call checkout only for an account with no plan. With an API key, a call on an account that already has a plan changes that plan without asking. ## Create a computer Find a workspace. The list is under `workspaces`. ```bash curl https://www.orgo.ai/api/workspaces \ -H "Authorization: Bearer $ORGO_API_KEY" ``` If it is empty, create one with `POST /workspaces` and `{"name": "agents"}`. Then create the computer: ```bash curl -X POST https://www.orgo.ai/api/computers \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"workspace_id": "'"$WORKSPACE_ID"'", "name": "agent-1"}' ``` The request returns when the computer is running. Its `id` is the `$COMPUTER_ID` for every call below. Set `os` to `windows` for Windows. See [Create computer](https://docs.orgo.ai/api-reference/computers/create) for sizes and templates. ## Use the computer Run commands and see the screen directly: ```bash # Run a shell command curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/bash \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command": "uname -a"}' # Save a screenshot curl "https://www.orgo.ai/api/computers/$COMPUTER_ID/screenshot?response_format=binary&format=png" \ -H "Authorization: Bearer $ORGO_API_KEY" -o screen.png # Click, then type curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/click \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"x": 640, "y": 400}' curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/type \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "hello"}' ``` Or hand over a whole task. Orgo's Computer Use agent screenshots, clicks, and types until it is done: ```bash curl https://www.orgo.ai/api/v1/chat/completions \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "computer_id": "'"$COMPUTER_ID"'", "messages": [{"role": "user", "content": "Open Chrome and find the weather in Paris"}] }' ``` The endpoint is OpenAI-compatible. See [Create chat completion](https://docs.orgo.ai/api-reference/chat/completions). ## Reference for agents | What | Where | | --- | --- | | The whole flow in one file | [orgo.ai/orgo.md](https://www.orgo.ai/orgo.md) | | Authentication, OAuth discovery | [orgo.ai/auth.md](https://www.orgo.ai/auth.md) | | OpenAPI spec | [orgo.ai/openapi.json](https://www.orgo.ai/openapi.json) | | Plans and limits | [orgo.ai/pricing.md](https://www.orgo.ai/pricing.md) | | Docs index | [docs.orgo.ai/llms.txt](https://docs.orgo.ai/llms.txt) | | Claude Code skill | [Skill](https://docs.orgo.ai/guides/skill) | --- # CLI Source: https://docs.orgo.ai/guides/cli Summary: Drive Orgo computers from your terminal. Install, sign in, create computers, and run agents. The Orgo CLI gives you complete control over your cloud computers from the terminal: launch and manage workspaces and computers, open an interactive shell, run multi-turn AI agent sessions, and pipe machine-readable output into any other tool. ## Watch the CLI setup tutorial Watch the walkthrough to install the CLI, sign in, create a computer, and hand the CLI to a coding agent. [Watch Orgo CLI setup on YouTube](https://www.youtube.com/watch?v=g3IXtMHgZQo). **Video transcript** This is a quick video on installing and setting up the Orgo CLI. If you haven't already, go to orgo.ai and create an account. You can see I have my account open here, and I'm in a workspace that has one computer. The first thing you want to do is go over to docs.orgo.ai and click into the CLI page. This page shows you the instructions and explains the specification for our CLI and everything it has available. For the CLI and everything else in our API documentation, you'll also see llms-full.txt on the bottom left. You can click into it and give it to Claude Code, Codex, or any coding agent for full context on how to install and use everything. Back on the CLI page, you can see all of these commands and the basic usage. I'll copy the one-line install command and run it. This usually takes a bit longer, but I already have it installed. I'm going to log out first to show you what it looks like to log back in. If you're already logged in to your Orgo account in the browser, this is simple. Type `orgo login` and it gives you a temporary code that expires in 10 minutes. Click Approve, then go back to the CLI. You can see it's authenticated now, and if you type `orgo`, it's there and ready to go. You can see a few of the common commands here, and I'll quickly show you some of the things you can run from the CLI. For every one of these commands, like workspaces, computers, templates, secrets, and SSH, you can type help to get the full set of what you can run with that command. Say `orgo computers help` and it gives you all of the options. There are a lot of options under the computers command, and you can control basically the entire computer lifecycle from the CLI. The first thing we want to do is target our workspace. The command to switch to a workspace is `orgo workspaces use`. We have my workspace here, and we can pass its name in quotes. Now we've switched over to my workspace, and if you run `orgo computers list`, you can see the one computer I have running in this workspace. From here I'll run a couple of quick commands. We can run `orgo computers create` and add `--help` to see the full set of inputs. We only need a few: `--name`, and we'll call this computer 123, then 8 GB of RAM and 2 CPUs. That should be all we need to create a computer, so I'll run it. Within a few seconds it's created, and you can see the new computer popped up in this workspace. That's a simple example of setting up the CLI and creating a new computer. One last thing I'll show you. If you go back to `orgo`, one of the commands is `orgo manifest`. It prints a machine-readable list of every command in the CLI. Let's say you use Claude Code. I'll open Claude Code here and trust this folder. Then I'll say: "Hey, I want to use the Orgo CLI with the orgo commands. You can first run orgo manifest to understand the CLI format." You can paste a prompt like this into a Claude Code or Codex session to give it full context on how to use and interact with the CLI, through any coding agent such as Claude, Codex, or even Hermes. Once you paste it in, it tells you what computers you have and shows you everything it can do from here. That was a simple introduction to installing and setting up the Orgo CLI, logging in, creating a computer, and giving the context to a coding agent. I hope that was helpful. ## Install ```bash macOS / Linux curl -fsSL https://orgo.ai/install.sh | bash ``` ```bash npm npm install -g orgo ``` The npm package requires Node 20 or later. The install script needs no system Node: it downloads its own into `~/.orgo/node`. No `sudo` needed. ## Sign in ```bash orgo login ``` Opens your browser, completes a device-code flow, and saves a credential under `~/.orgo/credentials.json` (mode `0600`). To use a separate identity in dev, set `ORGO_PROFILE=dev`. ## Commands ```text orgo Show account, workspace, and computer counts orgo login / logout / whoami Account management orgo workspaces (ws) list · create · use · current · rename · delete orgo computers (c) list · create · start · stop · restart · resize · delete bash · exec · clone · rename · wait · screenshot · view · vnc-password orgo ssh Interactive shell on a computer orgo templates list · show · run · build · validate · publish · schema · delete orgo files list · upload · download · export · delete orgo secrets list · set · delete orgo agent Multi-turn AI agent TUI (resumable sessions) orgo run "" One-shot agent run orgo resume [id] / sessions Resume or list agent sessions orgo api-keys Manage API keys orgo billing balance · topup · history · portal orgo completion Print a bash, zsh, or fish completion script orgo manifest Print every command and option as JSON ``` Every command accepts `--json` for machine-readable output and `--help` for details. When stdout isn't a terminal, output switches to plain text with no prompts. For JSON, pass `--json` or set `ORGO_JSON=1`. ## Give a coding agent the command list `orgo manifest` prints every command and option as JSON, with aliases and defaults. Tell Claude Code, Codex, or another coding agent to run it before it uses the CLI, so it works from the real commands and flags instead of guessing. ```bash orgo manifest ``` ## Create a computer ```bash orgo computers create ``` Interactive by default. It prompts for workspace, name, RAM, and CPU with input validation. For scripts and CI, pass everything as flags: ```bash orgo computers create \ --workspace my-workspace \ --name agent-1 \ --ram 4 \ --cpu 1 ``` | Flag | Values | |---|---| | `--ram ` | 4, 8, 16, 32, 64 | | `--cpu ` | 0.5, 1, 2, 4, 8, 16 | | `--disk ` | Up to your plan's per-computer limit (grows only on resize) | | `--os ` | linux (default), windows, macos, android | | `--gpu ` | 2q (2 GB) or 4q (4 GB), Linux-only. Falls back to CPU (with a warning) if no GPU host is free | | `--region ` | Region hint, comma-separated | | `--resolution ` | Defaults to `1280x720x24` (`1920x1080x24` on macOS). Change it and your agent's coordinate space changes with it | | `--snapshot ` | Boot from a snapshot or custom image | | `--template ` | Launch from a template by name or ref, e.g. `hermes-agent` or `system/hermes-agent@1.1.13` | Resize a running computer with `orgo computers resize --ram 8 --cpu 4 --disk 64` (disk grows only; live RAM resize caps at 32 GB). ## SSH ```bash orgo ssh computer-name ``` Opens an interactive terminal session over WebSocket. Lands you in `/root/Desktop` by default (override with `--cwd`). Standard SSH disconnect sequences: | Sequence | Action | |----------------------------------|----------------------------------------| | `~.` (at start of line) | OpenSSH-style disconnect | | `Ctrl+C` `Ctrl+C` (within 800ms) | Discoverable disconnect | | `Ctrl+D` or `exit` | Standard shell exit | | Single `Ctrl+C` | Forwarded to remote (interrupts process) | ## Agent TUI ```bash orgo agent ``` Multi-turn agent session with full conversation history, tool-call visibility, and persistent sessions. Inside the TUI: | Slash command | Action | |----------------|-------------------------------------------------------| | `/help` | List slash commands | | `/computer` | Switch the agent to a different computer (picker) | | `/model` | Switch model: Sonnet 5 (default) or Opus 5 | | `/sessions` | List saved sessions | | `/resume` | Resume a prior session (picker) | | `/compact` | Summarize older turns to shrink context | | `/usage` | Token usage for this session | | `/screenshot` | Save the live screenshot and open it | | `/save ` | Set the session title | Sessions auto-persist to `~/.orgo/sessions/` after every turn. Ctrl+C loses nothing. ### Resume a session ```bash orgo resume # interactive picker orgo resume # direct orgo sessions # list saved sessions ``` ## One-shot run For scripts and CI: ```bash orgo run "open chrome and search for hermes" --computer agent-1 ``` Streams text, tool calls, and tool results to stdout. Add `--json` to get a single structured object at the end: ```json { "text": "Chrome is open on the Google homepage…", "tool_calls": [{ "id": "toolu_…", "name": "computer", "input": { … } }], "tool_results": [{ "tool_use_id": "toolu_…", "display": "[screenshot]" }], "errors": [], "usage": { "prompt_tokens": 1420, "completion_tokens": 312, "total_tokens": 1732 }, "computer": { "id": "…", "name": "agent-1" } } ``` ## Run commands on a computer One-shot actions for scripts and CI: ```bash orgo computers bash agent-1 -c "ls -la" # shell command (or pipe via stdin) orgo computers exec agent-1 --code "print(2+2)" # Python (--file or stdin also work) orgo computers screenshot agent-1 -o shot.png # save a PNG orgo computers view agent-1 # show the screen in your terminal orgo computers clone agent-1 # duplicate, preserving disk state orgo computers rename agent-1 --to agent-2 orgo computers vnc-password agent-1 # print the VNC / terminal password ``` ## Templates Reproducible computer definitions: build once, launch anywhere. See the [Templates guide](https://docs.orgo.ai/guides/templates/introduction) for the full schema. ```bash orgo templates list # your templates plus curated ones orgo templates show hermes-agent # inspect a template orgo templates run hermes-agent # launch a computer from a template orgo templates build my-template # build a published template into a snapshot (Scale plan; streams the log live) orgo templates validate ./template.yaml orgo templates publish ./template.yaml orgo templates schema # print the template JSON Schema ``` Or launch straight from a template at create time: `orgo computers create --template hermes-agent`. ## Files and secrets ```bash # Files - upload caps at 10 MB; for larger data, pull it in from inside the computer orgo files list orgo files upload ./data.csv orgo files download orgo files export --computer agent-1 --path /root/Desktop/report.csv orgo files delete # Secrets - encrypted, write-only, and loaded by templates that declare them orgo secrets list orgo secrets set OPENAI_API_KEY orgo secrets delete OPENAI_API_KEY ``` ## API keys ```bash orgo api-keys list orgo api-keys create --name ci --workspace my-workspace orgo api-keys delete ``` Keys can be scoped to a single workspace, which is useful for CI tokens that should only see one workspace's resources. ## Environment | Variable | Purpose | |----------------------|------------------------------------------------------------| | `ORGO_API_KEY` | Use this key directly (skips `~/.orgo/credentials.json`) | | `ORGO_API_BASE_URL` | Point at a different API host | | `ORGO_PROFILE` | Credential profile (default: `default`) | | `ORGO_JSON=1` | Force JSON output everywhere | | `ORGO_NO_COLOR=1` | Disable ANSI colors (also respects `NO_COLOR`) | ## Shell completion The CLI ships with bash, zsh, and fish completion scripts. `orgo completion ` prints the script. Source it from your shell config to enable it: ```bash # zsh echo 'source <(orgo completion zsh)' >> ~/.zshrc # bash echo 'source <(orgo completion bash)' >> ~/.bashrc # fish orgo completion fish | source ``` Completion is dynamic. It queries your live workspaces and computers, so `orgo ssh ` shows your actual computers. ## License MIT. --- # Claude Code skill Source: https://docs.orgo.ai/guides/skill Summary: Let Claude Code reach for a computer. A drop-in skill that teaches Claude when and how to use Orgo. The Orgo skill is an [Agent Skill](https://docs.claude.com/en/docs/agents-and-tools/agent-skills/overview) for Claude Code. It teaches Claude when and how to spin up an Orgo cloud computer for browser automation, visual testing, sandboxed runs, or any task that needs a real GUI. Once installed, Claude Code discovers it automatically. No prompting required. ## Prerequisites The skill assumes you have: - An Orgo account. Sign up at [orgo.ai](https://orgo.ai). - The `orgo` CLI installed (`curl -fsSL https://orgo.ai/install.sh | bash`). - Signed in once with `orgo login`. If any of these are missing, Claude shows the command to run. ## Install ```bash Personal (all your projects) mkdir -p ~/.claude/skills/computer && \ curl -fsSL https://orgo.ai/skills/computer/SKILL.md \ -o ~/.claude/skills/computer/SKILL.md ``` ```bash Project (this repo only) mkdir -p .claude/skills/computer && \ curl -fsSL https://orgo.ai/skills/computer/SKILL.md \ -o .claude/skills/computer/SKILL.md ``` Restart Claude Code if it's already running. Claude will pick up the skill on next start. ## Try it ```text spin up an orgo computer and screenshot the orgo.ai homepage ``` ```text open chrome on my orgo computer, sign into github, take a screenshot ``` ```text test how my dev server renders at https://localhost:3000 on a real browser ``` Claude reads the skill's metadata at startup (~100 tokens) and only loads the full body when one of these prompts triggers it. Cost when idle: nothing. ## What's inside The skill is a single `SKILL.md` file with YAML frontmatter (`name`, `description`, `when_to_use`) followed by procedural instructions Claude uses when the user's request matches: - **One-shot agent runs**: `orgo run ""` with `--json` output for piping. - **Interactive agent TUI**: `orgo agent` with persistent, resumable sessions. - **Direct shell access**: `orgo ssh ` with OpenSSH-style disconnect. - **Provisioning**: `orgo computers create` interactive or flagged. - **Screenshots**: direct via REST or through the agent. - **Pricing awareness**: uses existing running computers before provisioning fresh ones. - **Guardrails**: clear “when NOT to use” so Claude doesn’t spin up a computer for tasks the Read/Edit/Bash tools already handle. Read the full file at [orgo.ai/skills/computer/SKILL.md](https://orgo.ai/skills/computer/SKILL.md). It is plain markdown, so review it before you install it, as you would any package. ## Where it works | Surface | Status | |---|---| | **Claude Code** (CLI / IDE) | Drop into `~/.claude/skills/computer/` and it works | | **claude.ai** | Settings → Features → upload as a `.zip` containing the `computer/` folder | | **Claude API** | Upload via `/v1/skills` endpoints (workspace-shared) | ## Uninstall ```bash rm -rf ~/.claude/skills/computer # personal install rm -rf .claude/skills/computer # project install ``` --- # MCP Source: https://docs.orgo.ai/guides/mcp Summary: Connect any MCP client to Orgo. Claude, Cursor, Codex, and others, with OAuth or an API key. The Orgo MCP server gives any [Model Context Protocol](https://modelcontextprotocol.io) client direct control of your cloud computers: spin up a computer, drive its browser, run shell commands, and read the screen back. Tools are generated from the published API specification, so the tool list follows the spec rather than the installed server version. ## Connect with OAuth For clients that support remote HTTP MCP servers, add `https://www.orgo.ai/mcp`. Your client opens Orgo's sign-in and consent page. Choose the workspace to authorize, then approve access. No API key is needed for this connection. ```bash Claude Code claude mcp add --transport http orgo https://www.orgo.ai/mcp ``` The hosted server supports the OAuth authorization-code flow with PKCE (S256), one-hour access tokens, and rotating refresh tokens. The grant is scoped to the selected workspace. It can create and manage computers, screens, and files in that workspace and read the workspace itself. Calls outside that surface, such as most Templates tools or creating and deleting workspaces, fail over an OAuth connection with `This MCP connection can only manage computers in its workspace.` Use an API key for those. Manage or revoke connections under **Connected apps** in [Settings → Credentials](https://www.orgo.ai/settings/credentials). Clients that let you set request headers can also call `https://www.orgo.ai/mcp` with an API key in `Authorization: Bearer $ORGO_API_KEY` instead of OAuth. An API key reaches every tool its scope allows. ## Install locally with an API key ```bash Claude Code claude mcp add orgo -e ORGO_API_KEY=sk_live_... -- npx -y orgo-mcp-server ``` ```bash Codex codex mcp add orgo --env ORGO_API_KEY=sk_live_... -- npx -y orgo-mcp-server ``` ```json Claude Desktop { "mcpServers": { "orgo": { "command": "npx", "args": ["-y", "orgo-mcp-server"], "env": { "ORGO_API_KEY": "sk_live_..." } } } } ``` ```json Cursor { "mcpServers": { "orgo": { "command": "npx", "args": ["-y", "orgo-mcp-server"], "env": { "ORGO_API_KEY": "${env:ORGO_API_KEY}" } } } } ``` ```json VS Code { "inputs": [ { "type": "promptString", "id": "orgo-key", "description": "Orgo API key", "password": true } ], "servers": { "orgo": { "type": "stdio", "command": "npx", "args": ["-y", "orgo-mcp-server"], "env": { "ORGO_API_KEY": "${input:orgo-key}" } } } } ``` ```json Any MCP client { "mcpServers": { "orgo": { "command": "npx", "args": ["-y", "orgo-mcp-server"], "env": { "ORGO_API_KEY": "sk_live_..." } } } } ``` Get a key at [Settings → Credentials](https://www.orgo.ai/settings/credentials), then restart your client. Config lives at `~/.cursor/mcp.json` for Cursor, `.vscode/mcp.json` for VS Code, `~/.codeium/windsurf/mcp_config.json` for Windsurf, and `~/Library/Application Support/Claude/claude_desktop_config.json` for Claude Desktop on macOS. **Tip:** Already using the [CLI](https://docs.orgo.ai/guides/cli)? Run `orgo login` once and drop the `env` block entirely. The server reads the same `~/.orgo/credentials.json`, so your key never has to sit in a config file. ## Try it ```text spin up an orgo computer and screenshot the orgo.ai homepage ``` ```text open chrome on my orgo computer, search for the weather, screenshot the result ``` ```text list my orgo computers and stop the ones that are idle ``` ```text run the test suite on a clean orgo computer and show me what failed ``` The first prompt runs `orgo_create_computer`, then `orgo_execute_bash`, then `orgo_get_screenshot`. By default `orgo_get_screenshot` returns a link to the stored capture. Pass `response_format: "base64"` and the screenshot comes back as an image the model can see. ## Tools One tool per API operation, named after its `operationId`. `createComputer` becomes `orgo_create_computer`. | Group | | Tools | | --- | --- | --- | | Workspaces | 5 | `list_workspaces` `create_workspace` `get_workspace` `update_workspace` `delete_workspace` | | Computers | 11 | `create_computer` `get_computer` `update_computer` `delete_computer` `clone_computer` `fork_computer` `resize_computer` `get_resize_limits` `move_computer` `get_vnc_password` `apply_computer_secrets` | | Computer Lifecycle | 4 | `start_computer` `stop_computer` `restart_computer` `get_computer_operation` | | Computer Actions | 10 | `get_screenshot` `mouse_click` `mouse_move` `mouse_drag` `type_text` `press_key` `scroll` `wait` `execute_bash` `execute_python` | | Screens | 5 | `list_screens` `create_screen` `get_screen` `resize_screen` `destroy_screen` | | Files | 5 | `upload_file` `list_files` `export_file` `download_file` `delete_file` | | Templates | 15 | `list_templates` `publish_template` `build_template` and 12 more | | Account | 3 | `get_account_capacity` `add_account_capacity` `reduce_account_capacity` | | Clients | 15 | `list_clients` `create_client` `get_client` `update_client` `close_client` and 10 more | | Computer Use API | 1 | `create_chat_completion` | **All 15 Templates tools** `orgo_get_template_schema` `orgo_list_templates` `orgo_publish_template` `orgo_validate_template` `orgo_list_curated_templates` `orgo_list_curated_template_versions` `orgo_list_template_versions` `orgo_get_template` `orgo_delete_template` `orgo_build_template` `orgo_get_build_status` `orgo_cancel_build` `orgo_stream_build_events` `orgo_test_run_template` `orgo_stop_template_run` **All 15 Clients tools** `orgo_list_clients` `orgo_create_client` `orgo_get_client` `orgo_update_client` `orgo_close_client` `orgo_replace_client_key` `orgo_transfer_client` `orgo_withdraw_client_transfer` `orgo_get_client_billing` `orgo_send_client_payment_link` `orgo_update_client_billing` `orgo_stop_client_billing` `orgo_resend_client_payment_link` `orgo_list_client_payments` `orgo_refund_client_payment` Group names are the spec's own tags, so `ORGO_TAGS="Workspaces,Computers"` narrows the surface using the same words the [API Reference](https://docs.orgo.ai/api-reference/introduction) uses. Set it when a client's tool picker starts guessing. ## Configuration Nothing here is required. `ORGO_API_KEY` is the only variable most setups ever set. | Variable | Default | Description | | --- | --- | --- | | `ORGO_API_KEY` | (none) | Your API key. Falls back to `~/.orgo/credentials.json` from `orgo login`. | | `ORGO_READ_ONLY` | `false` | Any value but `false`, `0`, `no` or `off` registers only the `GET` tools. | | `ORGO_TAGS` | all | Comma-separated spec tags to expose. Two contain a space, so quote it: `"Computers,Computer Actions"`. | | `ORGO_DEFAULT_COMPUTER_ID` | (none) | Pin a computer so action tools can omit `id`. An explicit `id` always wins. | **Escape hatches** | Variable | Default | Description | | --- | --- | --- | | `ORGO_PROFILE` | `current` in `credentials.json`, else `default` | Which credential profile to read. | | `ORGO_API_BASE_URL` | `https://www.orgo.ai` | Point at a different host. | | `ORGO_TIMEOUT_MS` | `120000` | Per-request timeout. | | `ORGO_RETRIES` | `2` | Retries for idempotent requests on a 5xx or a dropped connection. | | `ORGO_RAW` | unset | Return exactly what the API sent, without shaping. | | `ORGO_SPEC_URL` | [the published spec](https://docs.orgo.ai/api-reference/openapi.json) | Where to load the spec from. | | `ORGO_SPEC_MAX_AGE_MS` | `86400000` | How long a cached spec stays fresh. | | `XDG_CACHE_HOME` | `~/.cache` | Where the spec cache lives. | ## Security **Warning:** An `sk_live_` key is account-wide by default, and `orgo_execute_bash` and `orgo_execute_python` run arbitrary commands on a live machine. Leave your client's tool confirmation on. For a narrower blast radius, create a workspace-scoped key under [Settings → Credentials](https://www.orgo.ai/settings/credentials), or set `ORGO_READ_ONLY=true` to register only the read tools. ## How it works **Tools come from the spec.** Descriptions, types, enums, defaults and required fields are read straight out of it, so `orgo_create_computer` already knows `ram` is one of 4, 8, 16, 32 or 64 without anyone writing that down twice. Path, query and body parameters flatten into one argument list, so a call reads `orgo_resize_computer({ id, mem_gb })` rather than nesting a body. Annotations follow the HTTP method: `GET` tools are read-only, and every write is marked destructive, because `orgo_execute_bash` cannot promise otherwise. **Responses are shaped to it too.** Only the fields the spec declares survive, which keeps internal state the docs never promise out of the model's context. A schema matching none of the keys in hand is treated as describing something else, so the payload passes through untouched instead of being emptied. Spec drift costs verbosity, never data. Set `ORGO_RAW=1` to turn shaping off. **Failures retry only when that is provably safe.** A 5xx or a dropped connection is retried with doubling backoff, but only for `GET`, `PUT` and `DELETE` requests. `orgo_execute_bash` and `orgo_mouse_click` get exactly one attempt, because running either twice is worse than failing once. **The spec is loaded fresh.** In order, first one wins: 1. **Cache**: `$XDG_CACHE_HOME/orgo-mcp/openapi.json`, if fetched within `ORGO_SPEC_MAX_AGE_MS`. 2. **Network**: `ORGO_SPEC_URL`, 5 second timeout, written back to the cache. 3. **Stale cache**: whatever was last fetched. 4. **Bundled snapshot**: shipped in the npm package. A normal start is instant and offline. A start after the cache expires picks up whatever shipped in the API that day. A machine with no network still works. ## Troubleshooting Run the server directly to see what it thinks. It prints one line to stderr, then waits for JSON-RPC on stdin: ```bash ORGO_API_KEY=sk_live_... npx -y orgo-mcp-server # [orgo-mcp] 74 tools from Orgo API 2.0.0 (https://docs.orgo.ai/api-reference/openapi.json) → https://www.orgo.ai/api ``` That line names the tool count, the API version and which spec source won, which answers most setup questions on its own. Silence on stdout is correct; that channel carries the protocol. **spawn npx ENOENT on Windows** npm ships `npx.cmd`, and Node's `spawn` will not resolve a `.cmd` without a shell. Wrap it: ```json { "mcpServers": { "orgo": { "command": "cmd", "args": ["/c", "npx", "-y", "orgo-mcp-server"], "env": { "ORGO_API_KEY": "sk_live_..." } } } } ``` **HTTP 401: Invalid API key** Check the key on its own: ```bash curl -H "Authorization: Bearer $ORGO_API_KEY" https://www.orgo.ai/api/workspaces ``` If that works but the server does not, your client is not forwarding `env`. Pass the key inline with `claude mcp add -e` instead. **The tools don't appear in my client** Fully quit and relaunch. Most clients read MCP config only at startup, and Claude Desktop needs a real quit rather than closing the window. Then check `ORGO_READ_ONLY` and `ORGO_TAGS`, which reduce the tool list by design. Claude Desktop logs to `~/Library/Logs/Claude/mcp-server-orgo.log` on macOS. ## Next steps **API Reference** The same operations, with full request and response schemas. **CLI** Drive the same computers from your own terminal. **Claude Code skill** Teach Claude when to reach for a computer in the first place. --- # Use any model Source: https://docs.orgo.ai/guides/models Summary: Drive a computer with any model. Run Orgo's Computer Use agent with one request, or wire Claude, GPT, Gemini, or any other model to a computer yourself. Orgo exposes programmatic endpoints for every computer action: screenshot, click, type, key, scroll, bash. You bring the model, Orgo provides the computer. Any model with computer use support works. You can run the agent loop yourself, or let Orgo run it. ## Let Orgo run the agent [Create chat completion](https://docs.orgo.ai/api-reference/chat/completions) runs the whole loop on Orgo's side. Send a task and a `computer_id`, and the response is the agent's final message in OpenAI's chat completions format. `model` is optional. Leave it out and Orgo picks one for the computer: | Computer | Model that runs | |----------|-----------------| | Linux, Windows, macOS, iPhone | `orgo-agent`, shown as **Computer Use**: Orgo's computer-use engine on GPT-6 Astra, fast tier. It adapts its shell and shortcuts to the computer: PowerShell on Windows, Cmd shortcuts on macOS, and no shell on iPhone. | | Android | `claude-sonnet-5` | A request that sends its own Anthropic key in `x-anthropic-key` and names no model also runs `claude-sonnet-5`. Name a model to pin it: `orgo-agent`, `orgo-agent-opus-5.5` (the same engine on Claude Opus 5.5), `claude-sonnet-5`, `claude-opus-5.5`, `claude-opus-5`, `claude-opus-4.8`, `claude-sonnet-4.6`, or `claude-opus-4.6`. The two `orgo-agent` models run on every computer except Android. ```bash curl https://www.orgo.ai/api/v1/chat/completions \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "computer_id": "'"$COMPUTER_ID"'", "messages": [{"role": "user", "content": "Open Chrome and search for AI news"}] }' ``` Set `$COMPUTER_ID` to the `id` of a running computer. Runs bill to the workspace owner's AI credits. ## Run the loop yourself The sections below call each model's own API and execute its actions through Orgo. **Warning:** The examples pass `ram` and `cpu`, and take screenshots and scroll over HTTP, so they also run on older SDKs. SDK versions before 0.0.47 (Python) and 1.0.22 (TypeScript) default to 2 GB, which `POST /computers` rejects with `400`. Their `screenshot()` and `screenshot_base64()` cannot read the stored-image path the API returns by default, and their `scroll()` takes no coordinates, so the computer scrolls at the top-left corner. ## Coordinate space Orgo computers boot at `1280x720x24`. Claude and OpenAI read the screen size from the screenshots you send. They return coordinates in that same pixel space. Send an Orgo screenshot unresized and the coordinates are already the computer's pixels, so you can click with them directly. Gemini is different. It returns a normalized 0-999 grid, which you denormalize against the computer's real resolution: ```python x = int(fn_args["x"] / 1000 * 1280) y = int(fn_args["y"] / 1000 * 720) ``` **Warning:** Downscale a screenshot before sending it, or denormalize Gemini's grid against `1024x768` on a `1280x720` computer, and every click lands short of where the model aimed. Nothing errors. The agent looks like it is confidently clicking the wrong thing. If you create a computer at a non-default resolution, or resize its screen later, use that resolution instead. `GET /computers/{id}/screens` reports the current `width` and `height`. ## Anthropic Claude Claude drives computer use through the Messages API. `computer_toolset_20260801` declares every member tool in one entry and needs no beta header. ```python import os import anthropic import requests from orgo import Computer API = "https://www.orgo.ai/api" HEADERS = {"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"} def screenshot_base64(computer): """The screen as base64 PNG, straight from the API.""" response = requests.get( f"{API}/computers/{computer.computer_id}/screenshot", params={"response_format": "base64"}, headers=HEADERS, ) response.raise_for_status() return response.json()["image"] def scroll(computer, x, y, direction, amount): """Scroll at (x, y). Orgo scrolls up or down only.""" if direction not in ("up", "down"): raise ValueError(f"Unsupported scroll direction: {direction}") response = requests.post( f"{API}/computers/{computer.computer_id}/scroll", json={"x": x, "y": y, "direction": direction, "amount": amount}, headers=HEADERS, ) response.raise_for_status() # Pass a size: SDKs before 0.0.47 default to 2 GB, which the API rejects. computer = Computer(ram=4, cpu=1) client = anthropic.Anthropic() messages = [{"role": "user", "content": "Open Chrome and search for AI news"}] tools = [ {"type": "computer_toolset_20260801"}, {"type": "bash_20250124", "name": "bash"}, ] response = client.messages.create( model="claude-sonnet-5", max_tokens=8192, tools=tools, messages=messages, ) # Claude can return several member calls per turn. Run them in order. for block in response.content: if block.type != "tool_use": continue if block.name == "bash": output = computer.bash(block.input["command"]) elif getattr(block, "toolset_name", None) == "computer": if block.name == "screenshot": image = screenshot_base64(computer) # Return as a tool_result with a base64 image elif block.name == "left_click": computer.left_click(*block.input["coordinate"]) elif block.name == "type": computer.type(block.input["text"]) elif block.name == "key": computer.key(block.input["text"]) elif block.name == "scroll": scroll(computer, *block.input["coordinate"], block.input["scroll_direction"], block.input["scroll_amount"]) computer.destroy() ``` **Models:** `claude-opus-5-5`, `claude-sonnet-5-5`, `claude-opus-5`, `claude-sonnet-5`, `claude-opus-4-8` **Tools:** `computer_toolset_20260801` + `bash_20250124` **Beta header:** none **Older models:** Opus 4.7 and Sonnet 4.6 use `computer_20251124` with the `computer-use-2025-11-24` beta **Guide:** [Claude Computer Use](https://docs.orgo.ai/guides/claude-computer-use) **Docs:** [Anthropic Computer Use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool) --- ## OpenAI GPT OpenAI's computer use works through the Responses API with a built-in `computer` tool. The tool takes no configuration, and each turn returns a batch of actions. ```python import os import requests from openai import OpenAI from orgo import Computer API = "https://www.orgo.ai/api" HEADERS = {"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"} def scroll(computer, x, y, direction, amount): """Scroll at (x, y). Orgo scrolls up or down only.""" if direction not in ("up", "down"): raise ValueError(f"Unsupported scroll direction: {direction}") response = requests.post( f"{API}/computers/{computer.computer_id}/scroll", json={"x": x, "y": y, "direction": direction, "amount": amount}, headers=HEADERS, ) response.raise_for_status() # Pass a size: SDKs before 0.0.47 default to 2 GB, which the API rejects. computer = Computer(ram=4, cpu=1) client = OpenAI() response = client.responses.create( model="gpt-5.6", tools=[{"type": "computer"}], input="Open Chrome and search for AI news", ) # Process computer_call actions, in order for item in response.output: if item.type != "computer_call": continue for action in item.actions: if action.type == "click": computer.left_click(action.x, action.y) elif action.type == "double_click": computer.double_click(action.x, action.y) elif action.type == "type": computer.type(action.text) elif action.type == "keypress": # Orgo presses keys with xdotool, which expects X keysym names. # The OpenAI guide below maps the rest, such as the arrow keys. names = {"ENTER": "Return", "ESC": "Escape", "SPACE": "space", "TAB": "Tab", "BACKSPACE": "BackSpace"} computer.key("+".join(names.get(k.upper(), k.lower()) for k in action.keys)) elif action.type == "scroll": scroll_y = getattr(action, "scroll_y", 0) scroll(computer, action.x, action.y, "down" if scroll_y > 0 else "up", max(1, abs(scroll_y) // 100)) computer.destroy() ``` **Models:** `gpt-5.6`, `gpt-5.5` **Tool:** `computer` **Older integration:** `computer-use-preview` with the `computer_use_preview` tool **Guide:** [OpenAI Computer Use](https://docs.orgo.ai/guides/openai-computer-use) **Docs:** [OpenAI Computer Use](https://developers.openai.com/api/docs/guides/tools-computer-use) --- ## Google Gemini Computer use is a built-in tool on Gemini 3.x, called through the Interactions API. Declare `environment: "desktop"` for an Orgo computer. ```python import base64 import io import os import requests from google import genai from orgo import Computer from PIL import Image # Pass a size: SDKs before 0.0.47 default to 2 GB, which the API rejects. computer = Computer(ram=4, cpu=1) client = genai.Client(api_key="YOUR_GEMINI_API_KEY") def get_screenshot_png(computer: Computer) -> str: """Gemini accepts PNG only: fetch the screenshot as base64 and re-encode it as PNG.""" response = requests.get( f"https://www.orgo.ai/api/computers/{computer.computer_id}/screenshot", params={"response_format": "base64"}, headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, ) response.raise_for_status() image = Image.open(io.BytesIO(base64.b64decode(response.json()["image"]))) png_buffer = io.BytesIO() image.save(png_buffer, format="PNG") return base64.b64encode(png_buffer.getvalue()).decode("utf-8") screenshot_png = get_screenshot_png(computer) interaction = client.interactions.create( model="gemini-3.7-flash", input=[ {"type": "text", "text": "Open Chrome and search for AI news"}, {"type": "image", "data": screenshot_png, "mime_type": "image/png"}, ], tools=[{ "type": "computer_use", "environment": "desktop", "enable_prompt_injection_detection": True, }], ) # Coordinates are normalized 0-999, so scale them to the screen for step in interaction.steps: if step.type != "function_call": continue args = step.arguments if step.name == "click": computer.left_click(int(args["x"] / 1000 * 1280), int(args["y"] / 1000 * 720)) elif step.name == "type": computer.type(args["text"]) computer.destroy() ``` **Model:** `gemini-3.7-flash` **Tool:** `computer_use` with `environment` of `browser`, `desktop`, or `mobile` **SDK:** `google-genai` 2.7.0 or later **Coordinates:** normalized 0-999, scale to pixel resolution **Legacy:** `gemini-2.5-computer-use-preview-10-2025` with `generate_content` **Guide:** [Gemini Computer Use](https://docs.orgo.ai/guides/gemini-computer-use) **Docs:** [Gemini Computer Use](https://ai.google.dev/gemini-api/docs/computer-use) --- ## Other models Any model that outputs structured actions (click, type, screenshot) can work with Orgo. The pattern is always the same: 1. **Create** a computer with `Computer(ram=4, cpu=1)` 2. **Screenshot** with `GET /computers/{id}/screenshot?response_format=base64` 3. **Send** the screenshot and instruction to your model 4. **Execute** the model's actions with `computer.left_click()`, `computer.type()`, and so on 5. **Loop** until the task is done Open-source computer-use agents follow the same pattern. [Agent S3](https://docs.orgo.ai/guides/agent-s2) is one worked example. ## Orgo action reference | Action | Orgo call | Description | |--------|--------|-------------| | Screenshot | `GET /computers/{id}/screenshot?response_format=base64` | Capture screen as base64 PNG | | Left click | `computer.left_click(x, y)` | Click at coordinates | | Right click | `computer.right_click(x, y)` | Right-click | | Double click | `computer.double_click(x, y)` | Double-click | | Type | `computer.type("text")` | Type text | | Key press | `computer.key("Return")` | Press key or combo (`ctrl+c`), using xdotool key names | | Scroll | `POST /computers/{id}/scroll` with `x`, `y`, `direction`, `amount` | Scroll up or down at coordinates | | Bash | `computer.bash("ls -la")` | Run terminal command | | Wait | `computer.wait(2)` | Pause in seconds | Full API details: [API Reference](https://docs.orgo.ai/api-reference/introduction) --- # OpenClaw Source: https://docs.orgo.ai/guides/openclaw Summary: Run OpenClaw on an Orgo computer. [OpenClaw](https://openclaw.ai) is an open-source AI agent that runs on a computer, controls applications, automates tasks, and connects to messaging platforms through a gateway. This guide puts OpenClaw on an Orgo computer, where it runs 24/7. ## Launch the OpenClaw template The fastest way. The curated template boots with OpenClaw and the Claude Code CLI installed, and its gateway supervised: it restarts on a crash and on every boot. It runs on 1 vCPU and 8 GB RAM, and any paid plan can launch it. **Dashboard** Create a computer and choose **OpenClaw**. **API** ```bash curl -X POST https://www.orgo.ai/api/computers \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"workspace_id": "'"$WORKSPACE_ID"'", "name": "openclaw", "template_ref": "system/openclaw@1.1.8"}' ``` `system/openclaw@1.1.8` is the current version. [List curated templates](https://docs.orgo.ai/api-reference/templates/list-curated) returns every published version. **CLI** ```bash orgo computers create --name openclaw --template openclaw ``` ## Set up OpenClaw Pick one: - **From the dashboard.** Open the computer and chat with OpenClaw. Orgo sets it up with `ANTHROPIC_API_KEY` from your workspace’s Secrets. - **From its terminal.** Open the computer’s **Terminal**. The template starts `openclaw onboard` there, which asks for a model provider, a key, and the messaging apps to connect. Set `ANTHROPIC_API_KEY` in the Secrets section of [Settings → Credentials](https://www.orgo.ai/settings/credentials) before launch and it is in the computer’s environment from the first boot. The gateway starts once OpenClaw has a config. ## Connect a messaging app The gateway takes messages from Discord, iMessage, Matrix, Microsoft Teams, Signal, Slack, Telegram, WhatsApp, and more. To change channels after onboarding: ```bash openclaw configure openclaw gateway status ``` A computer has no public inbound hostname, so run gateways in outbound mode, such as Slack Socket Mode or Telegram long polling. Webhooks cannot reach the gateway. ## Install on a computer you already have Open the computer’s **Terminal** and run the installer. It installs Node if needed (OpenClaw needs 24.16+ or 26.1+), installs the `openclaw` CLI, and starts onboarding. ```bash curl -fsSL https://openclaw.ai/install.sh | bash ``` Orgo computers do not run systemd, so `openclaw onboard --install-daemon` cannot register a service. Run the gateway in a detached `tmux` session: ```bash tmux new-session -d -s openclaw 'openclaw gateway run' openclaw gateway status ``` A `tmux` session does not survive a restart. Use the template for a gateway that does. Watch its output with `tmux attach -t openclaw`, and detach with `Ctrl+B`, then `D`. ## Install skills OpenClaw skills come from [ClawHub](https://docs.openclaw.ai/clawhub): ```bash npm i -g clawhub clawhub search "browser automation" clawhub install @openclaw/demo clawhub update --all ``` Skills install into `./skills` under the current directory. Run `clawhub` from a fixed directory so skills stay in one place, or set `CLAWHUB_WORKDIR`. ## Good to know - The agent reaches only what you put on the computer. Your laptop’s files, browser sessions, and keys stay out of reach. - Config and gateway state live in `~/.openclaw/` on the computer’s disk. They survive stops and restarts. - `openclaw doctor` diagnoses install and config issues. - To move an existing OpenClaw from a VPS, see [Migrate to Orgo](https://docs.orgo.ai/guides/migrate). --- # Hermes Agent Source: https://docs.orgo.ai/guides/hermes Summary: Run Hermes Agent on an Orgo computer. Nous Research’s agent with a 24/7 home. [Hermes Agent](https://hermes-agent.nousresearch.com/) is Nous Research's self-improving AI agent. It builds skills from experience, refines them during use, and maintains a persistent model of the user across sessions. This guide puts Hermes on an Orgo computer, where it runs 24/7. ## Launch the Hermes Agent template The fastest way. The curated template boots with Hermes installed and its gateway supervised: it restarts on a crash and on every boot. It runs on 1 vCPU and 8 GB RAM, and any paid plan can launch it. **Dashboard** Create a computer and choose **Hermes Agent**. **API** ```bash curl -X POST https://www.orgo.ai/api/computers \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"workspace_id": "'"$WORKSPACE_ID"'", "name": "hermes", "template_ref": "system/hermes-agent@1.1.13"}' ``` `system/hermes-agent@1.1.13` is the current version. [List curated templates](https://docs.orgo.ai/api-reference/templates/list-curated) returns every published version. **CLI** ```bash orgo computers create --name hermes --template hermes-agent ``` ## Give Hermes a model Pick one: - **From the dashboard.** Open the computer and chat with Hermes. Orgo sets it up with `ANTHROPIC_API_KEY` from your workspace’s Secrets, or, without one, with Orgo’s models billed to your AI credits. - **From its terminal.** Open the computer’s **Terminal** and run `hermes setup`. `hermes setup --portal` signs in with a Nous Portal account. Set `ANTHROPIC_API_KEY` in the Secrets section of [Settings → Credentials](https://www.orgo.ai/settings/credentials) before launch and the template writes it into Hermes’ own env file. The gateway starts once Hermes has a config. ## Connect a messaging app Hermes takes messages from Telegram, Discord, Slack, WhatsApp, Signal, and email. In the computer’s terminal: ```bash hermes gateway setup ``` On the template, the gateway is already supervised. Nothing else to run. A computer has no public inbound hostname, so run gateways in outbound mode, such as Slack Socket Mode or Telegram long polling. Webhooks cannot reach the gateway. ## Install on a computer you already have Open the computer’s **Terminal** and run the installer. It installs Python, Node.js, ripgrep, ffmpeg, and Git if missing, and registers the `hermes` command. ```bash curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash source ~/.bashrc hermes setup ``` Orgo computers do not run systemd, so `hermes gateway install` cannot register a service. Run the gateway in a detached `tmux` session with `--no-supervise`, or Hermes hands it to a service manager that is not there and exits: ```bash tmux new-session -d -s hermes 'hermes gateway run --no-supervise' ``` A `tmux` session does not survive a restart. Use the template for a gateway that does. ## Commands | Command | What it does | |---|---| | `hermes` | Start a chat session. Quit with `Ctrl+D`. | | `hermes setup` | Run the guided setup again. | | `hermes model` | Switch provider or model. | | `hermes gateway setup` | Connect Telegram, Discord, Slack, and other apps. | | `hermes status` | Show agent, auth, and platform status. | | `hermes doctor` | Diagnose install or config issues. | | `hermes update` | Pull the latest Hermes release. | | `hermes claw migrate` | Import an existing OpenClaw setup. | Full reference: [hermes-agent.nousresearch.com/docs](https://hermes-agent.nousresearch.com/docs). ## Good to know - The agent reaches only what you put on the computer. Your laptop’s files, browser sessions, and keys stay out of reach. - Identity, memory, and learned skills live in `~/.hermes/` on the computer’s disk. They survive stops and restarts. - For many users, run one Hermes computer per user. - To move an existing Hermes from a VPS, see [Migrate to Orgo](https://docs.orgo.ai/guides/migrate). --- # Instance types Source: https://docs.orgo.ai/guides/instance-types Summary: Pick a computer’s CPU, RAM, and disk. The sizes on offer and the values the API accepts. Every Orgo computer gets the CPU and RAM you ask for when you create it. Choose the size that fits your workload. ## Available instances `POST /computers` takes no size name. A computer's size is the `cpu`, `ram`, and `disk_size_gb` you send. The names below are the sizes the dashboard offers and [Orgo's price list](https://www.orgo.ai/pricing.md) prices. | Size | vCPU | RAM | Disk | Create with | |------|------|-----|------|-------------| | **Small** | 0.5 | 4 GB | 20 GB | `"cpu": 0.5, "ram": 4, "disk_size_gb": 20` | | **Medium** | 1 | 8 GB | 40 GB | `"cpu": 1, "ram": 8, "disk_size_gb": 40` | | **Large** | 1 | 16 GB | 80 GB | `"cpu": 1, "ram": 16, "disk_size_gb": 80` | | **XL** | 2 | 32 GB | 150 GB | `"cpu": 2, "ram": 32, "disk_size_gb": 150` | | **2XL** | 4 | 64 GB | 300 GB | `"cpu": 4, "ram": 64, "disk_size_gb": 300` | Omit `cpu` and `ram` and you get 1 vCPU and 4 GB, unless you create from a template, which sets its own size. Omit `disk_size_gb` and you get your plan's default disk. Your plan caps each computer, so a size above its ceiling is refused until you add capacity, as described in [Accepted values](#accepted-values). A Linux computer boots at 1280x720 unless you pass `resolution`. Linux is the default operating system. Set `os` on [create](https://docs.orgo.ai/api-reference/computers/create) to choose another. --- ## Usage Pass `cpu` and `ram` when creating a computer to select its size: ```bash cURL # A Medium. Replace $WORKSPACE_ID with the workspace to create it in. curl -X POST https://www.orgo.ai/api/computers \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"workspace_id": "'"$WORKSPACE_ID"'", "name": "agent-1", "cpu": 1, "ram": 8, "disk_size_gb": 40}' ``` ```python Python from orgo import Computer # Medium computer = Computer(ram=8, cpu=1) # Large computer = Computer(ram=16, cpu=1) # XL computer = Computer(ram=32, cpu=2) ``` ```typescript TypeScript import { Computer } from 'orgo'; // Medium const medium = await Computer.create({ ram: 8, cpu: 1 }); // Large const large = await Computer.create({ ram: 16, cpu: 1 }); // XL const xl = await Computer.create({ ram: 32, cpu: 2 }); ``` **Note:** The SDKs take a `cpu` of 0.5, 1, 2, 4, 8, or 16 and send no disk size, so a computer made with one gets your plan's default disk. Choose a disk size over HTTP. Leave out `ram` and `cpu` and the SDKs send neither, so you get the default size above. SDK versions before 0.0.47 (Python) and 1.0.22 (TypeScript) default to 2 GB, which the API rejects, so pass `ram` and `cpu` to those. The TypeScript SDK ships in version 1 of the `orgo` npm package. Install it with `npm install orgo@1`. Version 2 and later of `orgo` contain only the [CLI](https://docs.orgo.ai/guides/cli). --- ## Accepted values `POST /computers` validates `ram`, `cpu`, and `os` before it provisions anything. A value outside the sets below returns `400` with the accepted set in the message, for example `{"error": "cpu must be 0.5, 1, 2, 4, 8, or 16 cores"}`. Omit any of the three and you get the default. Clearing that check is not the whole story. **One computer is capped at 4 vCPU, 64 GB RAM, and 300 GB of disk.** No plan on sale raises that cap, and add-ons only take you up to it. Your plan's per-computer ceiling is at or below the cap. See [Orgo pricing](https://www.orgo.ai/#pricing). ### RAM Integer, optional, in GB. Defaults to `4`. | Value | Description | |-------|-------------| | `4` | 4GB (default) | | `8` | 8GB | | `16` | 16GB | | `32` | 32GB | | `64` | 64GB (the per-computer cap) | Anything else returns `400`: `{"error": "ram must be 4, 8, 16, 32, or 64 GB"}`. A macOS computer also accepts `12`. For `os: "macos"` the error lists that value too: `{"error": "ram must be 4, 8, 12, 16, 32, or 64 GB"}`. **Note:** Your plan also caps RAM per computer, below the 64 GB cap, and RAM across your account. A request inside the table above can still be refused with `403` on quota grounds. See [Orgo pricing](https://www.orgo.ai/#pricing) for the current limits. ### CPU Number, optional, in cores. Defaults to `1`. | Value | Description | |-------|-------------| | `0.5` | Half a core | | `1` | 1 core (default) | | `2` | 2 cores | | `4` | 4 cores (the per-computer cap) | | `8` | Accepted by the value check, then refused with `403` | | `16` | Accepted by the value check, then refused with `403` | Anything else returns `400`: `{"error": "cpu must be 0.5, 1, 2, 4, 8, or 16 cores"}`. `8` and `16` are in the accepted set, so they clear the value check. They then fail the quota check with `403` and code `PER_COMPUTER_CPU_CAP`, on every plan on sale. Treat `4` as the real maximum. ### OS String, optional. Defaults to `linux`. | Value | Description | |-------|-------------| | `linux` | Linux (default) | | `windows` | Windows | | `macos` | macOS | | `android` | Android | | `ios` | iOS on a physical iPhone, not a virtual machine | Anything else returns `400`: `{"error": "os must be one of: linux, windows, macos, android, ios"}`. A non-Linux computer is placed on a host that can boot that guest, so capacity for it is narrower than for Linux. ### Resolution Resolution is passed as a string in `WIDTHxHEIGHTxDEPTH` format. Default is `1280x720x24`, or `1920x1080x24` on macOS. A computer created from a template uses the template's resolution. ```text 1024x768x24 1280x720x24 1920x1080x24 ``` ## Persistence A computer's disk and files persist across stop/start. Stopping a computer archives its disk. Starting it again restores your files and installed software, on the same host when Orgo can place it there. If it lands on another host, its IP can change. In-memory state and running processes are not preserved. Computers run until you stop them or turn on auto-stop. A free trial computer stops after 30 minutes. --- ## Included with every instance All instance types share these capabilities: - Full REST API and SDK access - Screenshot, mouse, keyboard, and bash execution - File upload and download - VNC access ## Plans Instance type sets the resources of one computer. How many computers you can run, how much RAM and storage they draw from in total, and how many seats your team gets are all properties of your plan. Plans change, so this page does not restate them. See [Orgo pricing](https://www.orgo.ai/#pricing) for current limits and pricing. ## Scaling and limits Your **Usage** page (the gauge button at the bottom of the sidebar, or `https://www.orgo.ai/account?tab=usage`) shows what you have in use against your limits and lets you add computers, RAM, vCPU, and storage to your account. If you're planning a large or sustained fleet, [reach out](mailto:spencer@orgo.ai) and we'll provision for it. --- # Scale to thousands Source: https://docs.orgo.ai/guides/scaling Summary: Give every user their own computer. Add capacity from code as you grow. A platform that gives each of its users a computer grows one computer at a time. You can add the capacity for them from code, the moment you need it, with the [capacity API](https://docs.orgo.ai/api-reference/account/capacity). ## Check what fits, then add it ```python import os, uuid, requests API = "https://www.orgo.ai/api" headers = {"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"} capacity = requests.get(f"{API}/account/capacity", headers=headers).json() free = capacity["computers"]["limit"] - capacity["computers"]["in_use"] if free < 10: requests.post( f"{API}/account/capacity", headers={**headers, "Idempotency-Key": str(uuid.uuid4())}, json={"size": "medium", "count": 50}, ).raise_for_status() ``` Then create computers as usual with [Create computer](https://docs.orgo.ai/api-reference/computers/create). ## Things to know - **Use an account-wide key.** Capacity is billed to the whole account, so a workspace-scoped key is refused. - **Send an `Idempotency-Key` with every purchase.** A retry with the same key returns the first answer and never buys twice. - **Price it first** with `"dry_run": true` if you want to show or log the charge. - **Up to 100 added computers on your own.** Past that the API answers `403` `capacity_cap`. [Ask us](mailto:support@orgo.ai) and we raise it for your account, to thousands of computers, at the same prices. - **Give capacity back** with [Reduce capacity](https://docs.orgo.ai/api-reference/account/reduce-capacity). It takes effect at the next renewal. --- # Browser networking Source: https://docs.orgo.ai/guides/browser-networking Summary: Use your own proxy for browser traffic. Why websites may challenge a computer, and how to change its exit IP. ## Bot checks Computers run in data centers. Websites may challenge or restrict traffic from these addresses. A browser proxy can change the exit IP; it does not guarantee that a website will accept automated traffic. ## Bring your own proxy For a running Linux computer, `POST /api/computers/{id}/egress/upstream` selects a customer-provided HTTP CONNECT proxy: ```json { "mode": "custom", "upstream": { "host": "proxy.example.com", "port": 443, "user": "your-proxy-user", "pass": "your-proxy-password", "tls": true } } ``` Authenticate with your Orgo API key and use your provider's actual settings. `tls` describes the connection to the proxy and defaults to `false` when omitted. The password is encrypted when stored and omitted from `GET /api/computers/{id}/egress/upstream` responses. Selecting a custom upstream enables the computer's browser relay if it is off. Initial enablement restarts Chrome; save browser work first. When the relay is already on, changing its upstream restarts the relay without restarting Chrome. This setting controls browser traffic, not every program in the computer. Availability depends on computer readiness and proxy eligibility. A computer with a dedicated IP returns `409` until you turn the dedicated IP off. If applying the upstream fails, Orgo restores the previous selection. --- # Memory Source: https://docs.orgo.ai/guides/memory Summary: Agents that remember across sessions. Add long-term memory to a computer use agent with Mem0. Add persistent memory to your computer use agents with [Mem0](https://mem0.ai). The agent remembers user preferences, learns from past interactions, and gets better over time. ## Setup ```bash pip install mem0ai orgo openai ``` Set your keys in the environment. Replace the values with your Orgo API key and your OpenAI API key (Mem0 uses OpenAI by default). ```bash export ORGO_API_KEY=sk_live_... export OPENAI_API_KEY=sk-... ``` ## Example ```python import os from mem0 import Memory from openai import OpenAI from orgo import Computer memory = Memory() # Pass a size: SDKs before 0.0.47 default to 2 GB, which the API rejects. computer = Computer(ram=4, cpu=1) client = OpenAI( base_url="https://www.orgo.ai/api/v1", api_key=os.environ["ORGO_API_KEY"], ) user_id = "alice" # Teach preferences memory.add( messages=[{"role": "user", "content": "I prefer Firefox over Chrome"}], user_id=user_id, ) memory.add( messages=[{"role": "user", "content": "I like dark themes"}], user_id=user_id, ) # Recall memories before a task results = memory.search(query="open a browser", user_id=user_id, limit=5) context = "\n".join(f"- {m['memory']}" for m in results.get("results", [])) # Use memories as context response = client.chat.completions.create( model="orgo-agent", messages=[{ "role": "user", "content": f"User preferences:\n{context}\n\nTask: Open my preferred browser and enable dark mode" }], extra_body={"computer_id": computer.computer_id}, ) print(response.choices[0].message.content) # Store the interaction for next time memory.add( messages=[ {"role": "user", "content": "Open my preferred browser"}, {"role": "assistant", "content": "Opened Firefox with dark mode"}, ], user_id=user_id, ) computer.destroy() ``` ## Managing memories ```python from mem0 import Memory memory = Memory() # View all memories for m in memory.get_all(user_id="alice").get("results", []): print(f" {m['memory']}") # Search results = memory.search(query="browser", user_id="alice", limit=5) # Clear memory.delete_all(user_id="alice") ``` ## How it works 1. **Search**: before each task, query Mem0 for relevant memories 2. **Inject**: add memories as context in the prompt 3. **Execute**: the AI agent acts with full context of past preferences 4. **Store**: save new interactions so future tasks benefit Each `user_id` maintains its own separate memory, so you can run multiple users or contexts (e.g. `"work"` vs `"personal"`). ## Learn more **Mem0 Docs** Memory API reference **Computer Use API** Orgo completions endpoint --- # Embed computers Source: https://docs.orgo.ai/guides/embed-vms Summary: Put a live computer screen in your app. Connect over VNC from the browser. Every computer streams over one WebSocket URL on `www.orgo.ai`. Any websockify-compatible VNC client connects to it, so you need no Orgo package to embed one. This page uses [noVNC](https://github.com/novnc/noVNC). ## The connection URL ```text wss://www.orgo.ai/desktops/{instance_id}/ws/websockify?token={password} ``` | Value | Where it comes from | |---|---| | `instance_id` | `POST /computers` returns it as `instance_id`. `GET /computers/{id}` returns the same value under its legacy name `fly_instance_id`. It is not the computer UUID. | | `password` | `GET /computers/{id}` returns it as `vnc_password`. `GET /computers/{id}/vnc-password` returns it as `password`. It survives a restart but can change, so fetch it for each session rather than storing it. | Both endpoints need your API key, so call them from your server. ## Two credentials, and which one goes in the browser | Credential | Where it lives | What it opens | |---|---|---| | **API key** (`sk_…`) | Your server only. Never in browser code. | Your whole Orgo account: every computer, billing, and minting more keys. It also bypasses MFA. | | **Computer password** | Only share with people allowed to control the computer. | One computer. | Your server holds the API key, reads the password with it, and returns only the password to your page. **Warning:** **The computer password is root on that computer.** The same value opens `/ws/terminal` and `POST /api/desktops/{instance_id}/proxy/bash`, not only the screen. Anyone who can read it from your page can run commands on the computer. So fetch it per session from your own backend, and keep it out of your client bundle. In Next.js that means never a `NEXT_PUBLIC_*` variable: those are compiled into the JavaScript every visitor downloads. Point embeds at a disposable computer rather than one holding real work. **Warning:** An `sk_` key is **rejected** when it arrives from a browser. Passing one as `?token=` closes the socket with `4001`. Send the computer password instead. ## View-only sharing A client setting such as `rfb.viewOnly = true` only disables that client's controls. It does not reduce the permission of a computer password sent to the browser; a visitor can turn the setting off. For authenticated Orgo workspace viewers, the web proxy enforces view-only access on the server and filters input. Use workspace viewer access for this case. A public anonymous embed with a separate native VNC view-only password is not currently provided by this API. ## Embed from any domain There is no origin allowlist on this path and nothing to register with us. The per-computer password is the credential, and it is the only thing that decides whether a connection is allowed. ## Embed with noVNC Two files. First a server route that trades your API key for the two values the browser needs. ```ts app/api/computer/route.ts // SERVER. The API key never leaves this file. export async function GET() { const res = await fetch( `https://www.orgo.ai/api/computers/${process.env.ORGO_COMPUTER_ID}`, { headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` }, cache: 'no-store', }, ); const computer = await res.json(); // Only the per-computer password crosses to the browser. return Response.json( { instanceId: computer.fly_instance_id, password: computer.vnc_password }, { headers: { 'Cache-Control': 'no-store' } }, ); } ``` **Warning:** Gate that route with your own auth. Anyone who can call it can drive the computer. Then the browser. Install noVNC with `npm install @novnc/novnc`. ```js Browser import RFB from '@novnc/novnc'; const { instanceId, password } = await fetch('/api/computer').then((r) => r.json()); const rfb = new RFB( document.getElementById('screen'), `wss://www.orgo.ai/desktops/${instanceId}/ws/websockify?token=${encodeURIComponent(password)}`, { credentials: { password } }, ); rfb.viewOnly = false; // true renders the screen without sending input rfb.scaleViewport = true; // fit the screen to its container rfb.qualityLevel = 6; // 0 to 9 rfb.compressLevel = 2; // 0 to 9 // Answer the VNC auth challenge when the proxy forwards it. rfb.addEventListener('credentialsrequired', () => rfb.sendCredentials({ password })); rfb.addEventListener('securityfailure', (e) => console.error('VNC auth failed', e.detail)); // The close code is the only signal a rejected connection gives you. rfb._sock._websocket.addEventListener('close', (e) => console.log('closed', e.code)); ``` That last line reads noVNC internals, because noVNC’s own `disconnect` event does not carry the close code. ## A failed embed is a black screen The proxy rejects a connection by accepting the WebSocket upgrade and then closing it. noVNC therefore sees an ordinary disconnect, raises nothing, and the canvas stays black. The close code is the only way to tell a bad password from a stopped computer. | Code | What happened | What to do | |---|---|---| | `4000` | The path was not recognised. | Connect to `/desktops/{instance_id}/ws/websockify`. | | `4001` | Authentication was rejected. | Send the per-computer password, not an `sk_` key. It may have changed, so fetch it again. | | `4003` | The computer is not running. | Start it, then reconnect once its status is `running`. | | `4004` | No such computer. | Check `instance_id`. | | `4006` | The credential is valid but scoped to another workspace. | Use a key for this computer’s workspace, or an account-wide key. | | `4007` | MFA is required for this session. | Complete the MFA challenge, then reconnect. | | `4008` | Idle for 30 minutes. | Expected. Reconnect on user activity. | | `4010` | WebRTC is disabled for this computer. | Use the websockify path. | | `4011` | This grant allows viewing the screen only. | Connect to `/ws/websockify`. Terminal and audio are not available. | | `4500` | The proxy hit an internal error. | Retry. | | `4502` | The proxy has no address for the computer. | Usually transient while a computer is starting or moving hosts. Retry. | | `4503` | The proxy could not look up the computer. | Transient. Retry. | Anything outside 4000-4599 is a standard WebSocket code. The proxy closes with `1000` when its own connection to the computer fails or ends, and with `1001` when it shuts down. `4003`, `4007`, `4008`, `4500`, `4502` and `4503` are worth retrying. The rest need an input changed first. **Note:** **Every long-lived embed eventually hits `4008`.** After 30 minutes with no human input the proxy closes the connection. The timer measures keyboard, pointer and clipboard input from your page, not pixels, so a computer an agent is driving with nobody watching still counts as idle. Treat `4008` as a clean close and reconnect on the next user action. ## The orgo-vnc package The published `orgo-vnc` React package (0.2.x) takes a `hostname` prop and connects to `wss://{hostname}/websockify`, from when every computer had its own host. That address no longer exists, and the package does not send the `?token=` the proxy expects. Connect with noVNC as shown in [Embed with noVNC](#embed-with-novnc) instead. ## Next steps **Quickstart** Full SDK setup **API reference** Control computers programmatically --- # Invoicing Source: https://docs.orgo.ai/guides/invoicing Summary: Run Orgo for your clients from your own app, and cover them or bill them monthly. Invoicing lets you run Orgo for each of your clients or users from your own app. Each one gets their own workspace and an API key that reaches only it. They never sign up to Orgo. For each client you choose who pays: - **You.** Their computers count against your plan and their AI uses your credits, like your own workspaces. - **Your client.** They pay you monthly through a payment link in your name. Orgo keeps their computer cost, Stripe takes its card fee, and the rest goes to your bank. **Note:** Invoicing is on every paid plan. Manage it in **Settings → Invoicing**, or with the [Clients API](https://docs.orgo.ai/api-reference/clients/list). Use your account-wide key: a client's own key gets `403` there. ## 1. Add a client ```bash curl -X POST https://www.orgo.ai/api/v1/accounts \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Dental" }' ``` The response has the client's `api_key`, shown once, so store it with your user. Use it like any Orgo key: create and control computers, run the Computer Use API, read files. It can't reach your other workspaces, and it can't create workspaces, keys, or clients. Their workspace stays out of your own workspace list. Optionally set `external_id` (your id for them), `max_computers`, and `monthly_ai_cap_usd`. See [Add a client](https://docs.orgo.ai/api-reference/clients/create). ## 2. Bill them Set up payouts once in **Settings → Invoicing**. Then send a payment link: ```bash curl -X POST https://www.orgo.ai/api/v1/accounts/$CLIENT_ID/billing \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "billing@acme.com", "price_usd": 250, "ai_usd": 40 }' ``` Your client gets an email from your business, and replies come to you. Checkout carries your business name, logo, and color, set under **Settings → Invoicing**, and never mentions Orgo. The price must cover their computers. `ai_usd` is optional: AI they pay for at cost, which adds to your credits each paid month and caps their AI at it. While they pay, their computers don't count against your plan. If a payment fails for good, they go back on your plan and you get an email. Later, [change the price](https://docs.orgo.ai/api-reference/clients/update-billing), [refund a payment](https://docs.orgo.ai/api-reference/clients/refund-payment), or [stop billing](https://docs.orgo.ai/api-reference/clients/stop-billing). ## 3. Transfer it to them When the build is done, hand it over: ```bash curl -X POST https://www.orgo.ai/api/v1/accounts/$CLIENT_ID/transfer \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "it@acme.com" }' ``` Once they accept, signed in to their own Orgo account, the workspace, its computers, and its key are theirs, on their plan. Whatever you built on the key keeps working. You stay on as an admin until they remove you, and any billing through you ends. See [Transfer a client](https://docs.orgo.ai/api-reference/clients/transfer). ## What you earn Orgo keeps the client's computer cost and any AI amount. Stripe's card fee (2.9% + 30¢) comes out of your side, as on any payment you take. The rest is yours. A client who pays $250 a month for computers that cost $50 sends Orgo $50, Stripe $7.55, and you $192.45. ## All endpoints | | | | --- | --- | | Clients | [List](https://docs.orgo.ai/api-reference/clients/list) · [Add](https://docs.orgo.ai/api-reference/clients/create) · [Get](https://docs.orgo.ai/api-reference/clients/get) · [Update](https://docs.orgo.ai/api-reference/clients/update) · [Close](https://docs.orgo.ai/api-reference/clients/close) · [Replace key](https://docs.orgo.ai/api-reference/clients/replace-key) | | Billing | [Get](https://docs.orgo.ai/api-reference/clients/get-billing) · [Bill](https://docs.orgo.ai/api-reference/clients/send-payment-link) · [Change price](https://docs.orgo.ai/api-reference/clients/update-billing) · [Stop](https://docs.orgo.ai/api-reference/clients/stop-billing) · [Resend link](https://docs.orgo.ai/api-reference/clients/resend-payment-link) | | Payments | [List](https://docs.orgo.ai/api-reference/clients/list-payments) · [Refund](https://docs.orgo.ai/api-reference/clients/refund-payment) | | Transfer | [Send](https://docs.orgo.ai/api-reference/clients/transfer) · [Withdraw](https://docs.orgo.ai/api-reference/clients/withdraw-transfer) | --- # Troubleshooting Source: https://docs.orgo.ai/guides/troubleshooting Summary: Fix common API errors. Each error mapped to the action that fixes it. Orgo API error responses are JSON with an `error` string. The OpenAI-compatible `/v1/chat/completions` endpoint is the exception: its `error` is an object with `type`, `message`, and `code`. When a computer action fails at the desktop, or while Orgo is reaching it, the response also carries a `request_id` to use when contacting support, and some carry a machine-readable `code`. ```json { "error": "Could not reach the desktop. Try again in a moment.", "request_id": "f0c2c4a8-9d8e-4d2f-b8a6-2a9a4c2d4b1f", "code": "ECONNREFUSED" } ``` When the error originated on the desktop side of the request, the response also includes the upstream status: ```json { "error": "this VM already has 4 screens, which is the limit", "request_id": "f0c2c4a8-9d8e-4d2f-b8a6-2a9a4c2d4b1f", "upstream_status": 409 } ``` Always include `request_id` when emailing [spencer@orgo.ai](mailto:spencer@orgo.ai) about a failed call. It maps directly to a server-side log line. ## Authentication | Symptom | What it means | Fix | |---|---|---| | `401 Invalid API key` | The Bearer token in your `Authorization` header is not a known Orgo API key. | Generate a new key at [orgo.ai/settings/credentials](https://www.orgo.ai/settings/credentials). Check for stray whitespace. | | `401 Authentication required` | No `Authorization` header on the request (or a token without the `sk_` prefix), and no signed-in browser session. | Set `Authorization: Bearer $ORGO_API_KEY`. | | `Service temporarily unavailable.` with `401` or `503`, depending on the endpoint | Orgo could not verify credentials because of a server-side fault. Your key is not the problem. | Retry after a short wait. Do not rotate your key. | | `You do not have access to this workspace.` with `403` or `401`, depending on the endpoint | The computer or workspace is not in a workspace you belong to, or you were removed from the workspace. | Confirm the workspace ID, or ask the workspace owner to re-invite you. | | `workspace_scope_mismatch` in the `error` message, with `403` or `401`, depending on the endpoint | The API key is workspace-scoped and the request targets a different workspace, or an endpoint that needs an account-wide key. | Use an account-wide key, or create a key scoped to the target workspace. See [Authentication](https://docs.orgo.ai/api-reference/authentication). | Orgo API keys come in two flavors: **account-wide** (can access every workspace your account can) and **workspace-scoped** (locked to one workspace and refused everywhere else). Pick the scope when you create the key at [orgo.ai/settings/credentials](https://www.orgo.ai/settings/credentials). ## Computer not responding | Symptom | What it means | Fix | |---|---|---| | `404 Desktop not found` | The ID in the URL doesn't match a computer your account can see. | Re-fetch the ID from `GET /workspaces/{id}`. | | `402 Choose a plan to continue.` | The computer is a trial computer whose trial has ended. | Choose a paid plan in the dashboard. | | `400 Desktop instance not available` | The computer record exists, but it never got a backing VM (still booting or failed). | Wait a few seconds and retry. If it persists, `POST /computers/{id}/restart`. | | `503 Could not reach the desktop. Try again in a moment.` | The control plane could not connect to the desktop. Usually transient. | Retry with exponential backoff (1s, 2s, 4s). If it keeps failing, restart the computer. | | `409 Conflict` | The action is blocked by the computer's current state, for example `Computer must be running to resize`. | Read the message; restart or stop the computer if needed. | ## Action errors (bash, click, type, exec, etc.) When an action endpoint returns a non-200, the `upstream_status` field tells you whether the failure came from Orgo's control plane or from the desktop itself. | `upstream_status` | Meaning | |---|---| | Not present | The error originated in Orgo's control plane (auth, lookup, validation). | | `400` | The desktop rejected the request body, for example a click with `repeat` above 10. | | Not present, `code: "timeout"`, status `504` | The command ran past its allowed time. Common with long bash commands; pass a higher `timeout` in the body (bash defaults to 200 seconds and caps it at 300). | | `500` | The desktop could not carry out the action. If it persists, capture the `request_id` and contact support. | ## Recovering a stuck computer A computer is "stuck" if it returns `200` on `GET /computers/{id}` but action calls fail. 1. `GET https://www.orgo.ai/api/desktops/{instance_id}/proxy/health`. If this returns 200, the desktop is up and the issue is in the control plane. Retry your action. 2. If `/health` fails, `POST /computers/{id}/restart`. Files on disk are preserved across a restart; running processes are not. 3. If restart fails, capture `request_id`s from a few attempts and email [spencer@orgo.ai](mailto:spencer@orgo.ai). Do not delete the computer; deletion drops the disk. ## Reporting a bug When something is broken, send: - The `request_id` from the error response (the most important field). - The exact request you made (method, URL, body, redacted of secrets). - The full error response. Email: [spencer@orgo.ai](mailto:spencer@orgo.ai). Discord: [discord.gg/tbYGpvnnJD](https://discord.gg/tbYGpvnnJD). --- # Compare platforms Source: https://docs.orgo.ai/guides/migrations/overview Summary: How Orgo compares to Fly.io, DigitalOcean, and Hetzner: a plain-language map so you can pick the right tool and migrate cleanly. Orgo is a persistent cloud **computer** for AI agents. It runs Linux or Windows, and an agent drives it over the HTTP API, the `orgo` CLI, or MCP, with click, type, and screenshot built in. It also gives CLI agents like Hermes Agent, OpenClaw, or Claude Code a 24/7 home. Fly.io, DigitalOcean, and Hetzner are general compute platforms. This page maps them side by side so you can choose, then open the guide for your source. ## The map | | [Fly.io](https://docs.orgo.ai/guides/migrations/fly-io) | [DigitalOcean](https://docs.orgo.ai/guides/migrations/digitalocean) | [Hetzner](https://docs.orgo.ai/guides/migrations/hetzner) | Orgo | |---|---|---|---|---| | **What it is** | Apps on microVMs | VPS (Droplets) | VPS (Servers) | Persistent computer for agents | | **You manage** | App and Machine config | OS, firewall, TLS | OS, firewall, TLS | Nothing | | **Boot time** | Sub-second | Minutes to a ready server | Minutes to a ready server | Online in seconds | | **Click / type / screenshot** | Build it yourself | Build it yourself | Build it yourself | Built in | | **Interface** | CLI, REST, SSH | SSH | SSH | HTTP API, CLI, MCP | | **Public inbound** | Anycast IPs and proxy | Reserved IPs, DNS, firewalls | Primary and floating IPs | Authenticated proxy | | **Persistence** | Volumes (opt-in) | Droplet disk | Server disk | Persistent by default | | **Best for** | General apps and services, multi-region | General self-managed servers | Low-cost general servers | Agents driving a computer or browser, and a 24/7 home for CLI agents (Hermes, OpenClaw, Claude Code) | ## How to choose - **DigitalOcean or Hetzner** when you want a general Linux server you fully control: arbitrary services, ports, and public networking you manage yourself. - **Fly.io** when you want a general app platform with fast microVMs, multi-region, and Anycast networking. - **Orgo** when the job is an **agent driving a computer or browser** (built-in click/type/screenshot), or a **persistent 24/7 home for a CLI agent** like Hermes, OpenClaw, or Claude Code. Computers are persistent by default, with nothing to administer. These are not mutually exclusive. A common pattern is a VPS or Fly for backend services, and Orgo for the work where an agent drives a computer. **Info:** **Scaling out?** Planning a large or sustained fleet? [Reach out](mailto:spencer@orgo.ai) about an Enterprise plan with custom limits. You can also add computers to your current plan from the Usage page at [orgo.ai/account](https://www.orgo.ai/account?tab=usage). ## Pricing Pricing models differ enough that one number misleads. The VPS providers bill a flat monthly rate per server, Fly.io bills usage-based per second, and Orgo bills per plan (a bundle of computers, a RAM pool, and support). Compare on total cost for your workload, not RAM alone. A raw VM leaves you to install and run the desktop environment, browser, and automation yourself, while an Orgo computer ships that built in. See [Orgo pricing](https://www.orgo.ai/#pricing) for current plans. ## Migrate **From Fly.io** **From DigitalOcean** **From Hetzner** --- # Migrate from Fly.io Source: https://docs.orgo.ai/guides/migrations/fly-io Summary: Move a workload off a Fly.io Machine onto a persistent Orgo cloud computer: concept mapping, the clean way to transfer state, and the honest differences. You run apps on Fly.io as **Machines**: fast-booting [Firecracker microVMs](https://fly.io/docs/reference/architecture/) grouped under an [App](https://fly.io/docs/apps/), with optional [Volumes](https://fly.io/docs/volumes/) for persistence. Orgo gives an AI agent a full, always-on **computer** (browser, shell, and files) that it drives over the HTTP API, the `orgo` CLI, or MCP. This page maps the two and shows the cleanest way to move. **Info:** **Doing this with an agent?** Paste this page into Claude Code or Codex as context. Every command is copy-paste and self-contained, so the agent can run the switch end to end. ## How the platforms map | Fly.io | Orgo | |---|---| | App + Machine (Firecracker microVM) | A **computer** (`POST /computers`) | | `fly ssh console` (over WireGuard) | `POST /computers/{id}/bash`, or `orgo ssh ` | | Volume (persistent storage) | The computer's disk, persistent by default | | Volume snapshot | `POST /computers/{id}/clone` | | Anycast IPs + Fly Proxy (public inbound) | No public per-computer IP. Reach it via the authenticated proxy: `www.orgo.ai/api/desktops/{instance_id}/proxy/` | ## Move your state Orgo runs any shell command inside the computer via `POST /computers/{id}/bash`. The cleanest transfer is to **pull your data in from inside the computer**: no size limits, no laptop round-trip. **Recommended: via a Git repo.** Push the state you want to keep to a private repo, then clone it inside the Orgo computer: ```bash 1. Extract on Fly # Machines have no public SSH; this runs over Fly's private WireGuard mesh. fly ssh console -a YOUR_APP -C "tar czf /tmp/state.tgz -C /root ." fly ssh sftp get -a YOUR_APP /tmp/state.tgz ./state.tgz # Commit state.tgz (or the raw dir) to a private repo. ``` ```bash 2. Pull inside the Orgo computer curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/bash \ -H "Authorization: Bearer $ORGO_API_KEY" -H "Content-Type: application/json" \ -d '{"command":"git clone https://TOKEN@github.com/you/your-state.git ~/state && tar xzf ~/state/state.tgz -C ~"}' ``` **Alternatives** - **Any URL.** Host the tarball anywhere reachable and `curl`/`wget` it from inside the computer the same way. A Git repo is the tidiest, versioned option. - **Small files (< 10 MB).** `POST /files/upload` drops a file straight onto the computer. It caps at **10 MB**. For larger state, use the Git or URL pull above, or split into < 10 MB batches. Reference: [Fly SSH & SFTP](https://fly.io/docs/flyctl/ssh-sftp/), [Fly Volume snapshots](https://fly.io/docs/volumes/snapshots/). ## Set up on Orgo One call provisions the computer. Install your software with the same `/bash` endpoint. ```bash curl -X POST https://www.orgo.ai/api/computers \ -H "Authorization: Bearer $ORGO_API_KEY" -H "Content-Type: application/json" \ -d "{\"workspace_id\":\"$WORKSPACE_ID\",\"name\":\"prod\",\"ram\":8}" # → save the returned id as $COMPUTER_ID, then run your installer via /bash ``` See the [Quickstart](https://docs.orgo.ai/quickstart) for the full control surface (`bash`, `exec`, `screenshot`, click/type). ## What's different Neutral, both directions: - **Fly does, Orgo doesn't:** general-purpose compute and PaaS, with arbitrary long-running services, public inbound via Anycast [shared/dedicated IPs](https://fly.io/docs/networking/services/), multi-region deployments, and opt-in persistence via Volumes (a Machine's base filesystem is [ephemeral](https://fly.io/docs/machines/overview/)). - **Orgo does, Fly doesn't out of the box:** a persistent GUI **computer** with a built-in agent-control API (screenshot / click / type) and nothing to administer. You manage no OS, no firewall, and no TLS. Rule of thumb: keep general server workloads and public-facing services on Fly. Move to Orgo when the job is an **agent operating a durable computer or browser**. Fly's public-networking and multi-region features have no 1:1 Orgo analog. Reach the computer's own API through the [authenticated proxy](https://docs.orgo.ai/api-reference/introduction) instead. ## Reference **Fly Machines:** Compute primitive **Fly SSH & SFTP:** Extract files **Orgo Quickstart:** Control a computer over HTTP **Move an agent (Hermes/OpenClaw):** Full state-preserving playbook --- # Migrate from DigitalOcean Source: https://docs.orgo.ai/guides/migrations/digitalocean Summary: Move a workload off a DigitalOcean Droplet onto a persistent Orgo cloud computer: concept mapping, the clean way to transfer state, and the honest differences. You run workloads on DigitalOcean as [**Droplets**](https://docs.digitalocean.com/products/droplets/): Linux VMs you provision, SSH into, and administer yourself. Orgo gives an AI agent a full, always-on **computer** (browser, shell, and files) that it drives over the HTTP API, the `orgo` CLI, or MCP. This page maps the two and shows the cleanest way to move. **Info:** **Doing this with an agent?** Paste this page into Claude Code or Codex as context. Every command is copy-paste and self-contained, so the agent can run the switch end to end. ## How the platforms map | DigitalOcean | Orgo | |---|---| | Droplet (Linux VM) | A **computer** (`POST /computers`) | | `ssh root@` / `doctl compute ssh` | `POST /computers/{id}/bash`, or `orgo ssh ` | | Droplet disk | The computer's disk, persistent by default | | [Snapshot](https://docs.digitalocean.com/products/snapshots/) | `POST /computers/{id}/clone` | | [Reserved IP](https://docs.digitalocean.com/products/networking/reserved-ips/) + [Cloud Firewall](https://docs.digitalocean.com/products/networking/firewalls/) + [DNS](https://docs.digitalocean.com/products/networking/dns/) | No public per-computer IP. Reach it via the authenticated proxy: `www.orgo.ai/api/desktops/{instance_id}/proxy/` | ## Move your state Orgo runs any shell command inside the computer via `POST /computers/{id}/bash`. The cleanest transfer is to **pull your data in from inside the computer**: no size limits, no laptop round-trip. DigitalOcean snapshots [can't be downloaded off-platform](https://docs.digitalocean.com/products/snapshots/), so pull the files over SSH rather than exporting an image. **Recommended: via a Git repo.** Push the state you want to keep to a private repo, then clone it inside the Orgo computer: ```bash 1. Extract on the Droplet ssh root@ "tar czf /tmp/state.tgz -C /root ." scp root@:/tmp/state.tgz ./state.tgz # Commit state.tgz (or the raw dir) to a private repo. ``` ```bash 2. Pull inside the Orgo computer curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/bash \ -H "Authorization: Bearer $ORGO_API_KEY" -H "Content-Type: application/json" \ -d '{"command":"git clone https://TOKEN@github.com/you/your-state.git ~/state && tar xzf ~/state/state.tgz -C ~"}' ``` **Alternatives** - **Any URL.** Host the tarball anywhere reachable and `curl`/`wget` it from inside the computer the same way. A Git repo is the tidiest, versioned option. - **Small files (< 10 MB).** `POST /files/upload` drops a file straight onto the computer. It caps at **10 MB**. For larger state, use the Git or URL pull above, or split into < 10 MB batches. Reference: [Connect via SSH](https://docs.digitalocean.com/products/droplets/how-to/connect-with-ssh/), [Transfer files](https://docs.digitalocean.com/products/droplets/how-to/transfer-files/) (`scp`/`rsync` are standard OpenSSH tools). ## Set up on Orgo ```bash curl -X POST https://www.orgo.ai/api/computers \ -H "Authorization: Bearer $ORGO_API_KEY" -H "Content-Type: application/json" \ -d "{\"workspace_id\":\"$WORKSPACE_ID\",\"name\":\"prod\",\"ram\":8}" # → save the returned id as $COMPUTER_ID, then run your installer via /bash ``` See the [Quickstart](https://docs.orgo.ai/quickstart) for the full control surface (`bash`, `exec`, `screenshot`, click/type). ## What's different Neutral, both directions: - **DigitalOcean does, Orgo doesn't:** a general-purpose Linux server you fully own, with arbitrary services and ports, many hardware tiers (incl. GPU), first-class public networking ([reserved IPs](https://docs.digitalocean.com/products/networking/reserved-ips/), [managed DNS](https://docs.digitalocean.com/products/networking/dns/), [firewalls](https://docs.digitalocean.com/products/networking/firewalls/), load balancers), and scheduled [backups](https://docs.digitalocean.com/products/backups/). - **Orgo does, DigitalOcean doesn't out of the box:** a persistent GUI **computer** with a built-in agent-control API (screenshot / click / type), online in seconds, with nothing to administer. You manage no OS, no firewall, and no TLS. (Droplet firewalls are default-deny.) Rule of thumb: keep general server workloads and public-facing services on a Droplet. Move to Orgo when the job is an **agent operating a durable computer or browser**. ## Reference **DigitalOcean Droplets:** Their VM **Transfer files:** scp / rsync / SFTP **Orgo Quickstart:** Control a computer over HTTP **Move an agent (Hermes/OpenClaw):** Full state-preserving playbook --- # Migrate from Hetzner Source: https://docs.orgo.ai/guides/migrations/hetzner Summary: Move a workload off a Hetzner Cloud Server onto a persistent Orgo cloud computer: concept mapping, the clean way to transfer state, and the honest differences. You run workloads on Hetzner Cloud as [**Servers**](https://docs.hetzner.com/cloud/servers/overview/): Linux VMs you provision, SSH into, and administer yourself. Orgo gives an AI agent a full, always-on **computer** (browser, shell, and files) that it drives over the HTTP API, the `orgo` CLI, or MCP. This page maps the two and shows the cleanest way to move. **Info:** **Doing this with an agent?** Paste this page into Claude Code or Codex as context. Every command is copy-paste and self-contained, so the agent can run the switch end to end. ## How the platforms map | Hetzner Cloud | Orgo | |---|---| | Server (Linux VM) | A **computer** (`POST /computers`) | | `ssh root@` | `POST /computers/{id}/bash`, or `orgo ssh ` | | Server disk | The computer's disk, persistent by default | | [Snapshot](https://docs.hetzner.com/cloud/servers/getting-started/taking-snapshots/) | `POST /computers/{id}/clone` | | [Primary/Floating IP](https://docs.hetzner.com/cloud/servers/primary-ips/overview/) + [Firewall](https://docs.hetzner.com/cloud/firewalls/overview/) | No public per-computer IP. Reach it via the authenticated proxy: `www.orgo.ai/api/desktops/{instance_id}/proxy/` | ## Move your state Orgo runs any shell command inside the computer via `POST /computers/{id}/bash`. The cleanest transfer is to **pull your data in from inside the computer**: no size limits, no laptop round-trip. **Recommended: via a Git repo.** Push the state you want to keep to a private repo, then clone it inside the Orgo computer: ```bash 1. Extract on the Hetzner server ssh root@ "tar czf /tmp/state.tgz -C /root ." scp root@:/tmp/state.tgz ./state.tgz # Commit state.tgz (or the raw dir) to a private repo. ``` ```bash 2. Pull inside the Orgo computer curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/bash \ -H "Authorization: Bearer $ORGO_API_KEY" -H "Content-Type: application/json" \ -d '{"command":"git clone https://TOKEN@github.com/you/your-state.git ~/state && tar xzf ~/state/state.tgz -C ~"}' ``` **Alternatives** - **Any URL.** Host the tarball anywhere reachable and `curl`/`wget` it from inside the computer the same way. A Git repo is the tidiest, versioned option. - **Small files (< 10 MB).** `POST /files/upload` drops a file straight onto the computer. It caps at **10 MB**. For larger state, use the Git or URL pull above, or split into < 10 MB batches. Reference: [Connecting to the server](https://docs.hetzner.com/cloud/servers/getting-started/connecting-to-the-server/), [Migrate data](https://docs.hetzner.com/cloud/servers/getting-started/migrate-data/) (Hetzner's own doc uses `scp`). ## Set up on Orgo ```bash curl -X POST https://www.orgo.ai/api/computers \ -H "Authorization: Bearer $ORGO_API_KEY" -H "Content-Type: application/json" \ -d "{\"workspace_id\":\"$WORKSPACE_ID\",\"name\":\"prod\",\"ram\":8}" # → save the returned id as $COMPUTER_ID, then run your installer via /bash ``` See the [Quickstart](https://docs.orgo.ai/quickstart) for the full control surface (`bash`, `exec`, `screenshot`, click/type). ## What's different Neutral, both directions: - **Hetzner does, Orgo doesn't:** a general-purpose Linux server you fully own, with arbitrary services and ports, managed networking ([primary/floating IPs](https://docs.hetzner.com/cloud/servers/primary-ips/overview/), [firewalls](https://docs.hetzner.com/cloud/firewalls/overview/), private networks), and built-in daily [backups + persistent snapshots](https://docs.hetzner.com/cloud/servers/backups-snapshots/overview/). - **Orgo does, Hetzner doesn't out of the box:** a persistent GUI **computer** with a built-in agent-control API (screenshot / click / type), online in seconds, with nothing to administer. You manage no OS, no firewall, and no TLS. A Hetzner server has no public network interface until you assign a [Primary IP](https://docs.hetzner.com/cloud/servers/primary-ips/overview/). Orgo exposes its API through an authenticated proxy instead. Rule of thumb: keep general server workloads and public-facing services on Hetzner. Move to Orgo when the job is an **agent operating a durable computer or browser**. ## Reference **Hetzner Cloud Servers:** Their VM **Migrate data:** scp file transfer **Orgo Quickstart:** Control a computer over HTTP **Move an agent (Hermes/OpenClaw):** Full state-preserving playbook --- # Migrate to Orgo Source: https://docs.orgo.ai/guides/migrate Summary: Move Hermes Agent or OpenClaw from DigitalOcean, Fly.io, or Hetzner onto an Orgo cloud computer with memory, skills, and sessions intact. Orgo gives an AI agent a full cloud computer to operate: desktop, browser, shell, files, and a built-in HTTP control API. This guide moves [Hermes Agent](https://hermes-agent.nousresearch.com/) or [OpenClaw](https://openclaw.ai) from DigitalOcean, Fly.io, or Hetzner onto an Orgo cloud computer with memory, skills, sessions, and configuration preserved. ## Comparison Chart | | VPS | Orgo | |---|---|---| | You manage | OS, firewall, VNC, browser, fonts, TLS, monitoring, backups | Nothing | | Time to a desktop | 1-3 hours | One API call | | Boot time | 30 s to 2 min | Online in seconds | | Click / type / screenshot | Build it yourself | Built in | | Run shell | `ssh user@host` | `computer.bash(...)` | | Run Python | Set up Jupyter | `computer.exec(...)` | | File transfer | `scp` / `rsync` | `/files/upload` | | HTTP API + TLS | Floating IP + DNS + certs | `www.orgo.ai/api/desktops/{instance_id}/proxy/` (your API key), automatic TLS | | Live-resize | Usually a reboot | `PATCH /resize` | | Snapshot / clone | Manual | `POST /clone` | | Uptime | You patch and restart | Continuous by default | Orgo is purpose-built for agents that operate a desktop, browser, or any app without a programmatic API. Pure headless backend scripts are equally well served by a VPS. ## Pick your agent **Hermes Agent** Nous Research. Self-improving agent with persistent skills + memory. **OpenClaw** Open-source personal AI assistant with messaging-platform gateways. ## Five steps 1. **Snapshot** the agent's state directory on the source. 2. **Provision** an Orgo computer. 3. **Transfer** the tarball. 4. **Install** the agent with its upstream installer. 5. **Restore + start**, then verify. Same recipe for both agents. Only step 3 varies by source platform. ## State lives in one directory | Agent | Directory | Contents | |---|---|---| | Hermes | `~/.hermes/` | `.env`, `config.yaml`, `SOUL.md`, `sessions/`, `memories/`, `skills/`, `cron/`, `hooks/`, `pairing/`, `whatsapp/`, `logs/`, `image_cache/`, `audio_cache/` | | OpenClaw | `~/.openclaw/` | `openclaw.json`, `workspace/` (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `skills//SKILL.md`) | Snapshot that single directory and you preserve identity, memory, skills, and API keys. ## Run the migration Create an API key in Settings → Credentials at [orgo.ai/settings/credentials](https://www.orgo.ai/settings/credentials) and export it as `ORGO_API_KEY`. Export your workspace ID as `WORKSPACE_ID`. Then pick the tab for the agent you're moving. **Warning:** An Orgo computer does not run systemd, so `systemctl --user`, `hermes gateway install`, and `openclaw onboard --install-daemon` cannot register a background service there. The steps below run the gateway in a detached `tmux` session instead. It does not come back on its own after a restart. For a gateway that supervisord restarts on crash and on boot, launch the curated Hermes Agent or OpenClaw [template](https://docs.orgo.ai/guides/templates/introduction) instead. **Hermes Agent** **Snapshot on the source** ```bash # Sanity check there's room for the tarball df -h ~ systemctl --user stop hermes-gateway 2>/dev/null || true pkill -f 'hermes gateway' 2>/dev/null || true tar czf /tmp/hermes-state.tgz -C "$HOME" .hermes ls -lh /tmp/hermes-state.tgz ``` Tarball is typically 5 MB to a few hundred MB depending on session and media-cache size. **Create the Orgo computer** ```bash REST curl -X POST https://www.orgo.ai/api/computers \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"workspace_id\":\"$WORKSPACE_ID\",\"name\":\"hermes-prod\",\"ram\":8,\"disk_size_gb\":32}" # → save the returned id as $COMPUTER_ID ``` ```bash CLI orgo computers create \ --workspace $WORKSPACE_ID --name hermes-prod \ --ram 8 --disk 32 ``` **Transfer the tarball in** Pick the source platform tab in [Transfer the tarball](#transfer-the-tarball). End state: `/root/Desktop/hermes-state.tgz` exists on the Orgo computer. **Install Hermes** Run any shell command in the VM via `POST /computers/{id}/bash`. For interactive work, `orgo ssh hermes-prod` opens a live terminal. ```bash curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/bash \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command":"curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash","timeout":300}' ``` The installer can run longer than the default 200-second `timeout`, so the request asks for the maximum, 300. It pulls Python 3.11, Node.js, `uv`, ripgrep, ffmpeg. Registers `hermes` on `$PATH`. Creates an empty `~/.hermes/` skeleton, which the next step replaces. **Restore and start** ```bash # Restore over the fresh skeleton curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/bash \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command":"set -e; rm -rf ~/.hermes/sessions ~/.hermes/memories ~/.hermes/skills ~/.hermes/whatsapp ~/.hermes/cron ~/.hermes/hooks ~/.hermes/pairing ~/.hermes/image_cache ~/.hermes/audio_cache; tar xzf /root/Desktop/hermes-state.tgz -C \"$HOME\"; chmod 600 ~/.hermes/.env"}' # Start the gateway in a detached tmux session curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/bash \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command":"tmux new-session -d -s hermes-gateway \"hermes gateway run --no-supervise\""}' ``` `--no-supervise` keeps the gateway in the tmux session. Without it, current Hermes releases hand the gateway to an s6 or systemd service, which an Orgo computer does not have, and exit. **Verify** ```bash curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/bash \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command":"hermes doctor && ls ~/.hermes/sessions/ | head && tmux has-session -t hermes-gateway && echo gateway running"}' ``` **OpenClaw** **Snapshot on the source** ```bash # Sanity check there's room for the tarball df -h ~ # Linux source systemctl --user stop openclaw 2>/dev/null || true # macOS source launchctl unload ~/Library/LaunchAgents/ai.openclaw.plist 2>/dev/null || true tar czf /tmp/openclaw-state.tgz -C "$HOME" .openclaw ls -lh /tmp/openclaw-state.tgz ``` **Create the Orgo computer** ```bash REST curl -X POST https://www.orgo.ai/api/computers \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"workspace_id\":\"$WORKSPACE_ID\",\"name\":\"openclaw-prod\",\"ram\":8,\"disk_size_gb\":32}" # → save the returned id as $COMPUTER_ID ``` ```bash CLI orgo computers create \ --workspace $WORKSPACE_ID --name openclaw-prod \ --ram 8 --disk 32 ``` **Transfer the tarball in** Pick the source platform tab in [Transfer the tarball](#transfer-the-tarball). End state: `/root/Desktop/openclaw-state.tgz` exists on the Orgo computer. **Install OpenClaw** ```bash curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/bash \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command":"curl -fsSL https://openclaw.ai/install.sh | bash","timeout":300}' ``` Pulls Node 24 if missing and installs the `openclaw` CLI globally. **Restore and start the gateway** ```bash curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/bash \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command":"set -e; rm -rf ~/.openclaw; mkdir -p ~/.openclaw; tar xzf /root/Desktop/openclaw-state.tgz -C \"$HOME\"; tmux new-session -d -s openclaw-gateway \"openclaw gateway run\"; sleep 5; openclaw gateway status"}' ``` **Verify** ```bash curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/bash \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command":"openclaw doctor && ls ~/.openclaw/workspace/skills/ | head && cat ~/.openclaw/openclaw.json"}' ``` Dashboard inside the VM: `http://127.0.0.1:18789/`. ## Transfer the tarball A Hermes/OpenClaw tarball is usually larger than the **10 MB** `/files/upload` limit, so pull it in **from inside the computer**: no size cap, no laptop round-trip. Extract on the source, put the tarball somewhere the computer can reach (a private Git repo or any temporary URL), then `git clone`/`curl` it via `POST /computers/{id}/bash`. **Extract on the source** (see the provider page for detail): **DigitalOcean** ```bash ssh root@ "tar czf /tmp/hermes-state.tgz -C /root .hermes" scp root@:/tmp/hermes-state.tgz ./hermes-state.tgz ``` More: [Migrate from DigitalOcean](https://docs.orgo.ai/guides/migrations/digitalocean). **Fly.io** Machines have no public SSH, so this runs over Fly's WireGuard mesh: ```bash fly ssh console -a YOUR_APP -C "tar czf /tmp/hermes-state.tgz -C /root .hermes" fly ssh sftp get -a YOUR_APP /tmp/hermes-state.tgz ./hermes-state.tgz ``` More: [Migrate from Fly.io](https://docs.orgo.ai/guides/migrations/fly-io). **Hetzner** ```bash ssh root@ "tar czf /tmp/hermes-state.tgz -C /root .hermes" scp root@:/tmp/hermes-state.tgz ./hermes-state.tgz ``` More: [Migrate from Hetzner](https://docs.orgo.ai/guides/migrations/hetzner). **Pull it into the computer.** Commit the tarball to a private repo (or host it at a URL), then land it on the desktop: ```bash curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/bash \ -H "Authorization: Bearer $ORGO_API_KEY" -H "Content-Type: application/json" \ -d '{"command":"git clone https://TOKEN@github.com/you/hermes-state.git /tmp/pull && mv /tmp/pull/hermes-state.tgz /root/Desktop/hermes-state.tgz"}' # Or from any reachable URL: # curl -fsSL "$STATE_URL" -o /root/Desktop/hermes-state.tgz ``` Small state (< 10 MB)? Skip the repo. `POST /files/upload` drops the file straight onto the desktop at `/root/Desktop/`. ## What carries over | Item | In the tarball | Notes | |---|---|---| | LLM API keys | Yes | `~/.hermes/.env` or `~/.openclaw/openclaw.json`. Re-chmod to 0600. | | Learned skills | Yes | `~/.hermes/skills/`, `~/.openclaw/workspace/skills/` | | Long-term memory | Yes | `~/.hermes/memories/`, OpenClaw `workspace/` | | Chat sessions | Yes | `~/.hermes/sessions/` | | Slack / Discord / Telegram bot tokens | Yes | In `.env`, re-read on gateway start | | Hermes hooks + per-user cron | Yes | `~/.hermes/hooks/`, `~/.hermes/cron/` | | WhatsApp / iMessage / Signal pairings | No | Device-bound. Re-scan QR. | | Systemd unit files | No | Orgo computers do not run systemd. Start the gateway in `tmux`, as shown in [Run the migration](#run-the-migration). | | `journald` logs | No | App logs in `~/.hermes/logs/` do carry. | | System `crontab -e` entries | No | Only `~/.hermes/cron/` carries. | | Hand-installed OS packages | No | Re-install. Installer covers the defaults. | | Floating IPs / custom DNS | No | Reach the desktop's API via `www.orgo.ai/api/desktops/{instance_id}/proxy/` (your API key). | ## Gotchas **Warning:** **Don't run two gateways on the same bot token.** Stop the source gateway first, verify on Orgo, then delete the source. Two live gateways race for incoming messages. **Note:** **`hermes setup` rewrites `.env`.** Run the restore step *after* install. Don't re-run the wizard or it overwrites the API keys you brought over. - **sqlite needs disk headroom.** `fsync` returns `EIO` (Errno 5) on a full disk. Size the computer with at least 2× the source state-dir as free disk. - **Device-bound pairings re-pair.** WhatsApp Web, iMessage, Signal. Re-scan the QR on Orgo. - **Inbound webhooks don't hit the VM directly anymore.** There's no public per-VM hostname; reach the desktop's API through the authenticated proxy (`https://www.orgo.ai/api/desktops/{instance_id}/proxy/`, with your Orgo API key), or run gateways in outbound/socket mode. ## Rollback Keep the source machine powered on with its gateway stopped until you're satisfied. To revert: ```bash systemctl --user start hermes-gateway # Hermes systemctl --user start openclaw # OpenClaw ``` Delete the source after a few hours of clean operation on Orgo. ## Smoke test `hermes doctor` only confirms the binary is wired. To prove memory restored correctly, ask the agent something only it would know: **Hermes** ```bash curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/bash \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command":"hermes chat \"What did we last discuss in our previous session?\""}' ``` The reply should reference real context from the source machine. Generic answers ("I don't have memory of past chats") mean `~/.hermes/sessions/` or `~/.hermes/memories/` didn't restore. Re-check the `tar xzf` step, and confirm you didn't extract before the install step ran. **OpenClaw** ```bash curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/bash \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command":"openclaw send self \"What was our last topic?\""}' ``` If the reply has no continuity, inspect `~/.openclaw/workspace/` to confirm sessions and skill files copied across. ## Troubleshooting **Gateway won't start after restore** ```bash # Hermes tmux capture-pane -p -t hermes-gateway -S -80 # OpenClaw tmux capture-pane -p -t openclaw-gateway -S -80 ``` Most common cause: `.env` lost its 0600 mode during transfer, or `hermes setup` ran a second time and overwrote it. Restore from the tarball, then `chmod 600 ~/.hermes/.env`. **sqlite returns 'database is locked' or fsync EIO** ```bash df -h / # disk full? ``` If disk is at 100%, the in-VM gateway can't write checkpoints. Bump `disk_size_gb` with a live resize: ```bash curl -X PATCH https://www.orgo.ai/api/computers/$COMPUTER_ID/resize \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"disk_size_gb": 64}' ``` **Bot is unresponsive in Slack / Discord / Telegram** Two gateways online with the same bot token race for incoming messages. Stop the source: ```bash systemctl --user stop hermes-gateway # or: openclaw ``` Wait 30 s, then re-test from the messaging client. If still silent, check the gateway logs on Orgo for `unauthorized`-shaped errors. The bot token may have been rotated between snapshot and restore. **WhatsApp QR won't load** The pairing in `~/.hermes/whatsapp/` is bound to the old device's hardware fingerprint. Wipe it and re-pair: ```bash rm -rf ~/.hermes/whatsapp hermes whatsapp # or: openclaw whatsapp ``` **Upload landed but I can't find the file in the VM** `POST /files/upload` drops files at `/root/Desktop/` inside the VM. Verify: ```bash curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/bash \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command":"ls -lh /root/Desktop/"}' ``` If the file isn't there, check the upload's response body first. A `200` means the file was stored and pushed to the computers in that workspace that were running at the time. A file whose name already exists on the Desktop is not replaced. ## Full script One bash file that runs the whole Hermes migration with `curl` + `jq`. Set `ORGO_API_KEY`, `WORKSPACE_ID`, and `STATE_URL`, a reachable URL to your state tarball. A private GitHub raw link or a presigned S3 URL both work. ```bash #!/usr/bin/env bash set -e : "${ORGO_API_KEY:?Missing ORGO_API_KEY}" : "${WORKSPACE_ID:?Missing WORKSPACE_ID}" : "${STATE_URL:?Missing STATE_URL - a reachable URL to your state tarball}" API="https://www.orgo.ai/api" AUTH="Authorization: Bearer $ORGO_API_KEY" JSON="Content-Type: application/json" # 1. Create computer RESP=$(curl -sS -X POST "$API/computers" -H "$AUTH" -H "$JSON" \ -d "{\"workspace_id\":\"$WORKSPACE_ID\",\"name\":\"hermes-prod\",\"ram\":8,\"disk_size_gb\":32}") COMPUTER_ID=$(echo "$RESP" | jq -r .id) echo "Created computer $COMPUTER_ID" # 2. Pull state onto the desktop from STATE_URL (no 10 MB upload cap) curl -sS -X POST "$API/computers/$COMPUTER_ID/bash" -H "$AUTH" -H "$JSON" \ -d "$(jq -n --arg u "$STATE_URL" '{command:("curl -fsSL " + $u + " -o /root/Desktop/hermes-state.tgz")}')" > /dev/null # Helper: run a bash command inside the VM run() { curl -sS -X POST "$API/computers/$COMPUTER_ID/bash" -H "$AUTH" -H "$JSON" \ -d "$(jq -n --arg c "$1" '{command:$c, timeout:300}')" echo } # 3. Install Hermes run "curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash" # 4. Restore state on top of the fresh skeleton run 'set -e rm -rf ~/.hermes/sessions ~/.hermes/memories ~/.hermes/skills \ ~/.hermes/whatsapp ~/.hermes/cron ~/.hermes/hooks \ ~/.hermes/pairing ~/.hermes/image_cache ~/.hermes/audio_cache tar xzf /root/Desktop/hermes-state.tgz -C "$HOME" chmod 600 ~/.hermes/.env' # 5. Start the gateway in a detached tmux session run 'tmux new-session -d -s hermes-gateway "hermes gateway run --no-supervise"' # 6. Verify run "hermes doctor" echo echo "Done. Computer $COMPUTER_ID" ``` Swap the install URL + state-dir paths to migrate OpenClaw with the same script. ## Reference **Hermes Agent docs** Full Nous Research documentation **Hermes Agent GitHub** Source + release notes **OpenClaw site** Project home **OpenClaw GitHub** Source + `AGENTS.md` spec **DigitalOcean snapshots** Source-platform snapshots **Fly.io SSH & SFTP** `flyctl ssh sftp` reference **Hetzner snapshots** Source-platform snapshots **Orgo SDK quickstart** `computer.bash`, `.exec`, `.files` Need a migration outside this shape? [spencer@orgo.ai](mailto:spencer@orgo.ai) or [Discord](https://discord.gg/tbYGpvnnJD). --- # Templates Source: https://docs.orgo.ai/guides/templates/introduction Summary: Reproducible cloud computers, defined in a single file and launched in seconds. A template is a declarative spec for an Orgo computer. One file describes the hardware, the software to install, the services to run, the secrets it needs, and what the user sees on connect. Orgo builds that file once into a **golden snapshot**. Every launch starts from it in seconds, fully configured and identical every time. Think of it as a `Dockerfile` for a full desktop computer: write it once, version it, and hand out reproducible computers from it. **Quickstart** Author, build, and launch your first template. **Schema reference** Every field in the `orgo.ai/v1` format. **Secrets** Inject API keys without baking them in. **API reference** Publish, build, and launch over HTTP. ## Why templates Without a template, every new computer starts from a base image and you script the setup yourself: install packages, write config, start services, and wait for it all to converge. That work runs on every boot and drifts over time. A template moves all of that to **build time**. The result is a snapshot that boots ready. | | Base image + setup script | Template | |---|---|---| | Time to ready | Minutes of install on every boot | Seconds, restoring a prebuilt snapshot | | Reproducibility | Drifts as packages and scripts change | Content-addressed, identical every launch | | Definition | Imperative script you maintain | One declarative file, versioned | | Services & health | You wire up supervisord and watchdogs | Declared once, managed for you | | Sharing | Copy the script around | Publish a `ref`, launch it into any of your workspaces | ## Golden snapshots When you **build** a template, Orgo boots a real computer, runs your install steps, then pauses it and captures its full state (disk, memory, and CPU) as a *golden snapshot*. Launching a computer from the template copies that snapshot and resumes it, unless the launch has to cold-boot from the snapshot's disk instead. - **Build once:** rebuild only when the template changes. - **Launch in seconds:** a launch skips the entire install phase. - **Content-addressed:** the snapshot is keyed by a SHA-256 `digest` of the canonical template. Two identical templates share a build. Change one byte and you get a new digest. This is why a template computer comes up with Node already installed and your service already running, while a base image would still be downloading packages. **Note:** A launch that injects your [secrets](https://docs.orgo.ai/guides/templates/secrets), or lands on a host that cannot restore the snapshot's memory, cold-boots from the snapshot's disk instead, and the `on_every_boot` [hook](https://docs.orgo.ai/guides/templates/schema#hooks) runs. `on_first_boot` runs only during the build, and launched computers do not run `on_resume`. ## Lifecycle **Write** Author a template in YAML or JSON using the [`orgo.ai/v1`](https://docs.orgo.ai/guides/templates/schema) format. A short [sugar form](https://docs.orgo.ai/guides/templates/schema#sugar-form) keeps simple templates tiny. **Publish** `POST` the document to your registry. Refs are immutable and content-addressed: `namespace/name@version`. **Build** Bake the golden snapshot, and poll its build status until `ready`. **Launch** Create a computer with `template_ref`. It starts from the golden snapshot, pre-configured. ```text WRITE → PUBLISH → BUILD → LAUNCH yaml registry golden seconds immutable once per computer ``` ## Refs Every template version is addressed by a **ref**: ```text namespace / name @ version default / my-template @ 1.0.0 ``` - **namespace:** groups your templates. Your own default to `default`. Curated templates published by Orgo live in the `system` namespace (for example, `system/coding@1.0.4`). - **name:** lowercase kebab-case. - **version:** semver, immutable once published. Bump it to ship a change. Pass a ref as `template_ref` to [Create computer](https://docs.orgo.ai/api-reference/computers/create), or anywhere the API takes a template. ## Curated vs. your own **Curated templates** Published and maintained by Orgo in the `system` namespace: Coding (Claude Code and Codex), OpenClaw, and Hermes Agent. **Any paid plan can launch them.** Browse with [List curated templates](https://docs.orgo.ai/api-reference/templates/list-curated). **Your templates** Author and publish your own on a **Scale** plan. They live in your namespaces and launch into your workspaces. **Note:** **Plan requirements.** Creating a computer from a template is a computer create like any other: it needs a paid plan and counts against the workspace owner's computer quota. When the workspace owner is on the free plan, that quota is zero, so a launch, curated or your own, returns `403` with code `UPGRADE_REQUIRED`. **Publishing, building, and test-running** your own templates additionally require a **Scale** plan. See [pricing](https://www.orgo.ai/#pricing). ## What goes in a template A quick tour. See the [schema reference](https://docs.orgo.ai/guides/templates/schema) for every field. | Field | What it does | |---|---| | `hardware` | CPU, RAM, disk, resolution, audio, bandwidth | | `build` | `apt` / `pip` / `npm` / `run` steps baked into the snapshot | | `apps` | Installed apps with long-running [services and health checks](https://docs.orgo.ai/guides/templates/schema#apps) | | `files` | Inline or fetched files written into the computer | | `env` + `secrets` | Environment variables and [vault-injected secrets](https://docs.orgo.ai/guides/templates/secrets) | | `triggers` | [Reactive automation](https://docs.orgo.ai/guides/templates/triggers): a source fires, actions run | | `terminal` | Pre-staged tmux sessions the browser terminal attaches to | | `hooks` | Shell that runs at [lifecycle points](https://docs.orgo.ai/guides/templates/schema#hooks). Today only the first-boot and every-boot hooks run | | `egress_policy` | Per-computer domain/IP allow or block rules | ## Next steps **Build your first template** A complete walkthrough, end to end. **See real examples** Annotated curated templates you can copy. --- # Templates quickstart Source: https://docs.orgo.ai/guides/templates/quickstart Summary: Launch a curated template, then author and ship your own. Two paths. Launching a curated template is one API call on any paid plan. Authoring your own takes a few minutes and a [Scale plan](https://www.orgo.ai/#pricing). This page covers both. ```bash export ORGO_API_KEY=sk_live_... export WORKSPACE_ID=550e8400-e29b-41d4-a716-446655440000 ``` ## Launch a curated template The fastest way to see templates in action: launch a computer from one Orgo maintains. Pass its `ref` as `template_ref` to [Create computer](https://docs.orgo.ai/api-reference/computers/create). The computer starts from the prebuilt golden snapshot with everything already installed. Creating a computer from a curated template needs a paid plan, the same as any other computer. It does not need a Scale plan; that is only for publishing your own. Curated templates live in the `system` namespace, for example `system/coding@1.0.4`. Call [List curated templates](https://docs.orgo.ai/api-reference/templates/list-curated) for the current catalog and its exact refs. ```bash cURL curl -X POST https://www.orgo.ai/api/computers \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"workspace_id\": \"$WORKSPACE_ID\", \"name\": \"claude-1\", \"template_ref\": \"system/coding@1.0.4\" }" ``` ```python Python import os, requests r = requests.post( "https://www.orgo.ai/api/computers", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, json={ "workspace_id": os.environ["WORKSPACE_ID"], "name": "claude-1", "template_ref": "system/coding@1.0.4", }, ) computer = r.json() print(computer["id"], computer["status"]) ``` ```javascript JavaScript const r = await fetch("https://www.orgo.ai/api/computers", { method: "POST", headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ workspace_id: process.env.WORKSPACE_ID, name: "claude-1", template_ref: "system/coding@1.0.4", }), }); const computer = await r.json(); console.log(computer.id, computer.status); ``` The computer boots with Claude Code and Codex already installed, each in its own terminal session. Browse the full catalog with [List curated templates](https://docs.orgo.ai/api-reference/templates/list-curated). **Tip:** Hardware in the request overrides the template's defaults. Add `"cpu"` and `"ram"` to launch the same template on a bigger computer. A value above your plan's per-computer limit returns `403`. From the [CLI](https://docs.orgo.ai/guides/cli), `orgo templates list` shows the catalog and `orgo templates run system/coding@1.0.4 --name claude-1` launches the same computer. --- ## Author your own Now build a template from scratch. This section recreates a minimal Claude Code environment so you can see every step. It publishes to your own `default` namespace as `default/claude-code@1.0.0`. That is your private template, separate from the curated templates in the `system` namespace. **Note:** Publishing and building require a [Scale plan](https://www.orgo.ai/#pricing); below Scale, both return `403` with code `UPGRADE_REQUIRED`. Launching what you build counts against your normal computer quota. **Write the template** Save this as `claude-code.yaml`. It declares the hardware, an optional secret, a build-time install, and the terminal session the browser attaches to. ```yaml claude-code.yaml api_version: orgo.ai/v1 template: name: claude-code version: 1.0.0 description: Claude Code CLI, ready in the terminal. hardware: cpu: 2 ram_gb: 4 resolution: 1280x720x24 # The launching user supplies this from their vault; injected at create time. secrets: - name: anthropic_api_key description: Anthropic API key, used by Claude Code when set. optional: true # Baked into the golden snapshot at build time. apps: - name: claude-code title: Claude Code install: | curl -fsSL https://deb.nodesource.com/setup_22.x | bash - apt-get install -y nodejs npm install -g @anthropic-ai/claude-code # The tmux session the browser terminal attaches to by name. terminal: - name: claude-code cwd: /root ``` **Validate it** Catch mistakes before publishing. [Validate](https://docs.orgo.ai/api-reference/templates/validate) has no side effects. ```bash curl -X POST https://www.orgo.ai/api/templates/validate \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/yaml" \ --data-binary @claude-code.yaml ``` A clean template returns `200` with `{ "ok": true, "template": { … } }`. An invalid one returns `422` with `{ "ok": false, "errors": [ … ] }`, each entry carrying the exact `field`, a `code`, and a `message`. **Publish and build** [Publish](https://docs.orgo.ai/api-reference/templates/publish) with `?auto_build=true` to register the ref and start baking the golden snapshot in one call. ```bash curl -X POST "https://www.orgo.ai/api/templates?auto_build=true" \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/yaml" \ --data-binary @claude-code.yaml ``` ```json { "ref": "default/claude-code@1.0.0", "digest": "a1b2c3d4e5f6...", "published": "2026-06-08T17:00:00Z", "auto_build": "building", "build": { "queued": true, "status": "queued", "job_id": "9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f", "state": "queued", "phase": "queued", "tier": "standard", "ahead": 0 } } ``` From the [CLI](https://docs.orgo.ai/guides/cli), `orgo templates validate claude-code.yaml` checks the file, and `orgo templates publish claude-code.yaml --build` publishes it and starts the build. **Wait for the build** Poll [Get build status](https://docs.orgo.ai/api-reference/templates/build-status) until `status` is `ready`. The build runs on a dedicated build runner, which does not publish to [Stream build logs](https://docs.orgo.ai/api-reference/templates/build-events), so polling is how you follow it. ```bash curl https://www.orgo.ai/api/templates/default/claude-code/1.0.0/build \ -H "Authorization: Bearer $ORGO_API_KEY" # -> {"status": "building"} ... {"status": "ready"} ``` **Launch a computer from it** Once the build is `ready`, create a computer with your `template_ref`. ```bash curl -X POST https://www.orgo.ai/api/computers \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"workspace_id\": \"$WORKSPACE_ID\", \"name\": \"claude-dev\", \"template_ref\": \"default/claude-code@1.0.0\" }" ``` The computer starts from the golden snapshot, with Node and Claude Code already installed and your terminal session ready. Poll `https://www.orgo.ai/api/desktops/{instance_id}/proxy/health` until it returns `200`, then connect. See [Create computer](https://docs.orgo.ai/api-reference/computers/create) for the full response and connection details. ## Iterate Refs are immutable, so you have two ways to ship a change: - **Bump the version:** `1.0.0` → `1.0.1`. The clean path for anything you've shared. - **Force-replace:** re-publish the same version with [`?force=true`](https://docs.orgo.ai/api-reference/templates/publish) while you're still iterating locally. ```bash curl -X POST "https://www.orgo.ai/api/templates?auto_build=true&force=true" \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/yaml" \ --data-binary @claude-code.yaml ``` ## Next steps **Add secrets** Let users supply their own API keys. **Run services** Long-running processes with health checks. **React to events** Cron, file, HTTP, and metric triggers. **Full API** Every templates endpoint. --- # Template schema Source: https://docs.orgo.ai/guides/templates/schema Summary: Every field in the orgo.ai/v1 template format. A template is a single `orgo.ai/v1` document, written in YAML or JSON. This page is the field-by-field reference. The canonical machine-readable contract is the [JSON Schema](https://docs.orgo.ai/api-reference/templates/schema) at `GET /api/template-schema`. Point your editor at it for autocomplete and inline validation. ```yaml # yaml-language-server: $schema=https://www.orgo.ai/api/template-schema ``` **Warning:** The published JSON Schema describes the **canonical** form only. It requires `api_version` and `template`, and it rejects unknown top-level keys. An editor validating against it therefore flags every [sugar](#sugar-form) document as invalid on two counts. Sugar is desugared server-side, so check a sugar template with [Validate template](https://docs.orgo.ai/api-reference/templates/validate) instead of relying on editor validation. ## Two forms Templates accept a **canonical** form and a shorter **sugar** form. Both normalize to the same document and the same `digest`. ```yaml Canonical api_version: orgo.ai/v1 template: name: my-workstation version: 1.0.0 hardware: cpu: 2 ram_gb: 4 resolution: 1280x800x24 env: NODE_ENV: production files: - to: /opt/welcome.txt inline: "hello" hooks: on_every_boot: | echo "ready" ``` ```yaml Sugar api_version: orgo.ai/v1 name: my-workstation@1.0.0 cpu: 2 ram: 4gb display: 1280x800 env: NODE_ENV: production files: /opt/welcome.txt: "hello" on_every_boot: | echo "ready" ``` ### Sugar → canonical | Sugar | Canonical | |---|---| | `name: foo@1.0.0` | `template: { name: foo, version: 1.0.0 }` | | `version:`, `description:`, `publisher:` | the matching `template` field | | `cpu: 2` | `hardware: { cpu: 2 }` | | `ram: 4gb` | `hardware: { ram_gb: 4 }` | | `disk: 30gb` | `hardware: { disk_gb: 30 }` | | `display: 1280x800` | `hardware: { resolution: 1280x800x24 }`, where depth defaults to `24` | | `gpu: t4` / `audio: true` | the matching `hardware` field | | `bandwidth: 500` | `hardware: { bandwidth_mbps: 500 }` | | `files: { /path: "text" }` | `files: [{ to: /path, inline: "text" }]` | | `files: { /path: { from: …, mode: … } }` | `files: [{ to: /path, from: …, mode: … }]` | | `apps: { server: "cmd" }` | one app named `server` with one `restart: always` service running `cmd` | | `on_every_boot: \|` | `hooks: { on_every_boot: … }`, with the same shortcut for the other four hooks | | `wallpaper: "https://…"` | a `files` entry downloading the image to `/usr/share/backgrounds/wallpaper.jpg`, plus an `on_every_boot` hook that sets it as the XFCE background. That hook runs during the build's cold boot, so the wallpaper is baked into the golden snapshot | `api_version` has no sugar: every document needs it, in either form. Don't mix the forms: a document with a canonical `template:` block that otherwise parses as canonical is read as canonical, and its top-level sugar keys, such as `cpu` or `on_every_boot`, are ignored. In a sugar document, a canonical `hardware` value wins over its flat key: if a document sets both `hardware.cpu` and top-level `cpu`, the `hardware` value is used and the sugar key is ignored. `ram` and `disk` accept `4gb`, `4g`, or a bare `4`; a sub-gigabyte size such as `512mb` resolves to `0`, which is the same as omitting it. ## Top-level fields | Field | Type | Required | Description | |---|---|:---:|---| | `api_version` | string | yes | Must be `orgo.ai/v1`. Any other value is rejected with code `unknown_api_version`. | | `template` | object | yes | [Identity](#metadata): name, version, description. | | `hardware` | object | no | [Computer resource shape](#hardware). Omitted, every hardware field takes its default. | | `secrets` | array | no | [Vault secrets](https://docs.orgo.ai/guides/templates/secrets) the template needs. Max 64. | | `vars` | map | no | [Variables](#vars-and-interpolation) for `${var.X}` interpolation. | | `env` | map | no | [Environment variables](#env), literal or secret-backed. | | `build` | object | no | [Build-time install steps](#build). | | `files` | array | no | [Files](#files) written into the computer. Max 256. | | `apps` | array | no | [Apps, services, and health checks](#apps). Max 64. | | `triggers` | array | no | [Reactive automation](https://docs.orgo.ai/guides/templates/triggers). Max 256. | | `terminal` | array | no | [Pre-staged tmux sessions](#terminal). Max 32. | | `hooks` | object | no | [Lifecycle shell hooks](#hooks). | | `telemetry` | object | no | [Metrics and logs](#telemetry). | | `egress_policy` | object | no | [Per-computer network rules](#egress-policy). Omitted, no filtering is applied. | | `streaming` | array | no | [Outbound RTMP streams](#streaming). Max 16. | Every field except `api_version` and `template` is optional, so the smallest valid template is a name, a version, and the API version. ## template Identity and provenance. `name` and `version` are required. ```yaml template: name: claude-code # lowercase kebab-case, 1-64 chars version: 1.0.0 # semver, immutable once published description: Claude Code CLI, ready in the terminal. publisher: orgo # optional handle (kebab-case) license: MIT # optional SPDX id (informational) homepage: https://… # optional source: https://github.com/… # optional ``` ## hardware The computer's resource shape. Every field is optional; omit one and the platform default applies. A [Create computer](https://docs.orgo.ai/api-reference/computers/create) request's `cpu`, `ram`, and disk override these at launch. The template's `resolution` always wins over the request's, because the golden snapshot was captured at it. | Field | Type | Values | Notes | |---|---|---|---| | `os` | string | `linux` | Linux only. | | `cpu` | integer | `1`, `2`, `4`, `8`, `16` | vCPU count. Any other value is rejected. Capped by your plan. | | `ram_gb` | integer | `4`, `8`, `16`, `32`, `64` | RAM in GB. Any other value is rejected. Capped by your plan. | | `disk_gb` | integer | ≥ 1 | Disk in GB. | | `gpu` | string | `none`, `t4`, `l4`, `a10`, `l40s`, `a100-40`, `a100-80`, `h100` | Accepted, but not used at launch. | | `gpu_count` | integer | ≥ 0 | Accepted, but not used at launch. | | `resolution` | string | `WIDTHxHEIGHTxDEPTH` | e.g. `1280x720x24`. All three parts are required in the canonical form. | | `auto_stop_minutes` | integer | `0`-`1440` | Accepted, but not applied: computers launched from a template are created always-on. | | `region` | string | `auto-us`, `iad`, `sjc`, `lax` | Accepted, but not used for placement. | | `audio` | boolean | `true` / `false` | Enable the audio device. Defaults to `false`. | | `bandwidth_mbps` | integer | ≥ 0 | Network bandwidth cap in Mbps. | ## vars and interpolation `vars` are compile-time strings, interpolated across the document before build. ```yaml vars: region: sjc2 tag: v7 env: REGION: "${var.region}" files: - to: /opt/config inline: "region=${var.region} tag=${var.tag}" ``` - `${var.X}` resolves from `vars`. - `${env.X}` resolves from a literal `env` value (not secret-backed ones). - `$${var.X}` escapes to a literal `${var.X}`. - Only the `var.` and `env.` forms are substituted, so a shell variable such as `${HOME}` passes through unchanged. - An unknown reference is not caught by [Validate template](https://docs.orgo.ai/api-reference/templates/validate) or at publish. It fails the build, and the build's `error` names the exact field path. ## env Environment variables written to the computer. Keys must be `UPPER_SNAKE_CASE`. Each value is a literal string, or a `{secret: }` reference resolved from the launching user's vault at create time. A value cannot be both. ```yaml secrets: - name: anthropic_api_key env: NODE_ENV: production ANTHROPIC_API_KEY: secret: anthropic_api_key ``` A `{secret: …}` reference to a name you never declared under `secrets` is a validation error (`unknown_reference`). Literal values are written to `/root/.env`, which is sourced at boot before services, terminal sessions, and boot hooks start, and by new interactive shells. Two caveats apply at launch, both covered in [Secrets](https://docs.orgo.ai/guides/templates/secrets#reference): - A `{secret: …}` reference is not applied. The value arrives only under the secret's own `UPPER_SNAKE_CASE` name, so the example above works because `ANTHROPIC_API_KEY` is that name. - A launch that injects secrets rewrites `/root/.env` with only the secrets, so literal values are not set on that computer. Put a value a service needs in that service's own `env`. ## build Package and command steps run once when baking the golden snapshot, before app installs. This is where dependencies get pre-installed so launches are instant. ```yaml build: apt: [ffmpeg, ripgrep] pip: [requests, httpx] npm: [pnpm] run: # up to 64 shell commands, in order - curl -fsSL https://example.com/install.sh | bash ``` ## files Files materialized into the computer. Each entry sets exactly one of `from` or `inline`. Setting both, or neither, is a validation error. ```yaml files: - to: /opt/hello.txt inline: "hello world" - to: /usr/share/backgrounds/bg.png from: https://example.com/bg.png # fetched once, at build - to: /opt/run.sh mode: "0755" # octal permissions inline: | #!/bin/bash echo hi when: runtime # build (default) or runtime ``` | Field | Required | Description | |---|:---:|---| | `to` | yes | Absolute destination path. No `..`, no reserved system paths. A `~/…` path passes validation but is not expanded, so write `/root/…` instead. | | `from` | one of | Source URI with scheme `http`, `https`, `git`, `s3`, `file`, or `secret`. See [File sources](#file-sources). | | `inline` | one of | Inline file content. | | `mode` | no | Octal permissions, 3 or 4 digits, e.g. `"0644"`. Omitted, the file is `0644`. | | `owner` / `group` | no | Ownership, resolved against the computer's `/etc/passwd`. A name that does not resolve leaves the file owned by root. | | `when` | no | `build` (the default, baked into the snapshot) or `runtime` (re-applied on each cold boot). | ### File sources Sources are fetched once, on the build machine, and the result is baked in. - `http` and `https` download the URL. - `git` clones over HTTPS: `git://github.com/owner/repo#path/to/file` writes that file, and a directory is written as a tar archive. - `s3` works only for a public AWS bucket. - `file` is accepted by validation, but the build cannot read it and fails. - `secret://` must name a declared secret, but the file is never written. See [Secrets](https://docs.orgo.ai/guides/templates/secrets#reference). **Warning:** `to` must be an absolute path with no `..`. Reserved system paths are rejected: `/proc`, `/sys`, `/boot`, `/dev`, `/tmp`, `/run`, plus the Orgo runtime (`/etc/orgo`, `/var/orgo`, `/orgo`, `/etc/supervisor`, and Orgo's own binaries under `/opt` and `/usr/local/sbin`). Your apps can still write under `/opt` and elsewhere. Only Orgo's own runtime files are off-limits. ## apps An app bundles an install step with the long-running services, health checks, and ports it needs. Services are managed by supervisord, so they start at boot and respawn on crash. ```yaml apps: - name: my-app title: "My Application" install: | npm install -g my-app services: - name: my-app-server run: "my-app serve --port 8000" cwd: /opt/my-app user: root # default: orgo (unprivileged) restart: always # always | on-failure | no env: PORT: "8000" health: type: http # http | tcp | command | process url: http://127.0.0.1:8000/health every: 15s timeout: 3s retries: 3 on_fail: restart_service:my-app-server ports: - { internal: 8000, public: true } autostart: - { run: "firefox http://localhost:8000", delay: 3 } ``` App `name` is required and must be unique kebab-case. `requires` lists other app names this one depends on, and `data` lists persistent data paths, validated the same way as `files[].to`. ### services | Field | Required | Description | |---|:---:|---| | `name` | yes | Unique service name (kebab-case), unique across all apps. | | `run` | yes | The long-lived command. | | `cwd` / `user` / `env` | no | Working directory, run-as user, and extra environment variables for this service. | | `restart` | no | `always` (default), `on-failure`, or `no`. | | `stop_signal` / `log` | no | Stop signal (e.g. `TERM`) and log file path. | ### health A polled liveness check. After `retries` consecutive failures, `on_fail` runs. This is the per-app **watchdog**. `type` is required, and so is the field that type checks (`url`, `port`, `command`, or `process`). The other fields are optional. | `type` | Checks | |---|---| | `http` | `url` returns 2xx/3xx | | `tcp` | `port` accepts a connection | | `command` | `command` exits `0` | | `process` | `process` name is running | `every` defaults to `30s`, `timeout` to `5s`, and `retries` to `3`. All durations are Go duration strings (`30s`, `2m`, `1h`). `on_fail` is one of `restart_service:`, `restart_vm`, `alert`, or `none`. Only `restart_service` acts: `restart_vm` and `alert` write a log line and do nothing else. ### ports | Field | Required | Description | |---|:---:|---| | `internal` | yes | In-computer port, 1-65535. `5900`, `5999`, `6080`, and `8080` are reserved by the Orgo runtime and rejected. | | `external` | no | Host port to publish a `public` port on. `0` (the default) picks a free one. | | `protocol` | no | `tcp`, `udp`, `http`, or `ws`. Accepted, but a published port forwards TCP only. | | `public` | no | Expose publicly. Defaults to `false`. When `true`, the port is mapped to a host port. [Test-run template](https://docs.orgo.ai/api-reference/templates/test-run) returns that mapping in `template_ports`; [Get computer](https://docs.orgo.ai/api-reference/computers/get) does not. | ## terminal Pre-staged tmux sessions. Each one is created detached on every cold boot unless it already exists, and its `run`, if set, is typed into it as the first command. A launch that restores the golden snapshot's memory keeps the sessions the build left running. Orgo's browser terminal attaches to them by name. `name` is required and must be unique. ```yaml terminal: - name: logs title: "Service Logs" cwd: /opt/my-app run: "tail -F /var/log/orgo/my-app.log" ``` **Note:** Sessions are **not** auto-restarted. For a process that must respawn, use an [app service](#apps) instead. ## hooks Shell that runs at lifecycle points. Each runs with `set -e` and times out after 10 minutes. **Warning:** **Only `on_first_boot` and `on_every_boot` run today.** Validation accepts `on_pre_snapshot`, `on_resume`, and `on_shutdown`, but Orgo's builds do not run `on_pre_snapshot`, and launched computers do not run `on_resume` or `on_shutdown`. | Hook | When | |---|---| | `on_first_boot` | Once, during the build. The marker it leaves is baked into the golden snapshot, so it never runs on a launched computer. | | `on_every_boot` | On every **cold** boot: during the build, and on a launch that cold-boots instead of restoring the snapshot's memory. A launch cold-boots when it injects your [secrets](https://docs.orgo.ai/guides/templates/secrets), or when its host cannot restore the snapshot. `/root/.env` is sourced first. | | `on_pre_snapshot` | Accepted, but not run. | | `on_resume` | Accepted, but not run. | | `on_shutdown` | Accepted, but not run. | ```yaml hooks: on_every_boot: | echo "cold boot at $(date)" >> /var/log/orgo/boots.log ``` **Tip:** Work that should be baked into the image belongs in [`build`](#build) or an app's `install`, not in a boot hook. To start a program with a launch-time secret, use a [terminal](#terminal) session's `run` or an [app service](#apps). See [Secrets](https://docs.orgo.ai/guides/templates/secrets#secrets-and-golden-snapshots). ## telemetry ```yaml telemetry: metrics: true # push system metrics to otel_endpoint logs: [my-app-server] # service names whose logs are pushed to otel_endpoint otel_endpoint: https://otel.example.com # OTLP/HTTP collector; nothing is pushed without it ``` Metrics go to `/v1/metrics` and logs to `/v1/logs`, as OTLP JSON. Each `logs` entry names a service, whose log is `/var/log/orgo/.log`. ## egress_policy Per-computer network filtering, applied on the host when the computer is created. `mode` is `allow` or `block` (`allowlist` and `denylist` also work), and `rules` lists domains, IPs, or CIDRs. The [JSON Schema](https://docs.orgo.ai/api-reference/templates/schema) requires both fields, but publish does not check them: a policy with no rules, or with any other `mode`, leaves the computer unfiltered. ```yaml egress_policy: mode: allow # allow or block; see the rules below rules: - "*.github.com" - "api.anthropic.com" - "10.0.0.0/8" ``` - **Domain rules filter DNS lookups only.** A rule matches any lookup whose name contains its second-level label, so `*.github.com`, `github.com`, and `api.github.com` all match every name containing `github`. - **IP and CIDR rules** filter traffic to those addresses. - **`block`** drops lookups that match a domain rule and traffic to a listed address. Everything else is allowed. - **`allow`** accepts traffic to listed addresses and lookups that match a domain rule, and drops all other lookups. With at least one domain rule, only lookups are filtered, so a connection made by IP address is not blocked. With only IP or CIDR rules, all other traffic is dropped. The VNC and Desktop API paths are always reachable. Omit the block entirely for no filtering. ## streaming Outbound RTMP(S) streams of the desktop. `name`, `url`, and `key` are required; `url` must start with `rtmp://` or `rtmps://`. ```yaml secrets: - name: stream_key streaming: - name: live url: rtmps://ingest.example.com/app key: secret: stream_key # literal string or a vault secret autostart: true # defaults to false ``` **Note:** `streaming` is parsed and validated, but no runtime streamer is wired up yet. A declared stream does not broadcast. ## Next steps **Secrets** The full secret-injection model. **Triggers** Sources, actions, and dedup. **Examples** Annotated real templates. **Publish API** Ship a template over HTTP. --- # Secrets Source: https://docs.orgo.ai/guides/templates/secrets Summary: Let templates require API keys without ever storing them. A template often needs credentials: an Anthropic key, a database URL, an OAuth token. Secrets let a template **declare** what it needs without ever containing the value. The launching user supplies it from their vault, and Orgo injects it into the computer at create time. The value never lives in the template, the registry, or the golden snapshot. ## Existing computers In Settings → Credentials, save the value in the Secrets section, then choose **Apply** and select a running Linux computer. The [Apply secrets endpoint](https://docs.orgo.ai/api-reference/computers/sync-secrets) provides the same action for API clients. Workspace values override account values with the same name. Open a new terminal or restart the agent afterward; running programs keep their previous environment. Updating the vault alone does not modify an existing computer, and deleting a vault entry does not remove copies already installed there. ## The model **Declare** The template lists the secrets it needs under `secrets`. Names only, no values. **Supply** The launching user stores the value in their vault (once, per account). **Inject** At create, Orgo writes the user's matching secrets into the computer at `/root/.env` (mode `0600`). They are never baked into the shared snapshot. ## Declare ```yaml secrets: - name: anthropic_api_key # lowercase; maps to the ANTHROPIC_API_KEY vault entry description: Anthropic API key, used by Claude Code. example: "sk-ant-..." docs_url: https://console.anthropic.com/settings/keys optional: true # template still launches when absent ``` | Field | Description | |---|---| | `name` | Lowercase, alphanumeric with `-` or `_`. Orgo maps it to its `UPPER_SNAKE_CASE` form (e.g. `ANTHROPIC_API_KEY`) and looks that name up in your vault. **Required.** | | `description` | Shown to the launching user. | | `example` | Example value shape, e.g. `sk-ant-...`. | | `docs_url` | Where to obtain the secret. | | `optional` | Marks the secret as one the template works without. Defaults to `false`. It is informational: the [CLI](https://docs.orgo.ai/guides/cli) labels each secret optional or required and names the required ones when you launch, but a launch never fails over a missing secret. | ## Reference At launch, Orgo writes each declared secret you have set to `/root/.env` under its `UPPER_SNAKE_CASE` name, so `anthropic_api_key` becomes `ANTHROPIC_API_KEY`. New interactive shells source `/root/.env`, so terminals see the variable. You can also name a declared secret in `env` with `{secret: }`: ```yaml secrets: - name: anthropic_api_key env: ANTHROPIC_API_KEY: secret: anthropic_api_key ``` **Warning:** When Orgo launches a built template, it does not apply `env` secret references or `secret://` files. The value arrives only under the secret's own `UPPER_SNAKE_CASE` name, so an `env` key receives it only when the key is that name, as above. A file with `from: secret://` passes validation but is never written. Only declared secrets you have set are injected. A launch never fails over a missing secret, whether or not it is `optional`: the variable is simply absent. ## Secrets and golden snapshots A template computer starts from the **golden snapshot**, which was built *before any particular user launched it*. The user's secret is injected per computer at create, after the snapshot was baked. So a launch that injects secrets does not restore the snapshot's memory. It cold-boots from the snapshot's disk with `/root/.env` already written, and `/root/.env` is sourced before app services, terminal sessions, and the `on_every_boot` hook start, so all of them see the secret. A launch with no secrets to inject restores the snapshot's memory when its host can, with every process as the build left it. On a launch that injects secrets, `/root/.env` holds only the secrets: literal `env` values the template wrote there are not set. Put a value a service needs in that service's own `env`. To start a program with the launcher's key, give a [terminal](https://docs.orgo.ai/guides/templates/schema#terminal) session a `run` command, or run the program as an [app service](https://docs.orgo.ai/guides/templates/schema#apps): ```yaml secrets: - name: anthropic_api_key optional: true terminal: - name: claude-code cwd: /root run: claude ``` This is the pattern the curated `system/coding` template uses: its `claude-code` and `codex` sessions run `claude` and `codex`. Do not rely on the `on_resume` hook for this, because launched computers do not run it. ## Managing your vault Add and update secret values in the Secrets section of Settings → Credentials, at [orgo.ai/settings/credentials](https://www.orgo.ai/settings/credentials). A secret is either account-wide or scoped to one workspace. Store an account-wide key once and every template you launch can request it by name. A workspace value takes precedence over an account value with the same name. From the [CLI](https://docs.orgo.ai/guides/cli), `orgo secrets set ANTHROPIC_API_KEY` stores a value (it prompts for the value when you omit it) and `orgo secrets list` shows the names you have set. ## Security - Values are **encrypted at rest** and never returned to a client in plaintext. - A secret is **never** part of the template document, its `digest`, or the golden snapshot. - Build logs never print secret values. - Only secrets a template explicitly declares are injected, and only if you have them set. ## Next steps **Schema reference** `env`, `files`, and `secrets` in full. **Examples** See a launch-time secret in a real template. --- # Triggers Source: https://docs.orgo.ai/guides/templates/triggers Summary: Reactive automation inside a template: a source fires, actions run. Triggers turn a computer into a reactive system. Each trigger pairs a **source** that produces events with one or more **actions** that run in response, plus optional **dedup** to suppress noise. They run continuously inside the computer, managed for you. ```yaml triggers: - name: heartbeat title: "Minute heartbeat" source: type: cron schedule: "* * * * *" actions: - type: command run: "echo beat >> /var/lib/orgo/heartbeat.log" ``` Each trigger requires a unique `name`, a `source`, and at least one entry in `actions`. `title` and `description` are labels for the dashboard, and `enabled` defaults to `true` when omitted. A template may declare up to 256 triggers. ## Sources A source's `type` selects which fields apply. | Type | Fires when | Key fields | |---|---|---| | `cron` | A schedule elapses | `schedule` (5-field cron) | | `file` | A file at or under `path` is created, modified, or deleted | `path`, `events: [create, modify, delete]` (all three when omitted) | | `http` | Polling a URL returns `expect_code`, or the request fails | `url`, `method` (default `GET`), `expect_code` (default `200`), `interval` (default `30s`) | | `process` | A process with this exact name starts or stops | `process` | | `metric` | A system metric stays past a threshold for `duration` | `metric`, `op`, `value`, `duration` (default `30s`) | | `log` | A line of the service's log matches a regular expression | `service`, `match`, `parse: text\|json` | | `event` | A custom event is emitted in the computer | `event` | | `desktop` | A desktop event occurs | `event`: `window_focus`, `window_open`, `window_close`, or `clipboard` | A `cron` schedule has no seconds field, and when it restricts both day-of-month and day-of-week, both must match. An `http` source fires on every poll that returns `expect_code`, so with the defaults a healthy URL fires every 30 seconds. A `metric` source fires again every `duration` while the condition holds. To emit a custom event, append a JSON line with a `name` field to `/var/lib/orgo/events.jsonl`, for example `{"name": "deploy.done"}`. A trigger whose `event` matches that name fires. The line that creates the file is not read, so create the file first, for example with `touch /var/lib/orgo/events.jsonl` in [`build.run`](https://docs.orgo.ai/guides/templates/schema#build). Metric sources cover `cpu_percent`, `ram_percent`, `disk_percent`, `net_in_mbps`, and `net_out_mbps`, compared with `>`, `>=`, `<`, `<=`, `==`, or `!=`. **Warning:** Validation also accepts the metrics `gpu_percent`, `gpu_memory_percent`, and `gpu_temp_celsius`, and the desktop events `screen_change`, `file_change`, `audio_stream_start`, `audio_stream_stop`, `process_start`, `process_stop`, `idle`, and `active`. A trigger on any of them never fires. ```yaml # Restart a service if memory stays high for two minutes triggers: - name: mem-guard source: type: metric metric: ram_percent op: ">" value: 90 duration: 2m actions: - type: service op: restart target: my-app-server ``` ## Actions A trigger runs one or more actions in order. The `type` selects the fields. | Type | Does | Key fields | |---|---|---| | `webhook` | HTTP request out. Sets no `Content-Type` unless you add one in `headers` | `url`, `method` (default `POST`), `headers`, `body` | | `command` | Run a shell command as root | `run`, `cwd`, `timeout` (default `5m`) | | `service` | Control a service | `op: start\|stop\|restart`, `target` (a service declared under [`apps`](https://docs.orgo.ai/guides/templates/schema#apps)) | | `notify` | Write a notification line to the trigger log. No desktop notification is shown | `title`, `body`, `level: info\|warning\|critical` | | `log` | Write to the trigger log, `/var/log/orgo/triggers.log` | `message` | | `api` | Call the computer's Desktop API | `endpoint` (`METHOD /path`; the method defaults to `POST`) | **Warning:** The `api` action does not send the Desktop API's token, so a call to any route that requires authentication fails with `401`. String fields such as `url`, `body`, `headers`, `run`, `title`, `message`, and `endpoint` expand four placeholders: `{{trigger}}` (the trigger name), `{{source}}` (the source type), `{{time}}` (RFC 3339), and `{{data.KEY}}` (a field of the event, such as `{{data.path}}` for a file source). Write them without inner spaces; `{{ data.path }}` is left as is. ```yaml # Post to Slack when a file lands in the inbox triggers: - name: new-upload source: type: file path: /data/inbox events: [create] actions: - type: webhook url: https://hooks.slack.com/services/... method: POST headers: Content-Type: application/json body: '{"text": "new file: {{data.path}}"}' ``` ## Dedup Bursty sources can fire constantly. Add at most one dedup strategy per trigger. | Strategy | Effect | |---|---| | `debounce` | Drop events that arrive within this window of the last fire. It does not wait for a burst to settle, so it behaves like `cooldown`. | | `cooldown` | Enforce a minimum gap between fires. | | `rate_limit` | Cap fires within a sliding window, e.g. `10/1m`. Events over the cap are dropped. | Independent of `dedup`, a trigger fires at most 120 times a minute. ```yaml triggers: - name: config-reload source: type: file path: /etc/my-app/config.yaml events: [modify] dedup: debounce: 2s actions: - type: service op: restart target: my-app-server ``` ## Triggers vs. health checks They complement each other: - A [health check](https://docs.orgo.ai/guides/templates/schema#apps) is a per-app **watchdog**. It probes one URL, port, command, or process and can restart a service when the probe keeps failing (`on_fail: restart_service:…`). - A trigger is **general automation**: any source to any action, across the whole computer. Reach for a health check to keep a service alive; reach for a trigger to react to files, schedules, metrics, or desktop activity. ## Next steps **Schema reference** Every source and action field. **Examples** Triggers in real templates. --- # Examples Source: https://docs.orgo.ai/guides/templates/examples Summary: Complete, runnable templates you can copy, publish, and build. Each template below is a full `orgo.ai/v1` document. Copy it, [publish and build](https://docs.orgo.ai/guides/templates/quickstart#author-your-own) it, then launch a computer from its ref. The first four use the short [sugar form](https://docs.orgo.ai/guides/templates/schema#sugar-form). The last uses the canonical form. **Note:** Publishing and building templates requires a [Scale plan](https://www.orgo.ai/#pricing). Launching one, curated or your own, requires a paid plan and counts against the workspace owner's computer quota. ## Minimal The smallest valid template, a blank Linux computer. Everything except `api_version` and the name/version is optional. ```yaml my-template.yaml api_version: orgo.ai/v1 name: my-template@1.0.0 cpu: 2 ram: 4gb display: 1280x800 ``` ## Static site (nginx) A single HTTP service on port 8000, served by nginx, with Chrome set to auto-open the page when the desktop starts. The headline is driven by a compile-time [variable](https://docs.orgo.ai/guides/templates/schema#vars-and-interpolation), so changing it is a one-line edit. ```yaml static-site.yaml api_version: orgo.ai/v1 name: static-site@1.0.0 cpu: 2 ram: 4gb display: 1280x800 vars: headline: "Hello from Orgo" files: /opt/site/index.html: | ${var.headline}

${var.headline}

Edit the template and rebuild.

/etc/nginx/sites-available/orgo: mode: "0644" inline: | server { listen 8000 default_server; root /opt/site; index index.html; location / { try_files $uri $uri/ =404; } } apps: - name: nginx title: "nginx" install: | apt-get update -qq apt-get install -y --no-install-recommends nginx ln -sf /etc/nginx/sites-available/orgo /etc/nginx/sites-enabled/orgo rm -f /etc/nginx/sites-enabled/default services: - name: nginx run: "nginx -g 'daemon off;'" user: root restart: always health: type: http url: http://127.0.0.1:8000/ every: 15s retries: 3 on_fail: restart_service:nginx ports: - { internal: 8000, public: true } # A second "app" with no install or service, only an autostart entry # that opens Chrome on the page once the desktop is up. The command # opens a local port, so Orgo waits for that port to answer, then waits # the 4s delay, before it launches Chrome. - name: chrome title: "Chrome" autostart: - run: "google-chrome-stable --no-sandbox --disable-gpu --no-first-run --no-default-browser-check http://127.0.0.1:8000" delay: 4 ``` ## Browser desktop Chrome launches automatically on boot. Chrome is already in the base image, so there's no install step, only an [`autostart`](https://docs.orgo.ai/guides/templates/schema#apps) entry. ```yaml browser-desktop.yaml api_version: orgo.ai/v1 name: browser-desktop@1.0.0 cpu: 2 ram: 4gb display: 1280x800 apps: - name: chrome title: "Chrome" autostart: - run: "google-chrome-stable --no-sandbox --disable-gpu --no-first-run --no-default-browser-check https://orgo.ai" delay: 2 ``` ## Background worker with triggers A long-running [service](https://docs.orgo.ai/guides/templates/schema#apps) that logs a tick every 10 seconds, kept alive by a process [health check](https://docs.orgo.ai/guides/templates/schema#apps), plus two [triggers](https://docs.orgo.ai/guides/templates/triggers): a cron heartbeat and a CPU watchdog that writes a `notify` line to the trigger log (`/var/log/orgo/triggers.log`) on sustained load. ```yaml heartbeat-worker.yaml api_version: orgo.ai/v1 name: heartbeat-worker@1.0.0 cpu: 2 ram: 4gb display: 1280x800 vars: worker_name: "heartbeat" env: WORKER_NAME: "${var.worker_name}" files: /opt/worker/run.sh: mode: "0755" inline: | #!/bin/bash set -e while true; do echo "[$(date -Iseconds)] ${WORKER_NAME} tick" sleep 10 done apps: - name: worker title: "Heartbeat worker" services: - name: heartbeat-worker run: "/opt/worker/run.sh" restart: always health: type: process process: run.sh every: 30s retries: 3 on_fail: restart_service:heartbeat-worker triggers: - name: minute-mark source: { type: cron, schedule: "* * * * *" } actions: - type: log message: "minute mark" - name: cpu-watchdog source: type: metric metric: cpu_percent op: ">" value: 80 duration: 30s dedup: { cooldown: 2m } actions: - type: notify title: "CPU hot" level: warning ``` ## Agent with a user-supplied key The canonical form, with a [secret](https://docs.orgo.ai/guides/templates/secrets) the launching user supplies from their vault. The CLI is installed at build time, and the `claude-code` [terminal](https://docs.orgo.ai/guides/templates/schema#terminal) session runs it. When the launching user has `ANTHROPIC_API_KEY` set, the launch cold-boots with `/root/.env` already written, so `claude` starts with their key. This is the pattern the curated `system/coding` template uses. ```yaml claude-code.yaml api_version: orgo.ai/v1 template: name: claude-code version: 1.0.0 description: Claude Code CLI, ready in the terminal. hardware: cpu: 2 ram_gb: 4 resolution: 1280x720x24 secrets: - name: anthropic_api_key description: Anthropic API key, used by Claude Code when set. optional: true apps: - name: claude-code title: Claude Code install: | curl -fsSL https://deb.nodesource.com/setup_22.x | bash - apt-get install -y nodejs npm install -g @anthropic-ai/claude-code terminal: - name: claude-code cwd: /root run: claude ``` ## Publish, build, launch The flow is the same for every template on this page: ```bash # Publish + build the golden snapshot in one call curl -X POST "https://www.orgo.ai/api/templates?auto_build=true" \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/yaml" \ --data-binary @static-site.yaml # Poll until the build is ready curl https://www.orgo.ai/api/templates/default/static-site/1.0.0/build \ -H "Authorization: Bearer $ORGO_API_KEY" # Launch a computer from it curl -X POST https://www.orgo.ai/api/computers \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"workspace_id\": \"$WORKSPACE_ID\", \"name\": \"site-1\", \"template_ref\": \"default/static-site@1.0.0\"}" ``` **Quickstart** The full walkthrough, end to end. **Schema reference** Every field explained. **Secrets** The full secret-injection model. **Publish API** Ship a template over HTTP. --- # Claude Computer Use Source: https://docs.orgo.ai/guides/claude-computer-use Summary: Let Claude control an Orgo computer. Wire Anthropic's computer use toolset to an Orgo computer. Claude sees a screenshot, asks for a click or a keystroke, and your loop runs it on the computer. ## Coordinate space Orgo computers boot at `1280x720x24`. The current toolset, `computer_toolset_20260801`, takes no display size. Anthropic's documentation states the rule directly: coordinates are always in the pixel space of the screenshots you return. Send an Orgo screenshot unresized and the coordinates Claude returns are already the computer's pixels. Pass them straight to `left_click`. A 1280x720 screenshot is inside Claude's image size limit, so there is no reason to shrink one. If you resize anyway, keep the scale factor and divide the coordinates back before you click: ```python # Only needed if you resized the screenshot before sending it. def to_computer_pixels(x, y, scale): return round(x / scale), round(y / scale) ``` **Warning:** Send a downscaled screenshot and click the raw coordinates and every click lands short of where Claude aimed. Nothing errors. The agent looks like it is confidently clicking the wrong thing. The older tool versions work differently. `computer_20251124` and `computer_20250124` still take `display_width_px` and `display_height_px`, and those must match the size of the screenshots you send. On a default Orgo computer that is `1280` and `720`. If you create a computer at a non-default resolution, or resize its screen later, use that resolution instead. `GET /computers/{id}/screens` reports the current `width` and `height`. ## Setup Install the required packages: ```bash pip pip install orgo anthropic ``` ```bash npm npm install orgo@1 @anthropic-ai/sdk ``` ```bash yarn yarn add orgo@1 @anthropic-ai/sdk ``` ```bash pnpm pnpm add orgo@1 @anthropic-ai/sdk ``` **Note:** The TypeScript SDK ships in version 1 of the `orgo` npm package. From version 2, `orgo` on npm is the [CLI](https://docs.orgo.ai/guides/cli) and exports no library, so pin `orgo@1` to `import { Computer } from 'orgo'`. Set up your API keys: ```bash terminal icon="terminal" # Export as environment variables export ORGO_API_KEY=your_orgo_api_key export ANTHROPIC_API_KEY=your_anthropic_api_key ``` ```python setup.py icon="python" import os os.environ["ORGO_API_KEY"] = "your_orgo_api_key" os.environ["ANTHROPIC_API_KEY"] = "your_anthropic_api_key" ``` ```typescript setup.ts icon="square-js" process.env.ORGO_API_KEY = "your_orgo_api_key"; process.env.ANTHROPIC_API_KEY = "your_anthropic_api_key"; ``` ## Let Orgo run the loop To skip writing the loop, call the [Computer Use API](https://docs.orgo.ai/api-reference/chat/completions). `POST /v1/chat/completions` runs the agent on Orgo's side against the computer you name in `computer_id`, then returns the final assistant message in OpenAI's chat completions format. ## Run the agent loop yourself For full control, drive the Anthropic API yourself. Three things about the toolset shape the loop: - One entry in `tools` declares every member. The member name arrives in the `tool_use` block's `name`, and there is no `action` field in `input`. - Claude can return several member calls in one turn. Run them in order and stop at the first failure. - Return one `tool_result` per `tool_use` block, in a single user message. Member results echo `toolset_name`. **Warning:** The examples pass `ram` and `cpu`, and take screenshots and scroll over HTTP, so they also run on older SDKs. SDK versions before 0.0.47 (Python) and 1.0.22 (TypeScript) default to 2 GB, which `POST /computers` rejects with `400`. Their `screenshot()` and `screenshot_base64()` cannot read the stored-image path the API returns by default, and their `scroll()` takes no coordinates, so the computer scrolls at the top-left corner. ```python advanced.py expandable icon="python" import os import anthropic import requests from orgo import Computer API = "https://www.orgo.ai/api" HEADERS = {"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"} TOOLS = [ {"type": "computer_toolset_20260801"}, {"type": "bash_20250124", "name": "bash"}, ] def create_agent_loop(instruction, model="claude-sonnet-5", max_iterations=20): # Pass a size: SDKs before 0.0.47 default to 2 GB, which the API rejects. computer = Computer(ram=4, cpu=1) client = anthropic.Anthropic() try: messages = [{"role": "user", "content": instruction}] for _ in range(max_iterations): response = client.messages.create( model=model, messages=messages, tools=TOOLS, max_tokens=8192, ) messages.append({"role": "assistant", "content": response.content}) # No tool calls means Claude is done. tool_results = run_tool_calls(computer, response) if not tool_results: break messages.append({"role": "user", "content": tool_results}) return messages finally: computer.destroy() def run_tool_calls(computer, response): """Run one turn's tool calls in order, stopping at the first failure.""" results = [] failed = False for block in response.content: if block.type != "tool_use": continue if block.name == "bash": results.append({ "type": "tool_result", "tool_use_id": block.id, "content": computer.bash(block.input["command"]), }) continue # Dispatch on the pair, because a custom tool may reuse a member name. if getattr(block, "toolset_name", None) != "computer": continue result = { "type": "tool_result", "tool_use_id": block.id, "toolset_name": "computer", } if failed: result["content"] = "Not executed: an earlier computer action in this turn failed." result["is_error"] = True else: try: result["content"] = run_member(computer, block.name, block.input) except Exception as error: result["content"] = f"Error: {error}" result["is_error"] = True failed = True results.append(result) return results def screenshot_base64(computer): """The screen as base64 PNG, straight from the API.""" response = requests.get( f"{API}/computers/{computer.computer_id}/screenshot", params={"response_format": "base64"}, headers=HEADERS, ) response.raise_for_status() return response.json()["image"] def scroll(computer, x, y, direction, amount): """Scroll at (x, y). Orgo scrolls up or down only.""" if direction not in ("up", "down"): raise ValueError(f"Unsupported scroll direction: {direction}") response = requests.post( f"{API}/computers/{computer.computer_id}/scroll", json={"x": x, "y": y, "direction": direction, "amount": amount}, headers=HEADERS, ) response.raise_for_status() def run_member(computer, name, params): """Map one member tool to an Orgo call. Only screenshot returns an image.""" if name == "screenshot": return [{ "type": "image", "source": { "type": "base64", "media_type": "image/png", "data": screenshot_base64(computer), }, }] if name == "left_click": computer.left_click(*params["coordinate"]) elif name == "right_click": computer.right_click(*params["coordinate"]) elif name == "double_click": computer.double_click(*params["coordinate"]) elif name == "type": computer.type(params["text"]) elif name == "key": computer.key(params["text"]) elif name == "scroll": scroll(computer, *params["coordinate"], params["scroll_direction"], params["scroll_amount"]) elif name == "wait": computer.wait(params["duration"]) else: raise ValueError(f"Unsupported member: {name}") return "OK" ``` ```typescript advanced.ts expandable icon="square-js" import { Computer } from 'orgo'; import Anthropic from '@anthropic-ai/sdk'; const API = "https://www.orgo.ai/api"; const HEADERS = { Authorization: `Bearer ${process.env.ORGO_API_KEY}` }; const TOOLS: any[] = [ { type: "computer_toolset_20260801" }, { type: "bash_20250124", name: "bash" }, ]; async function createAgentLoop( instruction: string, model = "claude-sonnet-5", maxIterations = 20, ) { // Pass a size: SDKs before 1.0.22 default to 2 GB, which the API rejects. const computer = await Computer.create({ ram: 4, cpu: 1 }); const client = new Anthropic(); try { const messages: any[] = [{ role: "user", content: instruction }]; for (let i = 0; i < maxIterations; i++) { const response = await client.messages.create({ model, messages, tools: TOOLS, max_tokens: 8192, }); messages.push({ role: "assistant", content: response.content }); // No tool calls means Claude is done. const toolResults = await runToolCalls(computer, response); if (toolResults.length === 0) break; messages.push({ role: "user", content: toolResults }); } return messages; } finally { await computer.destroy(); } } async function runToolCalls(computer: Computer, response: any) { const results: any[] = []; let failed = false; for (const block of response.content) { if (block.type !== "tool_use") continue; if (block.name === "bash") { results.push({ type: "tool_result", tool_use_id: block.id, content: await computer.bash(block.input.command), }); continue; } // Dispatch on the pair, because a custom tool may reuse a member name. if (block.toolset_name !== "computer") continue; const result: any = { type: "tool_result", tool_use_id: block.id, toolset_name: "computer", }; if (failed) { result.content = "Not executed: an earlier computer action in this turn failed."; result.is_error = true; } else { try { result.content = await runMember(computer, block.name, block.input); } catch (error) { result.content = `Error: ${error}`; result.is_error = true; failed = true; } } results.push(result); } return results; } // The screen as base64 PNG, straight from the API. async function screenshotBase64(computer: Computer): Promise { const response = await fetch( `${API}/computers/${computer.computerId}/screenshot?response_format=base64`, { headers: HEADERS }, ); if (!response.ok) throw new Error(`Screenshot failed: ${response.status}`); return (await response.json()).image; } // Scroll at (x, y). Orgo scrolls up or down only. async function scroll(computer: Computer, x: number, y: number, direction: string, amount: number) { if (direction !== "up" && direction !== "down") { throw new Error(`Unsupported scroll direction: ${direction}`); } const response = await fetch(`${API}/computers/${computer.computerId}/scroll`, { method: "POST", headers: { ...HEADERS, "Content-Type": "application/json" }, body: JSON.stringify({ x, y, direction, amount }), }); if (!response.ok) throw new Error(`Scroll failed: ${response.status}`); } async function runMember(computer: Computer, name: string, params: any) { if (name === "screenshot") { return [{ type: "image", source: { type: "base64", media_type: "image/png", data: await screenshotBase64(computer), }, }]; } const [x, y] = params.coordinate ?? []; switch (name) { case "left_click": await computer.leftClick(x, y); break; case "right_click": await computer.rightClick(x, y); break; case "double_click": await computer.doubleClick(x, y); break; case "type": await computer.type(params.text); break; case "key": await computer.key(params.text); break; case "scroll": await scroll(computer, x, y, params.scroll_direction, params.scroll_amount); break; case "wait": await computer.wait(params.duration); break; default: throw new Error(`Unsupported member: ${name}`); } return "OK"; } ``` **Note:** The toolset has 17 members, and the loop above maps 8 of them. The rest come back as an error `tool_result`, which Claude recovers from but wastes a turn on. Disable them with `configs` so Claude never sees them: ```python { "type": "computer_toolset_20260801", "configs": { name: {"enabled": False} for name in [ "zoom", "middle_click", "triple_click", "left_click_drag", "mouse_move", "left_mouse_down", "left_mouse_up", "cursor_position", "hold_key", ] }, } ``` A `configs` that disables every member is rejected, so drop the whole entry instead of emptying it. ## Using Claude's thinking capability Claude can stream its reasoning through the `thinking` parameter: ```python thinking.py icon="python" import anthropic from orgo import Computer # Initialize components computer = Computer(ram=4, cpu=1) client = anthropic.Anthropic() try: response = client.messages.create( model="claude-sonnet-5", max_tokens=8192, messages=[{"role": "user", "content": "Find an image of a cat on the web"}], tools=[ {"type": "computer_toolset_20260801"}, {"type": "bash_20250124", "name": "bash"}, ], thinking={"type": "adaptive", "display": "summarized"}, ) # Access the thinking content for block in response.content: if block.type == "thinking": print("Claude's reasoning:") print(block.thinking) finally: # Clean up computer.destroy() ``` ```typescript thinking.ts icon="square-js" import { Computer } from 'orgo'; import Anthropic from '@anthropic-ai/sdk'; // Initialize components const computer = await Computer.create({ ram: 4, cpu: 1 }); const client = new Anthropic(); try { const response = await client.messages.create({ model: "claude-sonnet-5", max_tokens: 8192, messages: [{ role: "user", content: "Find an image of a cat on the web" }], tools: [ { type: "computer_toolset_20260801" }, { type: "bash_20250124", name: "bash" }, ] as any, thinking: { type: "adaptive", display: "summarized" }, }); // Access the thinking content for (const block of response.content) { if (block.type === "thinking") { console.log("Claude's reasoning:"); console.log((block as any).thinking); } } } finally { // Clean up await computer.destroy(); } ``` **Note:** Set `display: "summarized"` to read the reasoning. The current models default to `"omitted"`, which streams `thinking` blocks with empty text. ## Tool compatibility Each member the loop above handles maps to one Orgo call: | Toolset member | Orgo call (Python) | Orgo call (TypeScript) | Description | |-------------------|---------------------|-------------------------|-------------| | `screenshot` | `GET /computers/{id}/screenshot?response_format=base64` | `GET /computers/{id}/screenshot?response_format=base64` | Capture the screen (returns base64 PNG) | | `left_click` | `computer.left_click(x, y)` | `await computer.leftClick(x, y)` | Left click at coordinates | | `right_click` | `computer.right_click(x, y)` | `await computer.rightClick(x, y)` | Right click at coordinates | | `double_click` | `computer.double_click(x, y)` | `await computer.doubleClick(x, y)` | Double click at coordinates | | `type` | `computer.type(text)` | `await computer.type(text)` | Type text | | `key` | `computer.key(key_sequence)` | `await computer.key(keySequence)` | Press keys (e.g. "Return", "ctrl+c") | | `scroll` | `POST /computers/{id}/scroll` with `x`, `y`, `direction`, `amount` | `POST /computers/{id}/scroll` with `x`, `y`, `direction`, `amount` | Scroll up or down at coordinates | | `wait` | `computer.wait(seconds)` | `await computer.wait(seconds)` | Wait for a number of seconds | Only `screenshot` and `zoom` return images. Every other member returns text, so `"OK"` is a valid result. ## Picking a model | Model | When to use | |---|---| | `claude-opus-5-5` | Hardest, multi-step desktop tasks where accuracy and judgment matter most. | | `claude-sonnet-5-5` | The default for most computer-use agents: fast, capable, and cheaper than Opus. | | `claude-opus-5`, `claude-sonnet-5`, `claude-opus-4-8` | Previous generations, still supported on the current toolset. | `claude-fable-5` and `claude-fable-5-1` also support the toolset. `claude-haiku-4-5` supports only the older `computer_20250124` tool. **Warning:** `claude-opus-5-5` and `claude-sonnet-5-5` accept only `computer_toolset_20260801` on the Claude API. Sending them `computer_20251124` returns a 400. ## Tool versions Match the tool `type` to the model you are calling: | Tool `type` | Models | Beta header | |---|---|---| | `computer_toolset_20260801` | Opus 5.5, Sonnet 5.5, Opus 5, Sonnet 5, Opus 4.8, Fable 5, Fable 5.1 | None | | `computer_20251124` | Opus 5, Sonnet 5, Opus 4.7, Opus 4.6, Sonnet 4.6, Opus 4.5 | `computer-use-2025-11-24` | | `computer_20250124` | Haiku 4.5, Sonnet 4.5, and earlier Claude models | `computer-use-2025-01-24` | The two dated tool versions remain available for existing integrations. Use the toolset for anything new: it needs no beta header, it batches several actions into one turn, and it drops the display size fields that were the most common source of misplaced clicks. The `bash_20250124` tool needs no beta header on any of them. **Note:** TypeScript users: All methods are async and must be awaited. The TypeScript SDK uses camelCase for method names (e.g. `leftClick` instead of `left_click`). ## Video tutorial The video covers setup. The tool shapes above are the current ones. --- # OpenAI Computer Use Source: https://docs.orgo.ai/guides/openai-computer-use Summary: Control an Orgo computer with GPT. OpenAI's computer tool drives interfaces through the Responses API. This guide wires it to an Orgo computer. **Info:** The `computer` tool is the generally available integration and runs on the GPT-5.5 and GPT-5.6 families. The older `computer_use_preview` tool and its `computer-use-preview` model still work, and are covered at the end of this page. Use the GA tool for anything new. ## Coordinate space Orgo computers boot at `1280x720x24`. The `computer` tool takes no display size. The model reads the screen dimensions from the screenshots you send, and returns `x` and `y` in that same pixel space. Send an Orgo screenshot unresized and the coordinates come back as the computer's pixels. Pass them straight to `left_click`. Send `"detail": "original"` with each screenshot so the API keeps the resolution the model is measuring against. **Warning:** Downscale a screenshot and click the raw coordinates and every click lands short of where the model aimed. Nothing errors. The agent looks like it is confidently clicking the wrong thing. If you must resize, keep the scale factor and map each coordinate back to the original resolution. `GET /computers/{id}/screens` reports the current `width` and `height` if you created the computer at another resolution or resized its screen later. ## Quick start **Warning:** The examples pass `ram` and `cpu`, and take screenshots and scroll over HTTP, so they also run on older SDKs. SDK versions before 0.0.47 (Python) and 1.0.22 (TypeScript) default to 2 GB, which `POST /computers` rejects with `400`. Their `screenshot()` and `screenshot_base64()` cannot read the stored-image path the API returns by default, and their `scroll()` takes no coordinates, so the computer scrolls at the top-left corner. **Install packages** ```bash pip install orgo openai python-dotenv ``` **Set up API keys** ```bash export ORGO_API_KEY=your_orgo_api_key export OPENAI_API_KEY=your_openai_api_key ``` **Run your first task** ```python from openai import OpenAI from orgo import Computer # Initialize client = OpenAI() # Pass a size: SDKs before 0.0.47 default to 2 GB, which the API rejects. computer = Computer(ram=4, cpu=1) # Create request with task response = client.responses.create( model="gpt-5.6", tools=[{"type": "computer"}], input="Open Chrome and search for OpenAI", ) # Execute the suggested actions, in order call = next( (item for item in response.output if item.type == "computer_call"), None, ) if call: for action in call.actions: if action.type == "click": computer.left_click(action.x, action.y) elif action.type == "type": computer.type(action.text) # Clean up computer.destroy() ``` ## Complete example A full agent loop. The model returns a batch of actions per turn, so run every entry in `actions[]` in order before you screenshot. ```python example.py expandable icon="python" import os import time import requests from openai import OpenAI from orgo import Computer from dotenv import load_dotenv load_dotenv() API = "https://www.orgo.ai/api" HEADERS = {"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"} MODEL = "gpt-5.6" TOOLS = [{"type": "computer"}] INSTRUCTIONS = """You are controlling a desktop computer. - Always double-click desktop icons to open applications. - Use keyboard shortcuts as single commands (e.g. 'ctrl+c', not separate keys).""" def run_computer_task(task, computer_id=None, max_turns=20): """Execute a task using the OpenAI computer tool with Orgo.""" client = OpenAI() # Pass a size for a new computer: SDKs before 0.0.47 default to 2 GB, which the API rejects. computer = Computer(computer_id=computer_id, ram=4, cpu=1) print(f"Computer ID: {computer.computer_id}") response = client.responses.create( model=MODEL, tools=TOOLS, instructions=INSTRUCTIONS, input=task, reasoning={"summary": "concise"}, ) for _ in range(max_turns): # Display progress for item in response.output: if item.type == "reasoning": for summary in getattr(item, "summary", []): print(f"[thinking] {getattr(summary, 'text', '')}") call = next( (item for item in response.output if item.type == "computer_call"), None, ) # No computer call means the task is done. if call is None: print("Task completed") break # Run the whole batch, in order. for action in call.actions: print(f"-> {action.type}") execute_action(computer, action) time.sleep(1) # Allow the UI to settle screenshot = screenshot_base64(computer) response = client.responses.create( model=MODEL, tools=TOOLS, previous_response_id=response.id, input=[{ "type": "computer_call_output", "call_id": call.call_id, "output": { "type": "computer_screenshot", "image_url": f"data:image/png;base64,{screenshot}", "detail": "original", }, }], reasoning={"summary": "concise"}, ) return computer def screenshot_base64(computer): """The screen as base64 PNG, straight from the API.""" response = requests.get( f"{API}/computers/{computer.computer_id}/screenshot", params={"response_format": "base64"}, headers=HEADERS, ) response.raise_for_status() return response.json()["image"] def scroll(computer, x, y, direction, amount): """Scroll at (x, y). Orgo scrolls up or down only.""" if direction not in ("up", "down"): raise ValueError(f"Unsupported scroll direction: {direction}") response = requests.post( f"{API}/computers/{computer.computer_id}/scroll", json={"x": x, "y": y, "direction": direction, "amount": amount}, headers=HEADERS, ) response.raise_for_status() # Orgo presses keys with xdotool, which expects X keysym names. XDOTOOL_KEYS = { "enter": "Return", "return": "Return", "esc": "Escape", "escape": "Escape", "backspace": "BackSpace", "tab": "Tab", "space": "space", "delete": "Delete", "arrowup": "Up", "arrowdown": "Down", "arrowleft": "Left", "arrowright": "Right", "pageup": "Page_Up", "pagedown": "Page_Down", "home": "Home", "end": "End", } def to_xdotool(key): return XDOTOOL_KEYS.get(key.lower(), key.lower()) def execute_action(computer, action): """Execute one computer action using Orgo.""" match action.type: case "click": if getattr(action, "button", "left") == "right": computer.right_click(action.x, action.y) else: computer.left_click(action.x, action.y) case "double_click": computer.double_click(action.x, action.y) case "type": computer.type(action.text) case "keypress": keys = getattr(action, "keys", []) if keys: # One combo, e.g. ["CTRL", "C"] becomes "ctrl+c" computer.key("+".join(to_xdotool(k) for k in keys)) case "scroll": scroll_y = getattr(action, "scroll_y", 0) direction = "down" if scroll_y > 0 else "up" scroll(computer, action.x, action.y, direction, max(1, abs(scroll_y) // 100)) case "wait": computer.wait(2) case "screenshot": pass # The loop screenshots after every batch if __name__ == "__main__": computer = run_computer_task("Open a terminal and list files") # Always clean up computer.destroy() ``` ## Usage examples ### Basic tasks ```python # Open a browser computer = run_computer_task("Open Chrome") # Navigate to a website computer = run_computer_task("Go to github.com and search for orgo") # Fill out a form computer = run_computer_task("Fill out the contact form with test data") # Always clean up computer.destroy() ``` ### Complex workflows ```python # Multi-step task task = """ 1. Open a text editor 2. Write a Python hello world program 3. Save it as hello.py 4. Open a terminal 5. Run the program """ computer = run_computer_task(task) computer.destroy() ``` ### Reusing sessions ```python # First task computer = run_computer_task("Open VS Code") computer_id = computer.computer_id # Continue in same session computer = run_computer_task( "Create a new Python file", computer_id=computer_id ) # Clean up when done computer.destroy() ``` ## Key concepts ### The agent loop 1. **Request** Send the task to the model. 2. **Actions** The model returns a `computer_call` holding a batch of actions. 3. **Execute** Your code runs every action in the batch, in order. 4. **Screenshot** Capture the result and return it as a `computer_call_output`. 5. **Repeat** Continue until a response has no `computer_call`. ### Action types | Action | Description | Example | |--------|-------------|---------| | `click` | Click at coordinates | Click button at (100, 200) | | `double_click` | Double-click | Open desktop icon | | `move` | Move the cursor | Hover a menu | | `drag` | Drag between points | Move a file | | `type` | Type text | Enter username | | `keypress` | Press key(s) | Press Enter, Ctrl+C | | `scroll` | Scroll page | Scroll down 3 units | | `wait` | Pause execution | Wait 2 seconds | | `screenshot` | Take screenshot | Capture current state | ### Safety The model acts on whatever the screen shows, and page content is untrusted input. OpenAI's guidance is to isolate the environment, restrict which sites the agent can reach, and keep a human in the loop for consequential steps. An Orgo computer covers the isolation half: the blast radius is that one computer, and `computer.destroy()` ends it. ## Best practices ### 1. Clear instructions ```python # Good: specific and clear task = "Open Chrome, go to github.com, and star the orgo repository" # Avoid: too vague task = "Do some web stuff" ``` ### 2. Error handling ```python def safe_run_task(task): """Run task with error handling.""" computer = None try: computer = run_computer_task(task) return computer except Exception as e: print(f"Error: {e}") if computer: computer.destroy() raise ``` ### 3. Session management ```python # Use context manager pattern class ComputerSession: def __init__(self, task): self.task = task self.computer = None def __enter__(self): self.computer = run_computer_task(self.task) return self.computer def __exit__(self, *args): if self.computer: self.computer.destroy() # Usage with ComputerSession("Open calculator") as computer: print(f"Session ID: {computer.computer_id}") ``` ### 4. Timing considerations ```python # Add delays for UI updates time.sleep(1) # After clicks time.sleep(2) # After opening applications time.sleep(0.5) # After typing ``` ## Migrating from the preview tool The preview integration still runs. These are the differences: | | Preview | GA | |---|---|---| | Model | `computer-use-preview` | `gpt-5.6` | | Tool type | `computer_use_preview` | `computer` | | Display size | `display_width`, `display_height` | Removed, inferred from screenshots | | Environment | `environment: "linux"` | Removed | | Actions | One `action` per call | Batched `actions[]` array | | Truncation | `truncation: "auto"` required | Not needed | The preview tool declares its own display size, so on a default Orgo computer set `display_width: 1280` and `display_height: 720` to match the screenshots you send. ## Comparison with Claude | Feature | OpenAI computer tool | Claude computer use | |---------|-------------------|-------------------| | API | Responses API | Messages API | | Model | `gpt-5.6` | `claude-sonnet-5-5` | | Tool | `computer` | `computer_toolset_20260801` | | Beta header | None | None | | Reasoning | Optional summaries | Thinking blocks | | Coordinates | Screenshot pixels | Screenshot pixels | | Batched actions | Yes | Yes | ## Next steps **OpenAI Docs** Official OpenAI computer use documentation **Orgo Quickstart** Learn more about Orgo computers --- # Gemini Computer Use Source: https://docs.orgo.ai/guides/gemini-computer-use Summary: Control an Orgo computer with Gemini. Wire Google's computer use tool to an Orgo computer. Gemini sees a screenshot, returns a batch of actions, and your loop runs them on the computer. Computer use is a built-in tool on Gemini 3.x rather than a separate model. `gemini-3.7-flash` is the model Google recommends for it, through the Interactions API. **Info:** `gemini-2.5-computer-use-preview-10-2025` is now marked legacy. It used the older `generate_content` call and the `types.ComputerUse` tool object. The code below uses the current Interactions API. ## Coordinate space Orgo computers boot at `1280x720x24`. Gemini does not work in pixels. It returns coordinates on a normalized 0-999 grid, scaled to the screenshot you sent. You denormalize every coordinate against the computer's real resolution before you can click with it: ```python SCREEN_WIDTH = 1280 SCREEN_HEIGHT = 720 def denormalize_x(x: int) -> int: return int(x / 1000 * SCREEN_WIDTH) def denormalize_y(y: int) -> int: return int(y / 1000 * SCREEN_HEIGHT) ``` **Warning:** These two constants are load-bearing. They are the only thing translating Gemini's grid into pixels. Set them to `1024x768` against a `1280x720` computer and every horizontal click lands roughly 25% short of where Gemini aimed. Nothing errors. The agent looks like it is confidently clicking the wrong thing. There is no display size to declare on the request. If you create a computer at a non-default resolution, or resize its screen later, set the constants to that resolution instead. `GET /computers/{id}/screens` reports the current `width` and `height`. ## Setup Install the required packages. Gemini 3.x computer use needs `google-genai` 2.7.0 or later: ```bash pip pip install "orgo" "google-genai>=2.7.0" pillow python-dotenv ``` Set up your API keys in a `.env` file: ```bash .env icon="file" ORGO_API_KEY=your_orgo_api_key GEMINI_API_KEY=your_gemini_api_key ``` Or export them as environment variables: ```bash terminal icon="terminal" export ORGO_API_KEY=your_orgo_api_key export GEMINI_API_KEY=your_gemini_api_key ``` ## Complete example An Orgo computer is a full desktop, so declare `environment: "desktop"`. The `browser` environment adds page-level actions such as `navigate` and `go_back`, and expects the current page URL back in every result. ```python example.py expandable icon="python" import os import io import json import time import base64 import requests from google import genai from orgo import Computer from PIL import Image from dotenv import load_dotenv # Load environment variables load_dotenv() # Initialize Gemini client client = genai.Client(api_key=os.environ.get('GEMINI_API_KEY')) # Connect to your Orgo computer # Use the computer UUID (the `id` returned when you create a computer) computer = Computer(computer_id="your-computer-id") MODEL = 'gemini-3.7-flash' TOOLS = [{ "type": "computer_use", "environment": "desktop", "enable_prompt_injection_detection": True, }] # Screen resolution SCREEN_WIDTH = 1280 SCREEN_HEIGHT = 720 # System prompt with Ubuntu-specific instructions SYSTEM_PROMPT = f"""You are controlling an Ubuntu Linux virtual machine with a display resolution of {SCREEN_WIDTH}x{SCREEN_HEIGHT}. * CRITICAL: When opening applications or files on the Ubuntu desktop, you MUST USE DOUBLE-CLICK, not single-click * Single-click only selects desktop icons but DOES NOT open them * Desktop interactions: - Desktop icons (apps/folders): DOUBLE-CLICK to open - Menu items: SINGLE-CLICK to select - Taskbar/launcher icons: SINGLE-CLICK to open - Window buttons (close/minimize/maximize): SINGLE-CLICK - File browser items: DOUBLE-CLICK to open * When you need to submit or confirm, use the 'Enter' key * Wait for applications to load before acting on them * Batch multiple actions together when possible before checking the result """ def denormalize_x(x: int) -> int: """Convert a normalized x coordinate (0-999) to an actual pixel.""" return int(x / 1000 * SCREEN_WIDTH) def denormalize_y(y: int) -> int: """Convert a normalized y coordinate (0-999) to an actual pixel.""" return int(y / 1000 * SCREEN_HEIGHT) def get_screenshot_png() -> str: """Get a base64 PNG screenshot. Gemini accepts PNG only, so re-encode to be safe.""" # Ask the API for inline base64. The SDK's screenshot methods cannot read # the stored-image path the API returns by default. response = requests.get( f"https://www.orgo.ai/api/computers/{computer.computer_id}/screenshot", params={"response_format": "base64"}, headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, ) response.raise_for_status() image_data = base64.b64decode(response.json()["image"]) image = Image.open(io.BytesIO(image_data)) png_buffer = io.BytesIO() image.save(png_buffer, format='PNG') return base64.b64encode(png_buffer.getvalue()).decode('utf-8') def execute_action(name: str, args: dict) -> dict: """Run one Gemini action on the computer. Returns extra result fields.""" if name in ("click", "double_click", "right_click"): x, y = denormalize_x(args["x"]), denormalize_y(args["y"]) { "click": computer.left_click, "double_click": computer.double_click, "right_click": computer.right_click, }[name](x, y) elif name == "type": computer.type(args["text"]) if args.get("press_enter", False): computer.key("Return") elif name == "press_key": computer.key(args["key"]) elif name == "hotkey": computer.key("+".join(args["keys"]).lower()) elif name == "scroll": # Orgo scrolls up or down in clicks, so treat the pixel magnitude as a hint. if args["direction"] not in ("up", "down"): return {"error": f"Unsupported scroll direction: {args['direction']}"} amount = max(1, args.get("magnitude_in_pixels", 300) // 100) # The SDK's scroll() takes no coordinates, so call the API with them. response = requests.post( f"https://www.orgo.ai/api/computers/{computer.computer_id}/scroll", json={ "x": denormalize_x(args["x"]), "y": denormalize_y(args["y"]), "direction": args["direction"], "amount": amount, }, headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, ) response.raise_for_status() elif name == "wait": computer.wait(args.get("seconds", 2)) elif name == "take_screenshot": pass # Every result already carries a fresh screenshot else: return {"error": f"Unsupported action: {name}"} return {} def execute_function_calls(interaction) -> list: """Run every function call in the turn, in order.""" results = [] for step in interaction.steps: if step.type != "function_call": continue print(f" -> {step.name}: {step.arguments.get('intent', '')}") try: result = execute_action(step.name, step.arguments) except Exception as error: print(f" Error: {error}") result = {"error": str(error)} time.sleep(1) # Wait for the UI to update results.append((step.name, step.call_id, result)) return results def get_function_results(results: list) -> list: """Return one result per call, each carrying the new screen.""" screenshot_png = get_screenshot_png() return [ { "type": "function_result", "name": name, "call_id": call_id, "result": [ {"type": "text", "text": json.dumps({"status": "completed", **result})}, {"type": "image", "data": screenshot_png, "mime_type": "image/png"}, ], } for name, call_id, result in results ] # Define the task task = "Open Chrome and search for 'gemini ai'" print(f"Task: {task}\n") interaction = client.interactions.create( model=MODEL, system_instruction=SYSTEM_PROMPT, input=[ {"type": "text", "text": task}, {"type": "image", "data": get_screenshot_png(), "mime_type": "image/png"}, ], tools=TOOLS, ) # Agent loop for turn in range(20): print(f"\n--- Turn {turn + 1} ---") has_function_calls = any( step.type == "function_call" for step in interaction.steps ) if not has_function_calls: text = " ".join( block.text for step in interaction.steps if step.type == "model_output" for block in step.content if block.type == "text" ) print(f"Agent finished: {text}") break results = execute_function_calls(interaction) interaction = client.interactions.create( model=MODEL, previous_interaction_id=interaction.id, input=get_function_results(results), tools=TOOLS, ) # The computer keeps running. Call computer.destroy() to tear it down. ``` ## Usage examples ### Basic tasks ```python # Change the task variable to control what Gemini does task = "Open Chrome and search for 'gemini ai'" # Navigate to a website task = "Go to github.com and search for 'orgo'" # Fill a form task = "Fill out the contact form with test data" ``` ### Complex workflows ```python # Multi-step task task = """ 1. Open a text editor 2. Write a Python hello world program 3. Save it as hello.py 4. Open a terminal 5. Run the program """ ``` ## Key concepts ### The agent loop 1. **Request** Send the task and a screenshot to the model. 2. **Actions** The model returns `function_call` steps. 3. **Execute** Your code runs them in order. 4. **Screenshot** Capture the result and return one `function_result` per call. 5. **Repeat** Chain turns with `previous_interaction_id` until no call comes back. ### Reading the response Everything the model produced for a turn is in `interaction.steps`. A step of type `function_call` carries `name`, `call_id`, and `arguments`. A step of type `model_output` carries `content` blocks, and its `text` blocks are the agent's final answer. Every action's `arguments` includes an `intent` string explaining why the model chose it. It is the cheapest debugging signal in the loop, so log it. ### System prompt Pass the Ubuntu guidance as `system_instruction`. Without it Gemini single-clicks desktop icons and nothing opens: ```python interaction = client.interactions.create( model=MODEL, system_instruction=SYSTEM_PROMPT, input=[...], tools=TOOLS, ) ``` ### Getting your computer ID `computer_id` is the computer UUID: the `id` field that [Create computer](https://docs.orgo.ai/api-reference/computers/create) returns. In the dashboard, it is the last segment of the computer page's URL, `https://www.orgo.ai/computers/{id}`. Pass it as `Computer(computer_id="your-computer-id")`. ### Image format conversion Orgo returns screenshots as PNG by default. Gemini accepts PNG only, so the helper decodes and re-encodes each screenshot as PNG. That keeps the loop working if you ask Orgo for another screenshot format: ```python def get_screenshot_png() -> str: response = requests.get( f"https://www.orgo.ai/api/computers/{computer.computer_id}/screenshot", params={"response_format": "base64"}, headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, ) response.raise_for_status() image_data = base64.b64decode(response.json()["image"]) image = Image.open(io.BytesIO(image_data)) png_buffer = io.BytesIO() image.save(png_buffer, format='PNG') return base64.b64encode(png_buffer.getvalue()).decode('utf-8') ``` ### Safety confirmations With `enable_prompt_injection_detection` on, the model can return a `safety_decision` of `require_confirmation` on an action. Prompt your user before running it, and include a `safety_acknowledgement` in that call's result when they approve. Skip the confirmation and the action does not run. ### Action arguments Coordinates are normalized 0-999. Denormalize `x`, `y`, `start_x`, `start_y`, `end_x`, and `end_y` before use. | Action | Arguments | |--------|-----------| | `click`, `double_click`, `triple_click`, `right_click`, `middle_click` | `x`, `y`, `intent` | | `move` | `x`, `y`, `intent` | | `type` | `text`, `press_enter` (optional), `intent` | | `press_key` | `key`, `intent` | | `hotkey` | `keys` (list), `intent` | | `scroll` | `x`, `y`, `direction`, `magnitude_in_pixels` (optional), `intent` | | `drag_and_drop` | `start_x`, `start_y`, `end_x`, `end_y`, `intent` | | `wait` | `seconds` (optional), `intent` | | `take_screenshot` | `intent` | The `desktop` environment also exposes `mouse_down`, `mouse_up`, `key_down`, and `key_up` for held input. ## Tool compatibility | Gemini action | Orgo call | Description | |-------------------|-------------|-------------| | `click` | `computer.left_click(x, y)` | Click at coordinates | | `double_click` | `computer.double_click(x, y)` | Double-click | | `right_click` | `computer.right_click(x, y)` | Right-click | | `type` | `computer.type(text)` | Type text | | `press_key` | `computer.key(key)` | Press one key | | `hotkey` | `computer.key(keys)` | Press a combination (e.g. "ctrl+c") | | `scroll` | `POST /computers/{id}/scroll` with `x`, `y`, `direction`, `amount` | Scroll up or down at coordinates | | `wait` | `computer.wait(seconds)` | Wait | | `take_screenshot` | `GET /computers/{id}/screenshot?response_format=base64` | Capture screen (PNG) | `drag_and_drop` maps to `computer.drag(start_x, start_y, end_x, end_y)` once you denormalize its four coordinates. The loop above does not handle it. The loop takes screenshots and scrolls over HTTP rather than through the SDK. The SDK's `screenshot()` and `screenshot_base64()` cannot read the stored-image path the API returns by default, and its `scroll()` takes no coordinates, so the computer scrolls at the top-left corner. ## Best practices ### 1. Clear instructions ```python # Good: specific and clear task = "Go to amazon.com and find the top 3 rated laptops under $1000" # Avoid: too vague task = "Find some laptops" ``` ### 2. Send a system instruction, not a user turn ```python interaction = client.interactions.create( model=MODEL, system_instruction=SYSTEM_PROMPT, # OS-specific guidance input=[...], tools=TOOLS, ) ``` ### 3. Convert coordinates ```python actual_x = denormalize_x(args["x"]) actual_y = denormalize_y(args["y"]) ``` ### 4. Handle image format ```python screenshot_png = get_screenshot_png() ``` ### 5. Return one result per call Every `function_call` needs a matching `function_result` with the same `call_id`. Attach the new screenshot to each of them. ### 6. Add delays ```python time.sleep(1) # Wait for UI to update after actions ``` ## Comparison with Claude and OpenAI | Feature | Gemini computer use | Claude computer use | OpenAI computer use | |---------|-------------------|-------------------|-------------------| | API | Interactions API | Messages API | Responses API | | Model | `gemini-3.7-flash` | `claude-sonnet-5-5` | `gpt-5.6` | | Instructions | Supported | Supported | Supported | | Coordinates | Normalized 0-999 | Screenshot pixels | Screenshot pixels | | Image format | PNG required | JPEG or PNG | JPEG or PNG | | Batched actions | Yes | Yes | Yes | ## Limitations - **Image format**: PNG only. Orgo's default screenshot is already PNG. - **Coordinates**: normalized, so a wrong `SCREEN_WIDTH` or `SCREEN_HEIGHT` silently misplaces every click. - **Rate limits**: subject to Gemini API rate limits. ## Troubleshooting ### The model does not double-click desktop icons Pass the Ubuntu system prompt as `system_instruction`. Without it, single clicks only select icons. ### INVALID_ARGUMENT: Unable to process input image Gemini received an image that is not PNG. Run the screenshot through `get_screenshot_png()` first. ### Clicks land in the wrong place `SCREEN_WIDTH` and `SCREEN_HEIGHT` do not match the computer. Check them against `GET /computers/{id}/screens`. ### Missing API key Ensure both environment variables are set in your `.env` file: ```bash ORGO_API_KEY=your_orgo_api_key GEMINI_API_KEY=your_gemini_api_key ``` ## Next steps **Gemini Docs** Official Gemini computer use documentation **Orgo Quickstart** Learn more about Orgo computers **API Reference** Complete Orgo API documentation --- # Agent S3 Source: https://docs.orgo.ai/guides/agent-s2 Summary: Let Agent S3 control an Orgo computer. Set up Agent S3, the open-source computer-use agent by Simular AI, against your own laptop or against an Orgo computer. Agent S3 is the current release of the framework, distributed as the `gui-agents` package. It drops the manager and worker hierarchy Agent S2 used, adds a native coding agent that can write and run code alongside GUI actions, and adds Behavior Best-of-N, which runs several rollouts and picks the best one. **Note:** Agent S2 code imported from `gui_agents.s2`. Agent S3 lives under `gui_agents.s3` and renames the top-level class to `AgentS3`. Update both when you upgrade. ## Coordinate space Orgo computers boot at `1280x720x24`. Agent S3 never hands you a coordinate to translate. It emits `pyautogui` code, which `RemoteExecutor` runs **inside** the computer through `POST /computers/{id}/exec`. Both ends are already in the computer's native pixels, so there is nothing to scale. Two things follow from that: - Screenshots must reach the grounding model unresized. Downscaling one before Agent S3 sees it reintroduces exactly the offset this design avoids. - Any resolution you mention in a prompt must match the computer's real one. **Warning:** `grounding_width` and `grounding_height` are not your computer's resolution. They declare the coordinate space the *grounding model* emits in, and the framework maps that space onto the screenshot. Set them to the values your grounding model's docs specify, not to `1280x720`. The Agent S README pairs UI-TARS-1.5-7B with `1920x1080`. `GET /computers/{id}/screens` reports the current `width` and `height`. ## Setup Install the required packages. `gui-agents` supports Python 3.9 through 3.12: ```bash pip pip install gui-agents pyautogui python-dotenv orgo ``` ```bash requirements.txt gui-agents pyautogui python-dotenv orgo pillow ``` Set up your API keys. `HF_TOKEN` is for a grounding model hosted on Hugging Face Inference Endpoints: ```bash terminal icon="terminal" # Export as environment variables export OPENAI_API_KEY=your_openai_api_key export HF_TOKEN=your_huggingface_token export ORGO_API_KEY=your_orgo_api_key # Remote mode only ``` ```python setup.py icon="python" import os os.environ["OPENAI_API_KEY"] = "your_openai_api_key" os.environ["HF_TOKEN"] = "your_huggingface_token" os.environ["ORGO_API_KEY"] = "your_orgo_api_key" # Remote mode only ``` ```bash .env icon="file" OPENAI_API_KEY=your_openai_api_key HF_TOKEN=your_huggingface_token # Remote mode only ORGO_API_KEY=your_orgo_api_key USE_CLOUD_ENVIRONMENT=false ``` ## Simple usage Run Agent S3 with natural language commands: ```bash local icon="terminal" # Local mode - controls your own laptop python agent_s3.py "Open Chrome and search for weather" ``` ```bash remote icon="terminal" # Remote mode - controls an Orgo computer USE_CLOUD_ENVIRONMENT=true python agent_s3.py "Open Chrome" ``` ```bash interactive icon="terminal" # Interactive mode python agent_s3.py ``` The package also ships its own CLI, `agent_s`, which drives the laptop it runs on: ```bash icon="terminal" agent_s \ --provider openai \ --model gpt-5-2025-08-07 \ --ground_provider huggingface \ --ground_url http://localhost:8080 \ --ground_model ui-tars-1.5-7b \ --grounding_width 1920 \ --grounding_height 1080 ``` ## Complete example ```python agent_s3.py expandable icon="python" #!/usr/bin/env python3 import os import io import sys import time import requests from dotenv import load_dotenv from gui_agents.s3.agents.agent_s import AgentS3 from gui_agents.s3.agents.grounding import OSWorldACI from orgo import Computer import pyautogui load_dotenv() CONFIG = { "model": os.getenv("AGENT_MODEL", "gpt-5-2025-08-07"), "model_type": os.getenv("AGENT_MODEL_TYPE", "openai"), "grounding_model": os.getenv("GROUNDING_MODEL", "ui-tars-1.5-7b"), "grounding_type": os.getenv("GROUNDING_MODEL_TYPE", "huggingface"), "grounding_url": os.getenv("GROUNDING_URL", "http://localhost:8080"), "grounding_width": int(os.getenv("GROUNDING_WIDTH", "1920")), "grounding_height": int(os.getenv("GROUNDING_HEIGHT", "1080")), "max_steps": int(os.getenv("MAX_STEPS", "10")), "step_delay": float(os.getenv("STEP_DELAY", "0.5")), "remote": os.getenv("USE_CLOUD_ENVIRONMENT", "false").lower() == "true" } class LocalExecutor: def __init__(self): self.pyautogui = pyautogui if sys.platform == "win32": self.platform = "windows" elif sys.platform == "darwin": self.platform = "darwin" else: self.platform = "linux" def screenshot(self): img = self.pyautogui.screenshot() buffer = io.BytesIO() img.save(buffer, format="PNG") buffer.seek(0) return buffer.getvalue() def exec(self, code): exec(code, {"pyautogui": self.pyautogui, "time": time}) def destroy(self): # No cleanup needed for local executor pass class RemoteExecutor: def __init__(self): # Pass a size: SDKs before 0.0.47 default to 2 GB, which the API rejects. self.computer = Computer(ram=4, cpu=1) self.platform = "linux" # The Linux image ships python3 without pip or pyautogui, and the # actions Agent S3 emits import pyautogui. Install it once. self.computer.bash( "apt-get update -qq && apt-get install -y -qq python3-pip && " "pip3 install --break-system-packages -q pyautogui" ) def screenshot(self): # Agent S3 wants raw image bytes, and response_format=binary returns # the PNG itself. The SDK's screenshot methods cannot read the # stored-image path the API returns by default. response = requests.get( f"https://www.orgo.ai/api/computers/{self.computer.computer_id}/screenshot", params={"response_format": "binary"}, headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, ) response.raise_for_status() return response.content def exec(self, code): result = self.computer.exec(code) if not result['success']: raise Exception(result.get('error', 'Execution failed')) if result['output']: print(f"Output: {result['output']}") def destroy(self): self.computer.destroy() def create_agent(executor): engine_params = { "engine_type": CONFIG["model_type"], "model": CONFIG["model"], } # grounding_width/height describe the grounding model's output space, # not the computer's screen. Leave them as that model's docs specify. engine_params_for_grounding = { "engine_type": CONFIG["grounding_type"], "model": CONFIG["grounding_model"], "base_url": CONFIG["grounding_url"], "grounding_width": CONFIG["grounding_width"], "grounding_height": CONFIG["grounding_height"], } grounding_agent = OSWorldACI( env=None, platform=executor.platform, engine_params_for_generation=engine_params, engine_params_for_grounding=engine_params_for_grounding, ) return AgentS3( engine_params, grounding_agent, platform=executor.platform, ) def run_task(agent, executor, instruction): print(f"\nTask: {instruction}") print(f"Mode: {'Remote' if CONFIG['remote'] else 'Local'}\n") for step in range(CONFIG["max_steps"]): print(f"Step {step + 1}/{CONFIG['max_steps']}") obs = {"screenshot": executor.screenshot()} info, action = agent.predict(instruction=instruction, observation=obs) if info: print(f"[thinking] {info}") if not action or not action[0]: print("Complete") return True try: print(f"[action] {action[0]}") executor.exec(action[0]) except Exception as e: print(f"Error: {e}") instruction = "The previous action failed. Try a different approach." time.sleep(CONFIG["step_delay"]) print("Max steps reached") return False def main(): executor = RemoteExecutor() if CONFIG["remote"] else LocalExecutor() try: agent = create_agent(executor) if len(sys.argv) > 1: run_task(agent, executor, " ".join(sys.argv[1:])) else: print("Interactive mode (type 'exit' to quit)\n") while True: task = input("Task: ").strip() if task == "exit": break elif task: run_task(agent, executor, task) finally: # Clean up executor.destroy() if __name__ == "__main__": main() ``` **Note:** Pass a `LocalEnv` from `gui_agents.s3.utils.local_env` as `OSWorldACI(env=...)` to turn on the coding agent. It executes code on the host running the script, which for `RemoteExecutor` is your laptop rather than the Orgo computer. Leave it `None` unless that is what you want. ## Platform requirements These apply to local mode. Remote mode runs the actions on the Orgo computer, and `RemoteExecutor` installs `pyautogui` there when it starts. ### macOS Grant Terminal access: System Settings, then Privacy & Security, then Accessibility ### Windows May require running Terminal as Administrator ### Linux Install dependencies: ```bash icon="terminal" sudo apt-get install python3-tk python3-dev ``` ## Environment variables | Variable | Default | Description | |----------|---------|-------------| | `OPENAI_API_KEY` | - | Main model API key | | `HF_TOKEN` | - | Hugging Face token for a hosted grounding model | | `ORGO_API_KEY` | - | Orgo API key (remote mode) | | `USE_CLOUD_ENVIRONMENT` | `false` | Set to `true` for remote execution | | `AGENT_MODEL` | `gpt-5-2025-08-07` | Main reasoning model | | `GROUNDING_MODEL` | `ui-tars-1.5-7b` | Visual grounding model | | `GROUNDING_URL` | `http://localhost:8080` | Grounding model endpoint | | `GROUNDING_WIDTH` | `1920` | Grounding model output width | | `GROUNDING_HEIGHT` | `1080` | Grounding model output height | | `MAX_STEPS` | `10` | Maximum steps per task | | `STEP_DELAY` | `0.5` | Seconds between actions | ## Architecture Agent S3 replaced Agent S2's hierarchical planner with a flatter design: **Native coding agent**: writes and runs code alongside GUI actions, so tasks better solved in a shell stop being click sequences **Behavior Best-of-N**: runs several rollouts of a task, summarizes each as a behavior narrative, and has a judge pick the one that completed it **Mixture of grounding**: routes actions to specialized visual grounding models for precise UI localization **Cross-platform support**: works on macOS, Windows, and Linux For current benchmark results, see the [Agent S3 writeup](https://www.simular.ai/articles/agent-s3) and the [OSWorld leaderboard](https://os-world.github.io/). ## Resources - [GitHub Repository](https://github.com/simular-ai/Agent-S) - [Agent S3 writeup](https://www.simular.ai/articles/agent-s3) - [Agent S2 whitepaper](https://arxiv.org/abs/2504.00906) - [OSWorld Benchmark](https://os-world.github.io/) ## Video tutorial The video was recorded against Agent S2. The imports and class names above are the Agent S3 ones. --- # API Reference Source: https://docs.orgo.ai/api-reference/introduction Summary: Base URL, auth, and your first request. The shape of the Orgo API in one page. The Orgo API provisions virtual computers and controls them programmatically. Use it to run agent fleets, automation workflows, or browser testing. ## Base URL ```text https://www.orgo.ai/api ``` ## Authentication All requests require a Bearer token in the `Authorization` header: ```bash Authorization: Bearer $ORGO_API_KEY ``` Get your API key at [orgo.ai/start](https://www.orgo.ai/start). See [Authentication](https://docs.orgo.ai/api-reference/authentication) for key scopes and what they do and do not restrict. ## Quick start ### 1. Create a workspace Workspaces organize your computers. ```bash curl -X POST https://www.orgo.ai/api/workspaces \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "my-workspace"}' ``` ### 2. Create a computer ```bash curl -X POST https://www.orgo.ai/api/computers \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"workspace_id\": \"$WORKSPACE_ID\", \"name\": \"agent-1\", \"os\": \"linux\", \"ram\": 4, \"cpu\": 1}" ``` ### 3. Control the computer ```bash # Take a screenshot curl https://www.orgo.ai/api/computers/$COMPUTER_ID/screenshot \ -H "Authorization: Bearer $ORGO_API_KEY" # Click at coordinates curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/click \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"x": 100, "y": 200}' # Type text curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/type \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "Hello, world"}' # Run a bash command curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/bash \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command": "ls -la"}' ``` ## Resource hierarchy ```text User └── Workspaces └── Computers ``` Workspaces group related computers together. Use them to separate environments (production, staging) or projects. ## Computer specs | Parameter | Values the API accepts | Default when omitted | |-----------|---------|---------| | `os` | `linux`, `windows`, `macos`, `android`, `ios` | `linux` | | `cpu` | 0.5, 1, 2, 4, 8, 16 cores | 1, or the template's size when you pass `template_ref` | | `ram` | 4, 8, 16, 32, 64 GB (macOS also accepts 12) | 4, or the template's size when you pass `template_ref` | | `disk_size_gb` | 1 up to your plan's per-computer maximum | The template's disk when you pass `template_ref`. Otherwise set by your plan: 40 GB on current paid plans, 20 GB on legacy plans, and never above the plan's per-computer maximum (8 GB on Free). | | `gpu` | `2q` (2 GB VRAM), `4q` (4 GB VRAM) | none | | `resolution` | `WIDTHxHEIGHTxDEPTH` (e.g. `1024x768x24`, `1920x1080x24`) | `1280x720x24` (`1920x1080x24` on macOS), or the template's when you pass `template_ref` | Any other value for `os`, `cpu`, `ram`, or `gpu` is rejected with `400`. `ios` places the computer on a physical handset, so it succeeds only where one is available, and `cpu` and `ram` are ignored for it. GPU computers are Linux only. Windows requires either a Windows license on the account or a plan that includes Windows. Maximum CPU, RAM, and disk per computer are capped by your plan. See [orgo.ai/pricing](https://orgo.ai/pricing). ### Recommended configurations | RAM | CPU | Best for | |-----|-----|----------| | 4 GB | 1 core | Standard workflows (default) | | 8 GB | 2 cores | Heavier automation | | 16 GB | 4 cores | Development | | 32 GB | 8 cores | Large-scale processing | ## Available actions ### Mouse - Click (left, right, double) - [Move](https://docs.orgo.ai/api-reference/computers/mouse-move) (hover, without clicking) - Drag - Scroll ### Keyboard - Type text - Press keys (Enter, Tab, ctrl+c, etc.) ### Execution - Bash commands - Python code ### Real-time (WebSocket) - [Terminal](https://docs.orgo.ai/api-reference/computers/terminal): interactive PTY shell - [Audio](https://docs.orgo.ai/api-reference/computers/audio): live PCM audio stream from the computer's virtual speaker - [Events](https://docs.orgo.ai/api-reference/computers/events): subscribe to window, clipboard, file, process, and idle events ### Lifecycle - Start, stop, restart - Auto-stop (off by default; configurable per computer on paid plans) - Clone (copy a computer with full disk state) - Fork (copy a running computer, memory included) - Resize (live CPU/RAM/disk hot-resize) - Move (transfer between workspaces) ### Other - Screenshots - Wait/delays ## Templates [Templates](https://docs.orgo.ai/guides/templates/introduction) are reproducible computers defined in a single `orgo.ai/v1` file: hardware, installed apps, long-running services, secrets, and lifecycle hooks. Orgo builds the file once into a golden snapshot, and every launch restores from it in seconds. - **Launch a curated template.** Pass a `system/…` ref as `template_ref` to [Create computer](https://docs.orgo.ai/api-reference/computers/create). - **Author your own.** [Validate](https://docs.orgo.ai/api-reference/templates/validate), [publish](https://docs.orgo.ai/api-reference/templates/publish), and [build](https://docs.orgo.ai/api-reference/templates/build) over HTTP. Start at the [Templates API](https://docs.orgo.ai/api-reference/templates/schema). Template authoring availability depends on your plan. See [orgo.ai/pricing](https://orgo.ai/pricing). ## Resource IDs Workspaces, computers, files, and threads are identified by UUIDs. Pass the UUID in the URL path wherever you see an `{id}` placeholder, as in `/workspaces/{id}` or `/computers/{id}/click`. Workspace and computer UUIDs are returned in the `id` field of every create, get, and list response. A computer also has a separate instance id, returned as `instance_id` by [Create computer](https://docs.orgo.ai/api-reference/computers/create). The instance id addresses WebSocket paths such as `wss://www.orgo.ai/desktops/{instance_id}/ws/websockify`, not the REST endpoints. ## Error responses Errors return a JSON object with an `error` field holding a human-readable message: ```json { "error": "Invalid API key" } ``` The OpenAI-compatible endpoints under `/v1` are the exception. Their `error` is an object with `type`, `message`, and `code`, described in [Create chat completion](https://docs.orgo.ai/api-reference/chat/completions#errors). Some errors add a machine-readable `code` (for example `workspace_scope_mismatch`, `GUEST_RESTRICTED`, `disk_exceeds_quota`). Branch on `code` where it is present. It is stable, and the `error` string is not. Workspaces are called projects internally, so a few error messages on the workspace endpoints say "project". They refer to the same resource. | Status | Meaning | |--------|---------| | `200` | Success | | `201` | Created | | `204` | Success with no body (destroying a screen) | | `207` | Partial success (resize only: some dimensions applied, others rejected) | | `400` | Invalid request: bad JSON, missing required field, out-of-range value | | `401` | Invalid API key (`Invalid API key`) or no API key (`Authentication required`). Some endpoints also answer an access or scope failure with `401`. | | `402` | Payment required: the credit balance is too low, a free trial computer can no longer run, or a computer paid for on its own has a lapsed payment | | `403` | Authenticated, but not allowed: plan limit exceeded, workspace scope mismatch, view-only access, or no access to the resource | | `404` | Resource not found | | `405` | Method not allowed. Check the verb for the endpoint. | | `409` | Conflict: the resource is in a state that blocks this operation (e.g. resizing a computer that is not running) | | `422` | Validation failed: all dimensions of a resize were rejected | | `429` | Rate limited. Back off and retry. | | `500` | Unexpected server error | | `501` | Not supported on this computer's operating system (for example `exec` on an iPhone) | | `502` | The computer was asked to do something and did not confirm it (for example placing an uploaded file) | | `503` | Orgo, or the computer, could not be reached. Retry after the `Retry-After` delay when the response carries one. | | `504` | A command on the computer ran past its allowed time (`bash` and `exec`) | ## Rate limits Some endpoints are rate limited. If you get a `429`, back off with exponential retry: start at 1s, double each retry, and cap the delay at 60s. Email [spencer@orgo.ai](mailto:spencer@orgo.ai) if you need higher limits. ## Next steps **Create Workspace** Organize computers **Create Computer** Provision a computer **Templates** Reproducible computers **Authentication** API key setup **Use Any Model** Claude, GPT, Gemini, and more --- # Authentication Source: https://docs.orgo.ai/api-reference/authentication Summary: Create, scope, and rotate API keys. Every way a request authenticates. All API requests require a Bearer token in the `Authorization` header. ```http Authorization: Bearer sk_live_your_api_key_here ``` Only tokens beginning with `sk_` are treated as API keys. Tokens beginning with `orgo_mcp_` are issued to MCP clients through the MCP sign-in flow, and reach only computer, file, and workspace-read endpoints. Any other Bearer value is ignored, and the request falls through to browser session authentication. A server-to-server caller has no browser session, so the request fails with `401`. ## Get your API key 1. Sign in at [orgo.ai/start](https://www.orgo.ai/start). 2. Open [orgo.ai/settings/credentials](https://www.orgo.ai/settings/credentials). 3. In the **API keys** section, click **New key**. 4. Pick a scope (described in [Key scopes](#key-scopes)) and a name, then select **Create key**. 5. Copy the plaintext value. It is shown once. ## Key scopes Every API key is either account-wide or pinned to a single workspace. | Scope | What it means | |---|---| | **Account-wide** | No workspace attached. Reaches everything the account can reach. | | **Workspace-scoped** | One workspace attached. Reaches that workspace and the computers and files in it, and nothing else. | ### What a workspace-scoped key can do Scope is checked on every request, before the endpoint runs. A workspace-scoped key: - Can call the computer, workspace, file, and screenshot endpoints, but only for its own workspace and the computers and files in it. Any other workspace ID, computer ID, or file ID is refused. - Can call [chat completions](https://docs.orgo.ai/api-reference/chat/completions) for computers in its own workspace. - Sees only its own workspace in [List workspaces](https://docs.orgo.ai/api-reference/workspaces/list). - Cannot create workspaces. - Cannot call any other endpoint, including [templates](https://docs.orgo.ai/api-reference/templates/schema) and [threads](https://docs.orgo.ai/api-reference/chat/threads). - Can list API keys, filtered to its own workspace, but cannot create or delete keys. A refused request carries one of these messages: ```json { "error": "This API key cannot access this workspace (workspace_scope_mismatch)." } ``` ```json { "error": "This endpoint requires an account-wide credential (workspace_scope_mismatch)." } ``` ```json { "error": "Workspace-scoped keys cannot create workspaces (workspace_scope_mismatch)." } ``` The status is `403` on most endpoints. Some endpoints answer every failed access check with `401` instead, with the same body. Each endpoint's page lists which one it returns. The OpenAI-compatible surface (`/v1/...`) returns its own `401` envelope, described in the note under [Error responses](#error-responses). A few endpoints, such as [Upload file](https://docs.orgo.ai/api-reference/files/upload), add a second check with a richer body: ```json { "error": "This API key is scoped to workspace . It cannot access workspace . Use an account-wide key, or create a key scoped to the target workspace.", "code": "workspace_scope_mismatch", "key_workspace_id": "", "target_workspace_id": "" } ``` Match on the `workspace_scope_mismatch` text, which every variant contains, rather than on the full message. ### Workspace roles The same check applies your role in the workspace, whether you authenticate with a key or a session. An invited member with view-only access can read the workspace and its computers but cannot change them. A write attempt is refused with `This workspace is view-only. Ask the owner for write access (workspace_read_only).` A workspace you neither own nor belong to is refused with `You do not have access to this workspace.` ## Rotating a key Generate a new key, update your client to use it, then delete the old one. Both keys work while you migrate. Key list, creation, and deletion live at [orgo.ai/settings/credentials](https://www.orgo.ai/settings/credentials). There is no documented public endpoint for managing keys. **Warning:** Store API keys securely. Never commit them to version control or share them publicly. Rotate immediately if a key is exposed. ## Examples ```bash cURL curl https://www.orgo.ai/api/workspaces \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import requests import os api_key = os.environ["ORGO_API_KEY"] response = requests.get( "https://www.orgo.ai/api/workspaces", headers={"Authorization": f"Bearer {api_key}"}, ) ``` ```javascript JavaScript const apiKey = process.env.ORGO_API_KEY; const response = await fetch('https://www.orgo.ai/api/workspaces', { headers: { 'Authorization': `Bearer ${apiKey}` }, }); ``` ## Environment variables ```bash export ORGO_API_KEY=sk_live_abc123 ``` ```bash .env ORGO_API_KEY=sk_live_abc123 ``` ## Error responses | Status | Body | Meaning | |---|---|---| | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. Check for typos; generate a new one if needed. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key, and no signed-in session. | | `401` | `{ "error": "MFA required" }` | The signed-in session has a verified TOTP factor but no valid MFA cookie. Session calls only: API key requests are never MFA-gated. | | `401` | `{ "error": "Authentication error" }` | Authentication failed unexpectedly on the server. Retry. | | `401` or `403` | `{ "error": "This API key cannot access this workspace (workspace_scope_mismatch)." }` | A workspace-scoped key addressed another workspace, or a computer or file in one. | | `401` or `403` | `{ "error": "This endpoint requires an account-wide credential (workspace_scope_mismatch)." }` | A workspace-scoped key called an endpoint outside the computer, workspace, and file surfaces. | | `401` or `403` | `{ "error": "You do not have access to this workspace." }` | The key's account neither owns nor belongs to the target workspace. | | `401` or `403` | `{ "error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)." }` | The key's account has view-only access and the request changes something. | | `403` | `{ "error": "…", "code": "workspace_scope_mismatch", "key_workspace_id": "…", "target_workspace_id": "…" }` | A workspace-scoped key acted on another workspace, on an endpoint with the second check. | | `403` | `{ "error": "Sign up to create your own workspace.", "code": "GUEST_RESTRICTED" }` | The caller joined through a share link. Guests cannot create workspaces, computers, or invites. | | `503` | `{ "error": "Service temporarily unavailable. …" }` | Orgo could not verify the credential because of a server-side fault, such as a database outage. The response carries `Retry-After: 5`. Some endpoints report this as `401` with the same message. Retry either way. Do not rotate the key. | **Note:** The OpenAI-compatible surface (`/v1/chat/completions` and `/v1/threads`) uses a nested error envelope instead: `{ "error": { "type": "authentication_error", "message": "…", "code": "invalid_api_key" } }`. It returns that one body with `401` for every authentication or access failure, including a missing or invalid key, any workspace-scoped key on `/v1/threads`, and a workspace-scoped key naming a computer outside its workspace on `/v1/chat/completions`. See [Troubleshooting](https://docs.orgo.ai/guides/troubleshooting) for the full error reference and recovery steps. ## Security tips - Use environment variables. Never hardcode keys. - Add `.env` to `.gitignore`. - Create a separate workspace-scoped key per integration, keeping in mind the limits above. - Rotate keys when team members leave. ## Need help? Email [spencer@orgo.ai](mailto:spencer@orgo.ai). Include the `request_id` from the error response when the response carries one. --- # Get capacity Source: https://docs.orgo.ai/api-reference/account/capacity Endpoint: GET /account/capacity Summary: How many computers the account may run and runs, and how many more of each size it may add. Reads the account's capacity: its limits and what it uses, what it has added beyond its plan, its self-serve cap, and how many more computers of each size it may add right now. Use it before a purchase to know what fits, and after one to see the new limits. **Note:** Capacity is billed to the whole account, so this endpoint needs an account-wide API key. A workspace-scoped key returns `403` with code `account_key_required`. ```bash curl https://www.orgo.ai/api/account/capacity \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```json { "plan": "startup_v2", "computers": { "limit": 104, "in_use": 101 }, "ram_gb": { "limit": 832, "in_use": 808 }, "added": { "computers": 100, "ram_gb": 800, "vcpu": 50, "disk_gb": 4000 }, "self_serve_cap": { "computers": 100, "ram_gb": 800, "vcpu": 50, "disk_gb": 4000 }, "can_add": { "small": 0, "medium": 0, "large": 0, "xl": 0, "2xl": 0 }, "max_per_request": 500, "sizes": [ { "size": "small", "ram_gb": 4, "vcpu": 0.5, "disk_gb": 20, "monthly_usd": 14 }, { "size": "medium", "ram_gb": 8, "vcpu": 1, "disk_gb": 40, "monthly_usd": 25 } ], "terms": ["monthly", "yearly"], "windows_available": true, "has_subscription": true, "renews_at": "2026-11-01T00:00:00.000Z", "scheduled_reduction": null } ``` `can_add` at `0` with `added.computers` equal to `self_serve_cap.computers` means the account has reached its self-serve cap. [Ask us](mailto:support@orgo.ai) and we raise it for the account, at the same prices. --- # Add capacity Source: https://docs.orgo.ai/api-reference/account/add-capacity Endpoint: POST /account/capacity Summary: Add computers to the account, charged to the card on file. Adds `count` computers of `size` to the account, at the same prices as the Plan tab. The charge for the rest of the current period is made now, and the full amount joins every renewal after it. ```bash curl -X POST https://www.orgo.ai/api/account/capacity \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "size": "medium", "count": 100 }' ``` ## Price it first Send `dry_run: true` to see the charge without making it: ```json { "size": "medium", "count": 100, "dry_run": true } ``` ```json { "dry_run": true, "size": "medium", "count": 100, "os": "linux", "term": "monthly", "monthly_usd": 2500, "due_now_usd": 1650, "next_invoice_usd": 4149, "next_invoice_at": "2026-11-01T00:00:00.000Z" } ``` ## Retries never buy twice Send an `Idempotency-Key` header, a new UUID per purchase. If the connection drops before the answer arrives, retry with the same key and body: you get the first answer back, marked `Idempotent-Replayed: true`, and nothing is bought again. | Answer | Meaning | | --- | --- | | `409` `idempotency_in_progress` | The first request is still running. Retry in a few seconds. | | `409` `idempotency_unfinished` | The first request did not finish and may have charged. Check [Get capacity](https://docs.orgo.ai/api-reference/account/capacity) before trying again with a new key. | | `422` `idempotency_key_reused` | The key was already used for a different body. | ## The self-serve cap Every account can add up to 100 computers on its own (with up to 800 GB RAM, 50 vCPU and 4 TB of disk across them). Past that, a purchase returns `403` with code `capacity_cap`, naming the limit: ```json { "error": "That would go past this account's limit of 100 extra computers. Ask us to raise it.", "code": "capacity_cap", "self_serve_cap": { "computers": 100, "ram_gb": 800, "vcpu": 50, "disk_gb": 4000 } } ``` [Ask us](mailto:support@orgo.ai) and we raise the cap for the account, to thousands of computers if you need them. Nothing else about the account changes. --- # Reduce capacity Source: https://docs.orgo.ai/api-reference/account/reduce-capacity Endpoint: PATCH /account/capacity Summary: Keep fewer added computers from the next renewal. Sets how many added computers the account keeps from its next renewal. The current period is already paid for, so the computers and the bill stay until then and drop together. Sending the current number undoes a pending reduction. ```bash curl -X PATCH https://www.orgo.ai/api/account/capacity \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "added_computers": 20 }' ``` ```json { "scheduled": { "added_computers": 20, "effective_at": "2026-11-01T00:00:00.000Z" }, "capacity": { "...": "as Get capacity" } } ``` Capacity a running computer stands on can't be given back: the answer is `409` with code `in_use` and `min_added_computers`. Delete computers first, then reduce. --- # List clients Source: https://docs.orgo.ai/api-reference/clients/list Endpoint: GET /v1/accounts Summary: Your clients, newest first, with the total and what they pay you each month. Returns active clients unless you ask for closed ones. The `total` counts every match, not just this page, and `collecting` sums what paying clients send you each month. Use your account-wide API key. A client’s own key gets `403` here. See [Invoicing](https://docs.orgo.ai/guides/invoicing). ## Query parameters - `search` (query, string) Matches `name` or `external_id`. - `status` (query, string) `closed` lists closed clients. Anything else lists active ones. - `limit` (query, integer) Up to 200. Defaults to 50. - `before` (query, string) The `next` value from the previous page. ## Response - `accounts` (array) Clients, newest first. See [Get client](https://docs.orgo.ai/api-reference/clients/get) for each field. - `next` (string) Pass as `before` for the next page, or `null` on the last one. - `total` (integer) Clients matching the filters, across every page. - `collecting` (object) `clients` paying you, and `usd_per_month` they pay in all. ## Example ```bash cURL curl https://www.orgo.ai/api/v1/accounts \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os import requests res = requests.get( f"https://www.orgo.ai/api/v1/accounts", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, ) print(res.json()) ``` ```javascript JavaScript const res = await fetch(`https://www.orgo.ai/api/v1/accounts`, { headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` }, }); console.log(await res.json()); ``` ### Response ```json { "accounts": [ { "id": "8f14e45f-ceea-467a-9575-6f1c2b3d4e5f", "external_id": "acme-dental", "name": "Acme Dental", "status": "active", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2026-10-01T09:00:00Z", "closed_at": null, "limits": { "max_computers": 3, "monthly_ai_cap_usd": 40 }, "usage": { "computers": 2, "ai_spend_usd_this_month": 12.4 }, "billing": { "status": "active", "price_usd": 250, "email": "billing@acme.com" }, "transfer": null } ], "next": null, "total": 1, "collecting": { "clients": 1, "usd_per_month": 250 } } ``` ## Errors | Status | Body | Meaning | |---|---|---| | `401` | `{ "error": "Invalid API key" }` | No key, or not one of yours. | | `403` | `{ "error": "Use your account-wide API key to manage accounts. Workspace keys can’t." }` | A client’s own key, or any key scoped to one workspace. | | `403` | `{ "error": "Invoicing is on every paid plan. Upgrade to use it." }` | You’re on the Free plan. `code` is `UPGRADE_REQUIRED`. | --- # Add a client Source: https://docs.orgo.ai/api-reference/clients/create Endpoint: POST /v1/accounts Summary: Create a workspace for a client, with an API key that can reach only it. The client gets their own workspace and a key scoped to it. They never sign up to Orgo: your app uses the key on their behalf. The key is in this response only, so store it with your user. Use your account-wide API key. A client’s own key gets `403` here. See [Invoicing](https://docs.orgo.ai/guides/invoicing). ## Body - `name` (body, string) Shown in Settings. Send this, `external_id`, or both. - `external_id` (body, string) Your id for this client, unique among your clients. Made from `name` when you leave it out. - `max_computers` (body, integer) The most computers they can have. No limit when left out. - `monthly_ai_cap_usd` (body, number) Their AI stops at this amount each month. No limit when left out. ## Response - `api_key` (string) The client’s key. Shown once. Every field of [Get client](https://docs.orgo.ai/api-reference/clients/get). ## Example ```bash cURL curl -X POST https://www.orgo.ai/api/v1/accounts \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Acme Dental", "external_id": "acme-dental"}' ``` ```python Python import os import requests res = requests.post( f"https://www.orgo.ai/api/v1/accounts", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, json={"name": "Acme Dental", "external_id": "acme-dental"}, ) print(res.json()) ``` ```javascript JavaScript const res = await fetch(`https://www.orgo.ai/api/v1/accounts`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({"name": "Acme Dental", "external_id": "acme-dental"}), }); console.log(await res.json()); ``` ### Response ```json { "id": "8f14e45f-ceea-467a-9575-6f1c2b3d4e5f", "external_id": "acme-dental", "name": "Acme Dental", "status": "active", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2026-10-01T09:00:00Z", "closed_at": null, "limits": { "max_computers": null, "monthly_ai_cap_usd": null }, "usage": { "computers": 0, "ai_spend_usd_this_month": 0 }, "billing": null, "transfer": null, "api_key": "sk_live_…" } ``` ## Errors | Status | Body | Meaning | |---|---|---| | `401` | `{ "error": "Invalid API key" }` | No key, or not one of yours. | | `403` | `{ "error": "Use your account-wide API key to manage accounts. Workspace keys can’t." }` | A client’s own key, or any key scoped to one workspace. | | `403` | `{ "error": "Invoicing is on every paid plan. Upgrade to use it." }` | You’re on the Free plan. `code` is `UPGRADE_REQUIRED`. | | `400` | `{ "error": "Send a name, an external_id, or both." }` | Neither was sent. | | `409` | `{ "error": "An account with external_id "acme-dental" already exists." }` | `code` is `ACCOUNT_EXISTS`. | --- # Get client Source: https://docs.orgo.ai/api-reference/clients/get Endpoint: GET /v1/accounts/{id} Summary: One client, with this month’s usage, who pays, and their computers. One client, with this month’s usage, who pays, and their computers. Use your account-wide API key. A client’s own key gets `403` here. See [Invoicing](https://docs.orgo.ai/guides/invoicing). ## Path parameters - `id` (path, string, required) The client’s `id`. ## Response - `id` (string) The client’s id. - `external_id` (string) Your id for them. - `name` (string) Their name, or `null`. - `status` (string) `active`, `closed`, or `transferred` (they own it now). - `workspace_id` (string) Their workspace. Use it with any workspace or computer endpoint. - `limits` (object) `max_computers` and `monthly_ai_cap_usd`. `null` is no limit. - `usage` (object) `computers` they have and `ai_spend_usd_this_month`. - `billing` (object) `status`, `price_usd`, and `email` while they pay you, or `null` while you pay. - `transfer` (object) `email` and `sent_at` while a transfer waits to be accepted, or `null`. - `computers` (array) Their computers, newest first: `id`, `name`, `status`, `os`, `cpu`, `ram`, `disk_size_gb`. - `created_at` (string) ISO 8601 timestamp. - `closed_at` (string) When it was closed, or `null`. ## Example ```bash cURL curl https://www.orgo.ai/api/v1/accounts/$CLIENT_ID \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os import requests client_id = os.environ["CLIENT_ID"] res = requests.get( f"https://www.orgo.ai/api/v1/accounts/{client_id}", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, ) print(res.json()) ``` ```javascript JavaScript const clientId = process.env.CLIENT_ID; const res = await fetch(`https://www.orgo.ai/api/v1/accounts/${clientId}`, { headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` }, }); console.log(await res.json()); ``` ### Response ```json { "id": "8f14e45f-ceea-467a-9575-6f1c2b3d4e5f", "external_id": "acme-dental", "name": "Acme Dental", "status": "active", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2026-10-01T09:00:00Z", "closed_at": null, "limits": { "max_computers": 3, "monthly_ai_cap_usd": 40 }, "usage": { "computers": 2, "ai_spend_usd_this_month": 12.4 }, "billing": { "status": "active", "price_usd": 250, "email": "billing@acme.com" }, "transfer": null, "computers": [ { "id": "a3bb189e-8bf9-3888-9912-ace4e6543002", "name": "agent-1", "status": "running", "os": "linux", "cpu": 2, "ram": 8, "disk_size_gb": 40 } ] } ``` ## Errors | Status | Body | Meaning | |---|---|---| | `401` | `{ "error": "Invalid API key" }` | No key, or not one of yours. | | `403` | `{ "error": "Use your account-wide API key to manage accounts. Workspace keys can’t." }` | A client’s own key, or any key scoped to one workspace. | | `403` | `{ "error": "Invoicing is on every paid plan. Upgrade to use it." }` | You’re on the Free plan. `code` is `UPGRADE_REQUIRED`. | | `404` | `{ "error": "Account not found." }` | No client with this id is yours. | --- # Update client Source: https://docs.orgo.ai/api-reference/clients/update Endpoint: PATCH /v1/accounts/{id} Summary: Rename a client or change their limits. Send only what changes. `null` removes a limit. Use your account-wide API key. A client’s own key gets `403` here. See [Invoicing](https://docs.orgo.ai/guides/invoicing). ## Path parameters - `id` (path, string, required) The client’s `id`. ## Body - `name` (body, string) Their new name. - `max_computers` (body, integer) The most computers they can have, or `null`. - `monthly_ai_cap_usd` (body, number) Their monthly AI cap in dollars, or `null`. ## Response The client, as [Get client](https://docs.orgo.ai/api-reference/clients/get) returns it. ## Example ```bash cURL curl -X PATCH https://www.orgo.ai/api/v1/accounts/$CLIENT_ID \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"max_computers": 5}' ``` ```python Python import os import requests client_id = os.environ["CLIENT_ID"] res = requests.patch( f"https://www.orgo.ai/api/v1/accounts/{client_id}", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, json={"max_computers": 5}, ) print(res.json()) ``` ```javascript JavaScript const clientId = process.env.CLIENT_ID; const res = await fetch(`https://www.orgo.ai/api/v1/accounts/${clientId}`, { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({"max_computers": 5}), }); console.log(await res.json()); ``` ### Response ```json { "id": "8f14e45f-ceea-467a-9575-6f1c2b3d4e5f", "external_id": "acme-dental", "name": "Acme Dental", "status": "active", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2026-10-01T09:00:00Z", "closed_at": null, "limits": { "max_computers": 5, "monthly_ai_cap_usd": 40 }, "usage": { "computers": 2, "ai_spend_usd_this_month": 12.4 }, "billing": { "status": "active", "price_usd": 250, "email": "billing@acme.com" }, "transfer": null } ``` ## Errors | Status | Body | Meaning | |---|---|---| | `401` | `{ "error": "Invalid API key" }` | No key, or not one of yours. | | `403` | `{ "error": "Use your account-wide API key to manage accounts. Workspace keys can’t." }` | A client’s own key, or any key scoped to one workspace. | | `403` | `{ "error": "Invoicing is on every paid plan. Upgrade to use it." }` | You’re on the Free plan. `code` is `UPGRADE_REQUIRED`. | | `400` | `{ "error": "max_computers must be a whole number from 0, or null for no limit." }` | A limit isn’t valid. | | `404` | `{ "error": "Account not found." }` | No client with this id is yours. | | `409` | `{ "error": "This account is closed." }` | The client is closed. `code` is `ACCOUNT_CLOSED`. | | `409` | `{ "error": "This account was transferred to your client." }` | The client accepted a transfer and owns it now. `code` is `ACCOUNT_TRANSFERRED`. | --- # Close client Source: https://docs.orgo.ai/api-reference/clients/close Endpoint: DELETE /v1/accounts/{id} Summary: Close a client. Their key stops working at once. Delete their computers first: closing never deletes a computer for you. Use your account-wide API key. A client’s own key gets `403` here. See [Invoicing](https://docs.orgo.ai/guides/invoicing). ## Path parameters - `id` (path, string, required) The client’s `id`. ## Response The client, with `status` `closed`. ## Example ```bash cURL curl -X DELETE https://www.orgo.ai/api/v1/accounts/$CLIENT_ID \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os import requests client_id = os.environ["CLIENT_ID"] res = requests.delete( f"https://www.orgo.ai/api/v1/accounts/{client_id}", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, ) print(res.json()) ``` ```javascript JavaScript const clientId = process.env.CLIENT_ID; const res = await fetch(`https://www.orgo.ai/api/v1/accounts/${clientId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` }, }); console.log(await res.json()); ``` ### Response ```json { "id": "8f14e45f-ceea-467a-9575-6f1c2b3d4e5f", "external_id": "acme-dental", "name": "Acme Dental", "status": "closed", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2026-10-01T09:00:00Z", "closed_at": "2026-10-20T15:00:00Z", "limits": { "max_computers": 3, "monthly_ai_cap_usd": 40 }, "usage": { "computers": 0, "ai_spend_usd_this_month": 0 }, "billing": { "status": "active", "price_usd": 250, "email": "billing@acme.com" }, "transfer": null } ``` ## Errors | Status | Body | Meaning | |---|---|---| | `401` | `{ "error": "Invalid API key" }` | No key, or not one of yours. | | `403` | `{ "error": "Use your account-wide API key to manage accounts. Workspace keys can’t." }` | A client’s own key, or any key scoped to one workspace. | | `403` | `{ "error": "Invoicing is on every paid plan. Upgrade to use it." }` | You’re on the Free plan. `code` is `UPGRADE_REQUIRED`. | | `404` | `{ "error": "Account not found." }` | No client with this id is yours. | | `409` | `{ "error": "This account still has 2 computers. Delete them first." }` | `code` is `ACCOUNT_HAS_COMPUTERS`. | --- # Replace a client’s key Source: https://docs.orgo.ai/api-reference/clients/replace-key Endpoint: POST /v1/accounts/{id}/key Summary: A new key for the client. Every earlier one stops working. A new key for the client. Every earlier one stops working. Use your account-wide API key. A client’s own key gets `403` here. See [Invoicing](https://docs.orgo.ai/guides/invoicing). ## Path parameters - `id` (path, string, required) The client’s `id`. ## Response - `api_key` (string) The new key. Shown once. ## Example ```bash cURL curl -X POST https://www.orgo.ai/api/v1/accounts/$CLIENT_ID/key \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os import requests client_id = os.environ["CLIENT_ID"] res = requests.post( f"https://www.orgo.ai/api/v1/accounts/{client_id}/key", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, ) print(res.json()) ``` ```javascript JavaScript const clientId = process.env.CLIENT_ID; const res = await fetch(`https://www.orgo.ai/api/v1/accounts/${clientId}/key`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` }, }); console.log(await res.json()); ``` ### Response ```json { "api_key": "sk_live_…" } ``` ## Errors | Status | Body | Meaning | |---|---|---| | `401` | `{ "error": "Invalid API key" }` | No key, or not one of yours. | | `403` | `{ "error": "Use your account-wide API key to manage accounts. Workspace keys can’t." }` | A client’s own key, or any key scoped to one workspace. | | `403` | `{ "error": "Invoicing is on every paid plan. Upgrade to use it." }` | You’re on the Free plan. `code` is `UPGRADE_REQUIRED`. | | `404` | `{ "error": "Account not found." }` | No client with this id is yours. | | `409` | `{ "error": "This account is closed." }` | The client is closed. `code` is `ACCOUNT_CLOSED`. | | `409` | `{ "error": "This account was transferred to your client." }` | The client accepted a transfer and owns it now. `code` is `ACCOUNT_TRANSFERRED`. | --- # Transfer a client Source: https://docs.orgo.ai/api-reference/clients/transfer Endpoint: POST /v1/accounts/{id}/transfer Summary: Hand the workspace, its computers, and its key to your client’s own Orgo account. Emails your client a link from your name. Once they accept it, signed in to their own Orgo account (or a new one), the workspace, its computers, and its key are theirs, on their plan and AI credits. Whatever you built on the key keeps working. You stay on as an admin until they remove you, and any billing through you ends. The link works for 14 days. Sending again replaces it. If their plan can’t run the computers yet, they pick one before accepting. Use your account-wide API key. A client’s own key gets `403` here. See [Invoicing](https://docs.orgo.ai/guides/invoicing). ## Path parameters - `id` (path, string, required) The client’s `id`. ## Body - `email` (body, string, required) Your client’s email. They accept signed in with it. ## Response The client, with `transfer` set. After they accept, its `status` is `transferred`. ## Example ```bash cURL curl -X POST https://www.orgo.ai/api/v1/accounts/$CLIENT_ID/transfer \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "it@acme.com"}' ``` ```python Python import os import requests client_id = os.environ["CLIENT_ID"] res = requests.post( f"https://www.orgo.ai/api/v1/accounts/{client_id}/transfer", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, json={"email": "it@acme.com"}, ) print(res.json()) ``` ```javascript JavaScript const clientId = process.env.CLIENT_ID; const res = await fetch(`https://www.orgo.ai/api/v1/accounts/${clientId}/transfer`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({"email": "it@acme.com"}), }); console.log(await res.json()); ``` ### Response ```json { "id": "8f14e45f-ceea-467a-9575-6f1c2b3d4e5f", "external_id": "acme-dental", "name": "Acme Dental", "status": "active", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2026-10-01T09:00:00Z", "closed_at": null, "limits": { "max_computers": 3, "monthly_ai_cap_usd": 40 }, "usage": { "computers": 2, "ai_spend_usd_this_month": 12.4 }, "billing": { "status": "active", "price_usd": 250, "email": "billing@acme.com" }, "transfer": { "email": "it@acme.com", "sent_at": "2026-10-20T15:00:00Z" } } ``` ## Errors | Status | Body | Meaning | |---|---|---| | `401` | `{ "error": "Invalid API key" }` | No key, or not one of yours. | | `403` | `{ "error": "Use your account-wide API key to manage accounts. Workspace keys can’t." }` | A client’s own key, or any key scoped to one workspace. | | `403` | `{ "error": "Invoicing is on every paid plan. Upgrade to use it." }` | You’re on the Free plan. `code` is `UPGRADE_REQUIRED`. | | `400` | `{ "error": "Enter your client’s email." }` | Missing or not an email. | | `400` | `{ "error": "Send it to your client’s email, not yours." }` | It went to your own email. | | `404` | `{ "error": "Account not found." }` | No client with this id is yours. | | `409` | `{ "error": "This account is closed." }` | The client is closed. `code` is `ACCOUNT_CLOSED`. | | `409` | `{ "error": "This account was transferred to your client." }` | The client accepted a transfer and owns it now. `code` is `ACCOUNT_TRANSFERRED`. | --- # Withdraw a transfer Source: https://docs.orgo.ai/api-reference/clients/withdraw-transfer Endpoint: DELETE /v1/accounts/{id}/transfer Summary: Withdraw a transfer your client hasn’t accepted. Its link stops working. Withdraw a transfer your client hasn’t accepted. Its link stops working. Use your account-wide API key. A client’s own key gets `403` here. See [Invoicing](https://docs.orgo.ai/guides/invoicing). ## Path parameters - `id` (path, string, required) The client’s `id`. ## Response The client, with `transfer` `null`. ## Example ```bash cURL curl -X DELETE https://www.orgo.ai/api/v1/accounts/$CLIENT_ID/transfer \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os import requests client_id = os.environ["CLIENT_ID"] res = requests.delete( f"https://www.orgo.ai/api/v1/accounts/{client_id}/transfer", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, ) print(res.json()) ``` ```javascript JavaScript const clientId = process.env.CLIENT_ID; const res = await fetch(`https://www.orgo.ai/api/v1/accounts/${clientId}/transfer`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` }, }); console.log(await res.json()); ``` ### Response ```json { "id": "8f14e45f-ceea-467a-9575-6f1c2b3d4e5f", "external_id": "acme-dental", "name": "Acme Dental", "status": "active", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2026-10-01T09:00:00Z", "closed_at": null, "limits": { "max_computers": 3, "monthly_ai_cap_usd": 40 }, "usage": { "computers": 2, "ai_spend_usd_this_month": 12.4 }, "billing": { "status": "active", "price_usd": 250, "email": "billing@acme.com" }, "transfer": null } ``` ## Errors | Status | Body | Meaning | |---|---|---| | `401` | `{ "error": "Invalid API key" }` | No key, or not one of yours. | | `403` | `{ "error": "Use your account-wide API key to manage accounts. Workspace keys can’t." }` | A client’s own key, or any key scoped to one workspace. | | `403` | `{ "error": "Invoicing is on every paid plan. Upgrade to use it." }` | You’re on the Free plan. `code` is `UPGRADE_REQUIRED`. | | `404` | `{ "error": "Account not found." }` | No client with this id is yours. | --- # Get billing Source: https://docs.orgo.ai/api-reference/clients/get-billing Endpoint: GET /v1/accounts/{id}/billing Summary: Whether the client pays you, how much, and when. Whether the client pays you, how much, and when. Use your account-wide API key. A client’s own key gets `403` here. See [Invoicing](https://docs.orgo.ai/guides/invoicing). ## Path parameters - `id` (path, string, required) The client’s `id`. ## Response - `billing` (object) Their billing, or `null` while you pay for them. - `billing.status` (string) `sent` (link not paid yet), `active`, or `past_due`. - `billing.email` (string) Where the link and receipts go. - `billing.price_usd` (number) What they pay each month, before AI. - `billing.ai_usd` (number) Their monthly AI amount, billed at cost. `0` when none. - `billing.setup_fee_usd` (number) The one-time fee charged with the first month. - `billing.payment_link_url` (string) The link while it waits to be paid, else `null`. - `billing.current_period_end` (string) When the paid month ends. - `billing.ends_at_period_end` (boolean) `true` once you stop billing: it ends with the paid month. ## Example ```bash cURL curl https://www.orgo.ai/api/v1/accounts/$CLIENT_ID/billing \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os import requests client_id = os.environ["CLIENT_ID"] res = requests.get( f"https://www.orgo.ai/api/v1/accounts/{client_id}/billing", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, ) print(res.json()) ``` ```javascript JavaScript const clientId = process.env.CLIENT_ID; const res = await fetch(`https://www.orgo.ai/api/v1/accounts/${clientId}/billing`, { headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` }, }); console.log(await res.json()); ``` ### Response ```json { "billing": { "status": "active", "email": "billing@acme.com", "price_usd": 250, "setup_fee_usd": 0, "ai_usd": 40, "payment_link_url": null, "current_period_end": "2026-11-01T09:00:00Z", "ends_at_period_end": false } } ``` ## Errors | Status | Body | Meaning | |---|---|---| | `401` | `{ "error": "Invalid API key" }` | No key, or not one of yours. | | `403` | `{ "error": "Use your account-wide API key to manage accounts. Workspace keys can’t." }` | A client’s own key, or any key scoped to one workspace. | | `403` | `{ "error": "Invoicing is on every paid plan. Upgrade to use it." }` | You’re on the Free plan. `code` is `UPGRADE_REQUIRED`. | | `404` | `{ "error": "Account not found." }` | No client with this id is yours. | | `409` | `{ "error": "This account is closed." }` | The client is closed. `code` is `ACCOUNT_CLOSED`. | | `409` | `{ "error": "This account was transferred to your client." }` | The client accepted a transfer and owns it now. `code` is `ACCOUNT_TRANSFERRED`. | --- # Bill a client Source: https://docs.orgo.ai/api-reference/clients/send-payment-link Endpoint: POST /v1/accounts/{id}/billing Summary: Email your client a payment link. They pay you monthly, and need no Orgo account. The email comes from your business, and checkout carries your business name, logo, and color. Set up payouts once in **Settings → Invoicing** first. Orgo keeps the client’s computer cost and any AI amount, and Stripe takes its card fee (2.9% + 30¢). The rest is paid out to you. While they pay, their computers don’t count against your plan, and each paid month adds `ai_usd` to your credits and caps their AI at it. Use your account-wide API key. A client’s own key gets `403` here. See [Invoicing](https://docs.orgo.ai/guides/invoicing). ## Path parameters - `id` (path, string, required) The client’s `id`. ## Body - `email` (body, string, required) Where to send the link. - `price_usd` (body, number, required) What they pay each month. At least their computers’ cost. - `ai_usd` (body, number) A monthly amount of AI they pay for at cost, as its own line. - `setup_fee_usd` (body, number) Charged once, with the first month. ## Response - `billing` (object) As [Get billing](https://docs.orgo.ai/api-reference/clients/get-billing), with `status` `sent` and the `payment_link_url`. ## Example ```bash cURL curl -X POST https://www.orgo.ai/api/v1/accounts/$CLIENT_ID/billing \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "billing@acme.com", "price_usd": 250, "ai_usd": 40, "setup_fee_usd": 2500}' ``` ```python Python import os import requests client_id = os.environ["CLIENT_ID"] res = requests.post( f"https://www.orgo.ai/api/v1/accounts/{client_id}/billing", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, json={"email": "billing@acme.com", "price_usd": 250, "ai_usd": 40, "setup_fee_usd": 2500}, ) print(res.json()) ``` ```javascript JavaScript const clientId = process.env.CLIENT_ID; const res = await fetch(`https://www.orgo.ai/api/v1/accounts/${clientId}/billing`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({"email": "billing@acme.com", "price_usd": 250, "ai_usd": 40, "setup_fee_usd": 2500}), }); console.log(await res.json()); ``` ### Response ```json { "billing": { "status": "sent", "email": "billing@acme.com", "price_usd": 250, "setup_fee_usd": 2500, "ai_usd": 40, "payment_link_url": "https://buy.stripe.com/…", "current_period_end": null, "ends_at_period_end": false } } ``` ## Errors | Status | Body | Meaning | |---|---|---| | `401` | `{ "error": "Invalid API key" }` | No key, or not one of yours. | | `403` | `{ "error": "Use your account-wide API key to manage accounts. Workspace keys can’t." }` | A client’s own key, or any key scoped to one workspace. | | `403` | `{ "error": "Invoicing is on every paid plan. Upgrade to use it." }` | You’re on the Free plan. `code` is `UPGRADE_REQUIRED`. | | `400` | `{ "error": "price_usd is required: what they pay each month." }` | Missing price. | | `400` | `{ "error": "Set a price of at least $50, your cost." }` | The price doesn’t cover their computers. | | `409` | `{ "error": "Set up payouts first." }` | Payouts aren’t set up. | | `409` | `{ "error": "Your client is already paying for this workspace." }` | Change the price instead. | | `404` | `{ "error": "Account not found." }` | No client with this id is yours. | | `409` | `{ "error": "This account is closed." }` | The client is closed. `code` is `ACCOUNT_CLOSED`. | | `409` | `{ "error": "This account was transferred to your client." }` | The client accepted a transfer and owns it now. `code` is `ACCOUNT_TRANSFERRED`. | --- # Change price Source: https://docs.orgo.ai/api-reference/clients/update-billing Endpoint: PATCH /v1/accounts/{id}/billing Summary: Change what a client pays. It applies from their next invoice. Send `price_usd`, `ai_usd`, or both. A paying client moves to the new amount on their next invoice, with no proration. A link not paid yet is replaced at the new price. Use your account-wide API key. A client’s own key gets `403` here. See [Invoicing](https://docs.orgo.ai/guides/invoicing). ## Path parameters - `id` (path, string, required) The client’s `id`. ## Body - `price_usd` (body, number) The new monthly price. - `ai_usd` (body, number) The new monthly AI amount. `0` removes it. ## Response - `billing` (object) As [Get billing](https://docs.orgo.ai/api-reference/clients/get-billing). ## Example ```bash cURL curl -X PATCH https://www.orgo.ai/api/v1/accounts/$CLIENT_ID/billing \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"price_usd": 300}' ``` ```python Python import os import requests client_id = os.environ["CLIENT_ID"] res = requests.patch( f"https://www.orgo.ai/api/v1/accounts/{client_id}/billing", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, json={"price_usd": 300}, ) print(res.json()) ``` ```javascript JavaScript const clientId = process.env.CLIENT_ID; const res = await fetch(`https://www.orgo.ai/api/v1/accounts/${clientId}/billing`, { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({"price_usd": 300}), }); console.log(await res.json()); ``` ### Response ```json { "billing": { "status": "active", "email": "billing@acme.com", "price_usd": 300, "setup_fee_usd": 0, "ai_usd": 40, "payment_link_url": null, "current_period_end": "2026-11-01T09:00:00Z", "ends_at_period_end": false } } ``` ## Errors | Status | Body | Meaning | |---|---|---| | `401` | `{ "error": "Invalid API key" }` | No key, or not one of yours. | | `403` | `{ "error": "Use your account-wide API key to manage accounts. Workspace keys can’t." }` | A client’s own key, or any key scoped to one workspace. | | `403` | `{ "error": "Invoicing is on every paid plan. Upgrade to use it." }` | You’re on the Free plan. `code` is `UPGRADE_REQUIRED`. | | `400` | `{ "error": "Send price_usd, ai_usd, or both." }` | Nothing to change. | | `409` | `{ "error": "This client isn’t being billed. Send them a link first." }` | You pay for this client. | | `404` | `{ "error": "Account not found." }` | No client with this id is yours. | | `409` | `{ "error": "This account is closed." }` | The client is closed. `code` is `ACCOUNT_CLOSED`. | | `409` | `{ "error": "This account was transferred to your client." }` | The client accepted a transfer and owns it now. `code` is `ACCOUNT_TRANSFERRED`. | --- # Stop billing Source: https://docs.orgo.ai/api-reference/clients/stop-billing Endpoint: DELETE /v1/accounts/{id}/billing Summary: Stop billing a client. A paid month runs to its end, or ends now with `now=true`. An unpaid link stops working at once. When billing ends, the client goes back on your plan. Use your account-wide API key. A client’s own key gets `403` here. See [Invoicing](https://docs.orgo.ai/guides/invoicing). ## Path parameters - `id` (path, string, required) The client’s `id`. ## Query parameters - `now` (query, boolean) `true` ends a paid subscription now instead of at the end of the month. ## Response - `billing` (object) With `ends_at_period_end` `true`, or `null` once it has ended. ## Example ```bash cURL curl -X DELETE https://www.orgo.ai/api/v1/accounts/$CLIENT_ID/billing?now=true \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os import requests client_id = os.environ["CLIENT_ID"] res = requests.delete( f"https://www.orgo.ai/api/v1/accounts/{client_id}/billing", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, params={"now": "true"}, ) print(res.json()) ``` ```javascript JavaScript const clientId = process.env.CLIENT_ID; const res = await fetch(`https://www.orgo.ai/api/v1/accounts/${clientId}/billing?now=true`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` }, }); console.log(await res.json()); ``` ### Response ```json { "billing": { "status": "active", "email": "billing@acme.com", "price_usd": 250, "setup_fee_usd": 0, "ai_usd": 40, "payment_link_url": null, "current_period_end": "2026-11-01T09:00:00Z", "ends_at_period_end": true } } ``` ## Errors | Status | Body | Meaning | |---|---|---| | `401` | `{ "error": "Invalid API key" }` | No key, or not one of yours. | | `403` | `{ "error": "Use your account-wide API key to manage accounts. Workspace keys can’t." }` | A client’s own key, or any key scoped to one workspace. | | `403` | `{ "error": "Invoicing is on every paid plan. Upgrade to use it." }` | You’re on the Free plan. `code` is `UPGRADE_REQUIRED`. | | `404` | `{ "error": "Account not found." }` | No client with this id is yours. | | `409` | `{ "error": "This account is closed." }` | The client is closed. `code` is `ACCOUNT_CLOSED`. | | `409` | `{ "error": "This account was transferred to your client." }` | The client accepted a transfer and owns it now. `code` is `ACCOUNT_TRANSFERRED`. | --- # Resend payment link Source: https://docs.orgo.ai/api-reference/clients/resend-payment-link Endpoint: POST /v1/accounts/{id}/billing/resend Summary: Email a payment link that hasn’t been paid again. Email a payment link that hasn’t been paid again. Use your account-wide API key. A client’s own key gets `403` here. See [Invoicing](https://docs.orgo.ai/guides/invoicing). ## Path parameters - `id` (path, string, required) The client’s `id`. ## Response - `sent` (boolean) `true`. ## Example ```bash cURL curl -X POST https://www.orgo.ai/api/v1/accounts/$CLIENT_ID/billing/resend \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os import requests client_id = os.environ["CLIENT_ID"] res = requests.post( f"https://www.orgo.ai/api/v1/accounts/{client_id}/billing/resend", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, ) print(res.json()) ``` ```javascript JavaScript const clientId = process.env.CLIENT_ID; const res = await fetch(`https://www.orgo.ai/api/v1/accounts/${clientId}/billing/resend`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` }, }); console.log(await res.json()); ``` ### Response ```json { "sent": true } ``` ## Errors | Status | Body | Meaning | |---|---|---| | `401` | `{ "error": "Invalid API key" }` | No key, or not one of yours. | | `403` | `{ "error": "Use your account-wide API key to manage accounts. Workspace keys can’t." }` | A client’s own key, or any key scoped to one workspace. | | `403` | `{ "error": "Invoicing is on every paid plan. Upgrade to use it." }` | You’re on the Free plan. `code` is `UPGRADE_REQUIRED`. | | `409` | `{ "error": "There’s no link waiting to be paid." }` | They paid it, or no link was sent. | | `404` | `{ "error": "Account not found." }` | No client with this id is yours. | | `409` | `{ "error": "This account is closed." }` | The client is closed. `code` is `ACCOUNT_CLOSED`. | | `409` | `{ "error": "This account was transferred to your client." }` | The client accepted a transfer and owns it now. `code` is `ACCOUNT_TRANSFERRED`. | --- # List payments Source: https://docs.orgo.ai/api-reference/clients/list-payments Endpoint: GET /v1/accounts/{id}/billing/payments Summary: A client’s recent payments, newest first. A client’s recent payments, newest first. Use your account-wide API key. A client’s own key gets `403` here. See [Invoicing](https://docs.orgo.ai/guides/invoicing). ## Path parameters - `id` (path, string, required) The client’s `id`. ## Response - `payments` (array) Recent payments. - `payments[].id` (string) The payment id. Use it to refund. - `payments[].date` (string) ISO 8601 timestamp. - `payments[].amount_usd` (number) What they paid. - `payments[].status` (string) `paid`, `refunded`, `partly_refunded`, `open`, or `failed`. ## Example ```bash cURL curl https://www.orgo.ai/api/v1/accounts/$CLIENT_ID/billing/payments \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os import requests client_id = os.environ["CLIENT_ID"] res = requests.get( f"https://www.orgo.ai/api/v1/accounts/{client_id}/billing/payments", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, ) print(res.json()) ``` ```javascript JavaScript const clientId = process.env.CLIENT_ID; const res = await fetch(`https://www.orgo.ai/api/v1/accounts/${clientId}/billing/payments`, { headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` }, }); console.log(await res.json()); ``` ### Response ```json { "payments": [ { "id": "in_1UMk8rBw6QMtXnMR", "date": "2026-10-01T09:00:00Z", "amount_usd": 290, "status": "paid" } ] } ``` ## Errors | Status | Body | Meaning | |---|---|---| | `401` | `{ "error": "Invalid API key" }` | No key, or not one of yours. | | `403` | `{ "error": "Use your account-wide API key to manage accounts. Workspace keys can’t." }` | A client’s own key, or any key scoped to one workspace. | | `403` | `{ "error": "Invoicing is on every paid plan. Upgrade to use it." }` | You’re on the Free plan. `code` is `UPGRADE_REQUIRED`. | | `404` | `{ "error": "Account not found." }` | No client with this id is yours. | --- # Refund a payment Source: https://docs.orgo.ai/api-reference/clients/refund-payment Endpoint: POST /v1/accounts/{id}/billing/payments/{payment_id}/refund Summary: Refund a client’s payment in full. The money comes back from your side, Orgo returns its fee, and the AI credits that month added are taken back. Use your account-wide API key. A client’s own key gets `403` here. See [Invoicing](https://docs.orgo.ai/guides/invoicing). ## Path parameters - `id` (path, string, required) The client’s `id`. - `payment_id` (path, string, required) The payment’s `id` from [List payments](https://docs.orgo.ai/api-reference/clients/list-payments). ## Response - `refunded` (boolean) `true`. ## Example ```bash cURL curl -X POST https://www.orgo.ai/api/v1/accounts/$CLIENT_ID/billing/payments/$PAYMENT_ID/refund \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os import requests client_id = os.environ["CLIENT_ID"] payment_id = os.environ["PAYMENT_ID"] res = requests.post( f"https://www.orgo.ai/api/v1/accounts/{client_id}/billing/payments/{payment_id}/refund", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, ) print(res.json()) ``` ```javascript JavaScript const clientId = process.env.CLIENT_ID; const paymentId = process.env.PAYMENT_ID; const res = await fetch(`https://www.orgo.ai/api/v1/accounts/${clientId}/billing/payments/${paymentId}/refund`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` }, }); console.log(await res.json()); ``` ### Response ```json { "refunded": true } ``` ## Errors | Status | Body | Meaning | |---|---|---| | `401` | `{ "error": "Invalid API key" }` | No key, or not one of yours. | | `403` | `{ "error": "Use your account-wide API key to manage accounts. Workspace keys can’t." }` | A client’s own key, or any key scoped to one workspace. | | `403` | `{ "error": "Invoicing is on every paid plan. Upgrade to use it." }` | You’re on the Free plan. `code` is `UPGRADE_REQUIRED`. | | `404` | `{ "error": "Payment not found." }` | Not one of this client’s payments. | | `409` | `{ "error": "That payment is already refunded." }` | Nothing left to refund. | | `409` | `{ "error": "That payment hasn’t gone through, so there’s nothing to refund." }` | It was never paid. | | `404` | `{ "error": "Account not found." }` | No client with this id is yours. | --- # Create chat completion Source: https://docs.orgo.ai/api-reference/chat/completions Endpoint: POST /v1/chat/completions Summary: Run an agent on a computer with one OpenAI-compatible request: models and the Computer Use default, images, step limits, streaming, billing, and every error. Send a task and the ID of a computer. An agent works on that computer until the task is done: it reads the screen, clicks, types, and runs commands. Then it returns its final message. The request and response follow OpenAI's chat completions format. Point an OpenAI SDK at `https://www.orgo.ai/api/v1` and add `computer_id` to the request. ```http POST https://www.orgo.ai/api/v1/chat/completions Authorization: Bearer $ORGO_API_KEY ``` ## Quick start Set `$ORGO_API_KEY` to your API key and `$COMPUTER_ID` to the `id` of a running computer, from [Create computer](https://docs.orgo.ai/api-reference/computers/create). ```bash cURL curl https://www.orgo.ai/api/v1/chat/completions \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "computer_id": "'"$COMPUTER_ID"'", "messages": [ {"role": "user", "content": "Open Chrome and search for the weather in Paris"} ] }' ``` ```python Python import os from openai import OpenAI client = OpenAI( base_url="https://www.orgo.ai/api/v1", api_key=os.environ["ORGO_API_KEY"], ) response = client.chat.completions.create( model="orgo-agent", messages=[ {"role": "user", "content": "Open Chrome and search for the weather in Paris"} ], # computer_id is an Orgo field. extra_body sends it as-is. extra_body={"computer_id": os.environ["COMPUTER_ID"]}, ) print(response.choices[0].message.content) print(response.orgo["thread_id"], response.orgo["steps"], response.orgo["cost_cents"]) ``` ```typescript TypeScript import OpenAI from "openai"; import type { ChatCompletionCreateParamsNonStreaming } from "openai/resources/chat/completions"; const client = new OpenAI({ baseURL: "https://www.orgo.ai/api/v1", apiKey: process.env.ORGO_API_KEY, }); // The SDK sends every field you pass. Its types do not declare Orgo's fields, // so add them to the params type. type OrgoFields = { computer_id: string; max_steps?: number; thread_id?: string }; const params: ChatCompletionCreateParamsNonStreaming & OrgoFields = { model: "orgo-agent", messages: [ { role: "user", content: "Open Chrome and search for the weather in Paris" }, ], computer_id: process.env.COMPUTER_ID!, }; const response = await client.chat.completions.create(params); console.log(response.choices[0].message.content); ``` The OpenAI SDKs require `model`, so the SDK examples name `orgo-agent`. With cURL you can leave `model` out and get the default, described in [Choose a model](#choose-a-model). ## Choose a model `model` is optional. Leave it out and Orgo picks one for the computer: | Request | Model that runs | |---------|-----------------| | No `model`, Linux, Windows, macOS, or iPhone computer | `orgo-agent` (Computer Use) | | No `model`, Android computer | `claude-sonnet-5` | | No `model`, with an `x-anthropic-key` header | `claude-sonnet-5` | | A named `model` | That model, always | The response's `model` field names the model that ran. | Model | What it is | |-------|------------| | `orgo-agent` | **Computer Use**: Orgo's computer-use engine on GPT-6 Astra, fast tier. The default on every computer except Android. It adapts its shell and shortcuts to the computer: PowerShell on Windows, Cmd shortcuts on macOS, and no shell on iPhone. | | `orgo-agent-opus-5.5` | The same engine on Claude Opus 5.5, standard tier. | | `claude-sonnet-5` | Claude Sonnet 5. The default on Android computers and with `x-anthropic-key`. | | `claude-opus-5.5` | Claude Opus 5.5. | | `claude-opus-5` | Claude Opus 5. | | `claude-opus-4.8` | Claude Opus 4.8. | | `claude-sonnet-4.6` | Claude Sonnet 4.6. | | `claude-opus-4.6` | Claude Opus 4.6. | Any other value returns `400` `invalid_model`, and the message lists the accepted values. Naming `orgo-agent` or `orgo-agent-opus-5.5` for an Android computer returns `400` `model_unsupported_os`. **Note:** Model IDs on this endpoint use a dot for point versions (`claude-opus-5.5`). Anthropic's own API uses a hyphen (`claude-opus-5-5`). Use the hyphenated form only when you call Anthropic directly, as in the [Claude Computer Use guide](https://docs.orgo.ai/guides/claude-computer-use). ## Request body - `computer_id` (body, string, required) The computer to drive: its UUID (the `id` from [Create computer](https://docs.orgo.ai/api-reference/computers/create)) or its instance id. You need write access to its workspace. Omitted, the request returns `400` `missing_computer_id`. - `messages` (body, array, required) The whole conversation, as `{ role, content }` objects. `content` is a string or an array of content parts, described in [Send text and images](#send-text-and-images). Messages with a role other than `user` or `assistant`, such as `system`, are dropped. The first remaining message must be from the user, or the request returns `400` `invalid_message_order`. No remaining message returns `400` `empty_messages`. The server does not load earlier turns from a thread. Resend the full transcript on every request, described in [Threads and history](#threads-and-history). - `model` (body, string) One of `orgo-agent`, `orgo-agent-opus-5.5`, `claude-sonnet-5`, `claude-opus-5.5`, `claude-opus-5`, `claude-opus-4.8`, `claude-sonnet-4.6`, or `claude-opus-4.6`. Optional. Omitted, the request runs `orgo-agent`, or `claude-sonnet-5` on an Android computer or when you send `x-anthropic-key`. See [Choose a model](#choose-a-model). - `stream` (body, boolean, default `false`) `true` streams the answer as Server-Sent Events, described in [Streaming](#streaming). Omitted or `false`, the response is one JSON body sent when the run ends. - `max_steps` (body, integer, default `250`) The most steps the run may take, from 250 to 500. Optional. Omitted, the limit is 250. A value above 500 runs with 500. A value below 250 runs with 250: this field can raise the limit but never lower it. No value is an error. See [Step limits](#step-limits). - `thread_id` (body, string) The UUID of a thread to write this turn into. Optional. Omitted, a new thread is created and its ID returned in `orgo.thread_id` and the `X-Thread-Id` header. A `thread_id` that is not a UUID, does not exist, or belongs to another user is ignored, and a new thread is created. It does not change what the model sees. See [Threads and history](#threads-and-history). ## Headers - `Authorization` (header, string, required) `Bearer $ORGO_API_KEY`. An account-wide key reaches every computer your account can. A workspace-scoped key reaches only computers in its own workspace. Get a key at [orgo.ai/settings/credentials](https://orgo.ai/settings/credentials). - `x-anthropic-key` (header, string) Your own Anthropic API key. It pays for Anthropic-served models only: every `claude-*` model and `orgo-agent-opus-5.5`. The run is then billed to your Anthropic account, not to Orgo credits, and `orgo.cost_cents` is `0`. With `orgo-agent` it is ignored and the run bills to Orgo credits. With no `model`, sending it runs `claude-sonnet-5`. ## Send text and images `content` is a plain string, or an array of OpenAI content parts: | Part | Shape | Where | |------|-------|-------| | Text | `{ "type": "text", "text": "…" }` | User and assistant messages | | Image | `{ "type": "image_url", "image_url": { "url": "…" } }` | User messages only | An image `url` is an `https://` URL, or a base64 data URL of a PNG, JPEG, GIF, or WebP image (`data:image/png;base64,…`). Both forms work with every model. Orgo passes an `https://` URL to the model provider, which fetches the image itself, so the URL must be publicly reachable. ```bash IMAGE=$(base64 < mockup.png | tr -d '\n') curl https://www.orgo.ai/api/v1/chat/completions \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "computer_id": "'"$COMPUTER_ID"'", "messages": [{ "role": "user", "content": [ {"type": "text", "text": "Build this page in a local HTML file and open it in Chrome."}, {"type": "image_url", "image_url": {"url": "data:image/png;base64,'"$IMAGE"'"}} ] }] }' ``` Any other part, an image in an assistant message, or an invalid image URL returns `400` `invalid_messages`. The `param` field names the part that failed: ```json { "error": { "type": "invalid_request", "message": "image_url must be an https URL or a base64 data URL (png, jpeg, gif or webp).", "code": "invalid_messages", "param": "messages[0].content[1]" } } ``` The index in `param` counts every message you sent, including dropped ones. ## Step limits A step is one model call and the computer actions it makes. A run takes at most 250 steps. Send `max_steps` to allow up to 500. A run that reaches its limit stops and returns normally, with: - `finish_reason` set to `"length"` instead of `"stop"`. - A final assistant message that ends with `Stopped after N steps. Send a message to keep going.` To continue, resend the transcript with a new user message. A run also stops early when it is about to spend more credits than the payer has, described in [Billing](#billing). That run ends with `finish_reason` `"stop"` and the message `Out of AI credits. The run was stopped.` ## Response - `id` (string) `chatcmpl-` followed by a 16-character opaque id. Also sent as the `X-Request-Id` header. Include it when you report a problem. - `object` (string) Always `chat.completion`. - `created` (integer) Unix time in seconds. - `model` (string) The model that ran. With no `model` in the request, `orgo-agent` or `claude-sonnet-5`. - `choices` (array) One choice. **choice** - `index` (integer): Always `0`. - `message` (object): `{ "role": "assistant", "content": string }`: the text of the agent's last message. Empty when the run ended without text. - `finish_reason` (string): `"length"` when the run hit its step limit. `"stop"` otherwise. - `usage` (object) Tokens across every step of the run, not only the last message. **usage** - `prompt_tokens` (integer): Input tokens, not counting cache reads and cache writes. - `completion_tokens` (integer): Output tokens. - `total_tokens` (integer): `prompt_tokens` plus `completion_tokens`. Cache reads and writes are billed but not counted here. - `orgo` (object) Orgo's fields. Not part of OpenAI's format, and not sent when streaming. **orgo** - `thread_id` (string | null): The thread this turn was written to. `null` when no thread could be created. - `steps` (integer): Steps the run took. - `cost_cents` (integer): What the run cost, in cents. `0` when `x-anthropic-key` paid. On Enterprise, the computed cost, though no credits are deducted. - `credit_balance_cents` (integer): The payer's credit balance after the hold and before settlement, so a later read differs. Absent when `x-anthropic-key` paid and on Enterprise. ```json { "id": "chatcmpl-VNJf55hQTfngVIm0", "object": "chat.completion", "created": 1791460800, "model": "orgo-agent", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "It is 18°C and sunny in Paris." }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 1240, "completion_tokens": 312, "total_tokens": 1552 }, "orgo": { "thread_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "steps": 6, "cost_cents": 4, "credit_balance_cents": 9616 } } ``` | Response header | Value | |-----------------|-------| | `X-Request-Id` | Same as `id`. | | `X-Thread-Id` | The thread ID. Absent when no thread could be created. | ## Streaming With `"stream": true` the response is `text/event-stream`. Each event is an OpenAI `chat.completion.chunk`, and the stream ends with `data: [DONE]`: ```text data: {"id":"chatcmpl-VNJf55hQTfngVIm0","object":"chat.completion.chunk","created":1791460800,"model":"orgo-agent","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]} data: {"id":"chatcmpl-VNJf55hQTfngVIm0","object":"chat.completion.chunk","created":1791460800,"model":"orgo-agent","choices":[{"index":0,"delta":{"content":"Opening Chrome"},"finish_reason":null}]} data: {"id":"chatcmpl-VNJf55hQTfngVIm0","object":"chat.completion.chunk","created":1791460800,"model":"orgo-agent","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":1240,"completion_tokens":312,"total_tokens":1552}} data: [DONE] ``` - **Only text streams.** Screenshots, clicks, and commands run on Orgo's side and are not sent. - **The last chunk carries `finish_reason`** (`"stop"` or `"length"`) and `usage`. - **No `orgo` block.** Read the thread ID from the `X-Thread-Id` header, which arrives before the first chunk. - **Closing the connection stops the run** at its next step. Steps already taken are billed. ```bash cURL curl -N https://www.orgo.ai/api/v1/chat/completions \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "computer_id": "'"$COMPUTER_ID"'", "messages": [{"role": "user", "content": "Open github.com"}], "stream": true }' ``` ```python Python import os from openai import OpenAI client = OpenAI( base_url="https://www.orgo.ai/api/v1", api_key=os.environ["ORGO_API_KEY"], ) stream = client.chat.completions.create( model="orgo-agent", messages=[{"role": "user", "content": "Open github.com"}], stream=True, extra_body={"computer_id": os.environ["COMPUTER_ID"]}, ) for chunk in stream: choice = chunk.choices[0] if choice.delta.content: print(choice.delta.content, end="", flush=True) if choice.finish_reason: print(f"\nfinish_reason: {choice.finish_reason}") ``` ```typescript TypeScript import OpenAI from "openai"; import type { ChatCompletionCreateParamsStreaming } from "openai/resources/chat/completions"; const client = new OpenAI({ baseURL: "https://www.orgo.ai/api/v1", apiKey: process.env.ORGO_API_KEY, }); type OrgoFields = { computer_id: string; max_steps?: number; thread_id?: string }; const params: ChatCompletionCreateParamsStreaming & OrgoFields = { model: "orgo-agent", messages: [{ role: "user", content: "Open github.com" }], stream: true, computer_id: process.env.COMPUTER_ID!, }; const stream = await client.chat.completions.create(params); for await (const chunk of stream) { const choice = chunk.choices[0]; if (choice.delta.content) process.stdout.write(choice.delta.content); if (choice.finish_reason) console.log(`\nfinish_reason: ${choice.finish_reason}`); } ``` ### Errors during a stream The status is already `200` when the run starts, so a failure mid-run cannot change it. The stream sends one error event and ends: ```text data: {"error":{"message":"…","type":"server_error"}} data: [DONE] ``` The OpenAI SDKs raise this as an `APIError`. If you read the stream yourself, treat a stream that ends without a chunk carrying `finish_reason` as failed. A run that fails mid-stream is billed for the tokens it used before failing. Its hold is refunded only when it used none. ## Threads and history **Warning:** `thread_id` does not do what its name suggests, and assuming it does loses data. - **History is not loaded.** The agent sees exactly the `messages` you send. A `thread_id` with only the new user turn gives the model no earlier context. - **Stored history is replaced, not appended.** When the run ends, the thread's stored messages are overwritten with this request's `messages` plus this turn's replies. Sending only the new turn erases the earlier turns from the thread. Keep the transcript yourself and resend all of it on every request. Pass the same `thread_id` each time so the stored copy stays complete. ```python import os from openai import OpenAI client = OpenAI(base_url="https://www.orgo.ai/api/v1", api_key=os.environ["ORGO_API_KEY"]) computer_id = os.environ["COMPUTER_ID"] history = [{"role": "user", "content": "Open Chrome and go to github.com"}] first = client.chat.completions.create( model="orgo-agent", messages=history, extra_body={"computer_id": computer_id}, ) thread_id = first.orgo["thread_id"] # Add this turn's answer, then the next question. history.append({"role": "assistant", "content": first.choices[0].message.content}) history.append({"role": "user", "content": "Search for 'orgo'"}) follow_up = client.chat.completions.create( model="orgo-agent", messages=history, # the full transcript, not only the new turn extra_body={"computer_id": computer_id, "thread_id": thread_id}, ) print(follow_up.choices[0].message.content) ``` Image parts are stored in the thread as the text `[image]`. To read or manage stored threads, see [Threads](https://docs.orgo.ai/api-reference/chat/threads). ## Bring your own Anthropic key Send `x-anthropic-key` to pay for an Anthropic-served model on your own Anthropic account. No Orgo credits are held or spent, and `orgo.cost_cents` is `0`. ```bash curl https://www.orgo.ai/api/v1/chat/completions \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "x-anthropic-key: $ANTHROPIC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-opus-5.5", "computer_id": "'"$COMPUTER_ID"'", "messages": [{"role": "user", "content": "Take a screenshot and describe the screen"}] }' ``` `orgo-agent` runs on OpenAI's GPT-6 Astra, so an Anthropic key cannot pay for it. A request that names `orgo-agent` bills to Orgo credits even when the header is sent. ## Billing Runs draw on the AI credits of the computer's workspace owner, not the caller. - **Rates.** A run costs the model provider's list price for the tokens it used, plus 10%, rounded up to a whole cent. `orgo.cost_cents` returns the result. - **Computer Use.** `orgo-agent` runs GPT-6 Astra on its fast tier, which bills at twice Astra's standard rates. `orgo-agent-opus-5.5` runs Claude Opus 5.5 at standard rates. - **Hold and settle.** A flat hold is placed when the request starts. It is settled against actual usage after the response is sent, so `orgo.credit_balance_cents` reflects the hold, not the final cost. - **Budget.** The run stops early when its running cost comes within 25 cents of the balance available at the start. When a workspace member's request spends the owner's credits, that request may use at most $5. - **No hold** is placed when `x-anthropic-key` pays, or on Enterprise. See your balance and add credits at [orgo.ai/settings/billing](https://orgo.ai/settings/billing). ## Errors | Status | Code | When | |--------|------|------| | 400 | `invalid_json` | The body is not valid JSON. | | 400 | `invalid_model` | `model` is not an accepted value, listed in [Choose a model](#choose-a-model). The message lists the accepted values. | | 400 | `missing_computer_id` | No `computer_id`. | | 400 | `model_unsupported_os` | `orgo-agent` or `orgo-agent-opus-5.5` was named for an Android computer. Name another model, or leave `model` out. | | 400 | `invalid_messages` | `messages` is not an array, a `content` is neither a string nor an array, or a content part is not valid, described in [Send text and images](#send-text-and-images). `param` names the field. | | 400 | `empty_messages` | No `user` or `assistant` message remains. | | 400 | `invalid_message_order` | The first `user` or `assistant` message is not from the user. | | 400 | `context_overflow` | The conversation is larger than the model's context window. Shorten the transcript you resend. Non-streaming only. | | 401 | `invalid_api_key` | Every authentication and access failure: a missing or unknown key; a `computer_id` or `thread_id` in a workspace you cannot access; view-only access to the computer's workspace; or a workspace-scoped key naming a computer outside its workspace. The message is always `Invalid API key. Get yours at https://orgo.ai/settings/credentials`. | | 402 | `credits_exhausted` | Not enough AI credits to place the hold. The body carries `balance_cents`. Add credits at [orgo.ai/settings/billing](https://orgo.ai/settings/billing). | | 402 | `account_ai_limit` | The workspace is a [client account](https://docs.orgo.ai/api-reference/clients/create) at its monthly AI limit. `balance_cents` is `0`. | | 403 | `model_proxy_disabled` | This endpoint is turned off for the account. Call your model provider directly and use the Orgo API for computer actions only. | | 404 | `computer_not_found` | No computer matches `computer_id`, or it can no longer run: an expired free trial, or a computer paid for by its own subscription or dedicated purchase whose payment has lapsed. | | 500 | `internal_error` | Unexpected server error. Steps the run took before failing are billed. Retry with exponential backoff, and include `X-Request-Id` when you report it. | | 503 | `model_not_configured` | `orgo-agent` or `orgo-agent-opus-5.5` is not available right now. Retry later, or name a `claude-*` model. | Every error has this shape: ```json { "error": { "type": "invalid_request", "message": "computer_id is required. Pass the ID of a running Orgo computer.", "code": "missing_computer_id" } } ``` `type` is one of `invalid_request`, `authentication_error`, `insufficient_credits`, `permission_error`, `not_found`, or `server_error`. Branch on `code`. `invalid_messages` adds `param`, and `402` adds `balance_cents`. Once a stream has started, failures arrive as an error event instead of a status, described in [Errors during a stream](#errors-during-a-stream). --- # Threads Source: https://docs.orgo.ai/api-reference/chat/threads Summary: Stored transcripts for chat completions. A thread is a stored transcript attached to one computer. Threads are what the dashboard reads to show a conversation, and what [Create chat completion](https://docs.orgo.ai/api-reference/chat/completions) writes each turn into. **Warning:** A thread is a record, not a memory. Passing a `thread_id` to `POST /v1/chat/completions` does **not** replay the thread's messages to the model. Each completion **replaces** the thread's stored messages with that request's transcript rather than appending to it. Keep the conversation client-side and resend it in full on every completion. See [Threads and history](https://docs.orgo.ai/api-reference/chat/completions#threads-and-history). There are two thread surfaces, and they are not interchangeable. | Surface | Use it for | Identifier it accepts | Error shape | |---------|-----------|-----------------------|-------------| | `/v1/threads` | Programmatic access alongside `/v1/chat/completions`. | Computer UUID or instance id. | `{ "error": { "type", "message", "code" } }` | | `/chat/threads` | The surface the Orgo dashboard uses. Returns assistant-ui-shaped fields. | Computer UUID only. | `{ "error": "message" }` | **Base URL:** `https://www.orgo.ai/api` **Auth:** `Authorization: Bearer $ORGO_API_KEY` on every request. Both surfaces require an account-wide key. A workspace-scoped key is refused on every thread endpoint. --- ## The `/v1` surface | Method | Path | Purpose | |--------|------|---------| | `GET` | `/v1/threads?computer_id={id}` | List threads on a computer. | | `POST` | `/v1/threads` | Create an empty thread. | | `GET` | `/v1/threads/{thread_id}` | Get a thread with its messages. | | `DELETE` | `/v1/threads/{thread_id}` | Delete a thread. | ### List threads ```http GET /v1/threads?computer_id={id} ``` Returns every thread stored against the computer. Threads are scoped to the computer's workspace, not to whoever created them, so every workspace member sees the same list. Message bodies are not included, only a count. - `computer_id` (query, string, required) The computer's UUID or its instance id. Omitting it returns 400 `missing_computer_id`. ```json { "object": "list", "data": [ { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "object": "thread", "created_at": "2026-04-20T14:22:05.123Z", "title": "GitHub search", "message_count": 4 } ] } ``` - `object` (string): Always `list`. - `data` (array) Thread summaries, most recently updated first. **thread** - `id` (string): Thread UUID. - `object` (string): Always `thread`. - `created_at` (string): ISO 8601 creation timestamp. - `title` (string): Title, or `null` if none has been generated. - `message_count` (integer): Number of stored messages. ```bash curl "https://www.orgo.ai/api/v1/threads?computer_id=$COMPUTER_ID" \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ### Create thread ```http POST /v1/threads ``` Creates an empty thread bound to a computer, and returns `201 Created`. You rarely need this. A completion with no `thread_id` creates one for you. - `computer_id` (body, string, required) The computer's UUID or its instance id. Omitting it returns 400 `missing_computer_id`. ```json { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "object": "thread", "created_at": "2026-04-20T14:22:05.123Z" } ``` ```bash curl -X POST https://www.orgo.ai/api/v1/threads \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"computer_id": "'"$COMPUTER_ID"'"}' ``` ### Get thread ```http GET /v1/threads/{thread_id} ``` Returns the thread with its full stored transcript. On this surface a thread is readable only by the account that created it. A workspace teammate gets 404, not 403. A thread id that is not a UUID returns 500. ```json { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "object": "thread", "created_at": "2026-04-20T14:22:05.123Z", "title": "GitHub search", "messages": [ { "role": "user", "content": "Open github.com" }, { "role": "assistant", "content": "Done. GitHub is open." } ] } ``` ### Delete thread ```http DELETE /v1/threads/{thread_id} ``` Permanently deletes the thread and its messages. Creator only, like `GET`. The creator also needs write access to the computer's workspace. A view-only member gets 401 even for a thread they created. ```json { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "object": "thread.deleted", "deleted": true } ``` ### Errors | Status | Code | When | |--------|------|------| | 400 | `missing_computer_id` | `computer_id` was not supplied on list or create. | | 401 | `invalid_api_key` | Every authentication or access failure: a missing or unknown key, a workspace-scoped key, a computer or thread in a workspace you cannot access, or view-only access on `POST` and `DELETE`. This surface returns one body for all of them, with `type` `authentication_error` and the message `Invalid API key.` | | 403 | `forbidden` | On list or create: no computer matches `computer_id`, or it can no longer run (a free trial computer that has expired, or a computer paid for by its own subscription or dedicated purchase whose payment has lapsed). | | 403 | `model_proxy_disabled` | The OpenAI-compatible endpoint is switched off for this account, so list, create, and get are refused. `DELETE` is deliberately exempt, so you can always remove what is held. | | 404 | `thread_not_found` | The thread does not exist, or you are not the account that created it. | | 500 | None | Unexpected server error. Also returned for a malformed JSON body on `POST` and for a thread id that is not a UUID. Neither is caught, so neither carries the `error` envelope below. | ```json { "error": { "type": "not_found", "message": "Thread not found.", "code": "thread_not_found" } } ``` `type` is one of `invalid_request`, `authentication_error`, `permission_error`, or `not_found`. --- ## The `/chat` surface | Method | Path | Purpose | |--------|------|---------| | `GET` | `/chat/threads?desktopId={uuid}` | List threads with full message history. | | `POST` | `/chat/threads` | Create an empty thread. | | `GET` | `/chat/threads/{thread_id}` | Get a thread with full message history. | | `PATCH` | `/chat/threads/{thread_id}` | Update title, replace messages, archive, or unarchive. | | `DELETE` | `/chat/threads/{thread_id}` | Delete a thread. | | `POST` | `/chat/threads/{thread_id}/title` | Generate a title from the first messages. | | `GET` | `/chat/threads/{thread_id}/run` | Whether a dashboard chat run is in progress on this thread. | | `DELETE` | `/chat/threads/{thread_id}/run` | Stop that run. | Access on this surface is by workspace, not by author. Any member of the computer's workspace can read its threads. Creating, updating, deleting, and generating a title need write access to the workspace, so a view-only member gets 403. The `/run` endpoints are the exception and are restricted to the thread's creator. ### List threads ```http GET /chat/threads?desktopId={uuid} ``` Returns every thread on the given computer that you can see, including full message history. Archived threads are returned too; filter on `status` client-side if you want to hide them. - `desktopId` (query, string, required) The computer's UUID. This surface does not accept an instance id: a non-UUID value returns 500. Omitting it returns 400. - `threads` (array) Thread objects, most recently updated first. **thread** - `id` (string): Thread UUID. - `remoteId` (string): Same as `id`. Kept for compatibility with assistant-ui clients. - `status` (string): `regular` or `archived`. A newly created thread is `regular`. - `title` (string): Title. Omitted if none has been generated. - `messages` (array): Full message history, shaped as `[{ role, content }, …]`. Empty for a new thread. - `updated_at` (string): ISO 8601 timestamp of the last modification. ```python Python import os, requests r = requests.get( "https://www.orgo.ai/api/chat/threads", params={"desktopId": os.environ["COMPUTER_ID"]}, headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, ) for t in r.json()["threads"]: print(t["id"], t["status"], t.get("title", "(untitled)")) ``` ```typescript TypeScript const r = await fetch( `https://www.orgo.ai/api/chat/threads?desktopId=${process.env.COMPUTER_ID}`, { headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` } }, ); const { threads } = await r.json(); ``` ```bash cURL curl "https://www.orgo.ai/api/chat/threads?desktopId=$COMPUTER_ID" \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```json { "threads": [ { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "remoteId": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "status": "regular", "title": "GitHub search", "messages": [ { "role": "user", "content": "Open github.com" }, { "role": "assistant", "content": "Done. GitHub is open." } ], "updated_at": "2026-04-20T14:22:05.123Z" } ] } ``` ### Create thread ```http POST /chat/threads ``` Creates an empty thread bound to a computer, and returns `201 Created`. - `desktopId` (body, string, required) The computer's UUID. Omitting it returns 400. - `localId` (body, string) Client-side identifier. Echoed back as `externalId` so clients can reconcile local and remote threads. When omitted, `externalId` is absent from the response. - `remoteId` (string): The new thread's UUID. Use it as `thread_id` in later chat completions. - `externalId` (string): Mirror of `localId`. Omitted if not supplied. ```bash curl -X POST https://www.orgo.ai/api/chat/threads \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"desktopId": "'"$COMPUTER_ID"'"}' ``` ### Get thread ```http GET /chat/threads/{thread_id} ``` Fetches a single thread with its full message history. - `remoteId` (string): Thread UUID. - `status` (string): `regular` or `archived`. - `title` (string): Title. Omitted if none has been generated. - `messages` (array): Full message history in chronological order. ### Update thread ```http PATCH /chat/threads/{thread_id} ``` Updates the title, replaces the messages, or toggles the archive state. Only one thing happens per request: `archive: true` is applied first, otherwise `unarchive: true`, otherwise `title` and `messages` together. A body with none of these fields is accepted and changes nothing. - `title` (body, string) New title. - `messages` (body, array) Replaces the entire stored message history with this array. A full overwrite, not an append. - `archive` (body, boolean) `true` sets `status` to `archived`. Nothing else changes: archived threads still appear in the list response, so filter on `status` client-side. Any other value is ignored. - `unarchive` (body, boolean) `true` sets `status` back to `regular`. Ignored when `archive: true` is also present. Any other value is ignored. - `remoteId` (string): Thread UUID. - `status` (string): Updated status: `regular` or `archived`. - `title` (string): Updated title. Omitted if unset. ```bash curl -X PATCH https://www.orgo.ai/api/chat/threads/7c9e6679-7425-40de-944b-e07fc1f90ae7 \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"title": "GitHub research session"}' ``` ### Delete thread ```http DELETE /chat/threads/{thread_id} ``` Permanently deletes the thread and its message history. Prefer `PATCH` with `archive: true` if you might need the conversation back. ```json { "success": true } ``` ### Generate title ```http POST /chat/threads/{thread_id}/title ``` Asks Claude Haiku for a title of three to six words and saves it to the thread. Returns an assistant-ui text stream rather than JSON. The generation is metered against your credits but never blocks: an out-of-credit account still gets a title. - `messages` (body, array, required) The messages to summarize. Only the first three are read, and the joined text is truncated to 1000 characters. Each message is `{ role, content }` where `content` may be a string or an array of `{ type: "text", text }` blocks. A missing or empty array returns 400. Response is `text/plain`: ```text 0:"GitHub search session" ``` The title is also persisted, so a later `GET` returns it in `title`. ### Run state ```http GET /chat/threads/{thread_id}/run DELETE /chat/threads/{thread_id}/run ``` `GET` reports whether a run is in progress on this thread right now. Use it to decide between rendering a finished transcript and re-attaching to a live one. **Warning:** Only runs started from the Orgo dashboard's chat are tracked here. A [`/v1/chat/completions`](https://docs.orgo.ai/api-reference/chat/completions) request is never registered, so `GET` reports `running: false` for it and `DELETE` cannot stop it. ```json { "running": true, "startedAt": 1745136000000, "seq": 12 } ``` ```json { "running": false } ``` **Note:** `running` answers for the single server process that handled your request. With several processes behind the load balancer, a run in progress elsewhere reports `running: false`. Treat `true` as reliable and `false` as "not here". `DELETE` stops a run you started. It returns 200 in both outcomes: ```json { "stopped": true } ``` ```json { "stopped": false, "reason": "not running on this instance" } ``` Both `/run` methods are restricted to the thread's creator; a workspace teammate gets 403. ### Errors | Status | Meaning | |--------|---------| | 400 | A required field is missing: `desktopId` on list and create, `messages` on title generation. | | 401 | The Bearer token starts with `sk_` but is not a known Orgo key: `{ "error": "Invalid API key" }`. A request with no `Authorization` header returns `{ "error": "Authentication required" }`. On `/run`, the access failures listed under 403 are returned as 401 instead. | | 403 | You are not a member of the computer's workspace (`{ "error": "You do not have access to this workspace." }`), you have view-only access and the request writes (`{ "error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)." }`), or the key is workspace-scoped (`{ "error": "This endpoint requires an account-wide credential (workspace_scope_mismatch)." }`). On `/run`, also returned when the thread or the run belongs to someone else. On list and create, `{ "error": "Access denied" }` means no computer has that `desktopId`. | | 404 | The thread does not exist. | | 500 | Unexpected server error. Also returned for a malformed JSON body, and for a `desktopId` or thread id that is not a UUID. Retry with backoff. | | 503 | Orgo could not verify the credential because of a server-side fault: `{ "error": "Service temporarily unavailable. …" }`, with `Retry-After: 5`. On `/run` the same body comes back as 401. Retry either way. | Error responses on this surface carry a single string `error` field: ```json { "error": "desktopId is required" } ``` --- ## Using threads with completions ```python history = [{"role": "user", "content": "Open Chrome and go to github.com"}] # First turn: the server creates the thread and returns its ID. first = client.chat.completions.create( model="claude-sonnet-4.6", messages=history, extra_body={"computer_id": computer_id}, ) thread_id = first.orgo["thread_id"] history.append({"role": "assistant", "content": first.choices[0].message.content}) history.append({"role": "user", "content": "Search for 'orgo'"}) # Later turn: resend the whole transcript, and reuse the thread ID so the # stored copy stays complete. client.chat.completions.create( model="claude-sonnet-4.6", messages=history, extra_body={"computer_id": computer_id, "thread_id": thread_id}, ) ``` --- # Create workspace Source: https://docs.orgo.ai/api-reference/workspaces/create Endpoint: POST /workspaces Summary: Create a new workspace to organize computers. Creates a workspace and returns it with `201`. The calling account becomes the owner. **Info:** Workspaces are containers for computers. Use them to separate projects, environments, or teams. Creating a workspace needs an account-wide API key. A workspace-scoped key gets `403`. See [Authentication](https://docs.orgo.ai/api-reference/authentication). ## Request - `name` (body, string, required) Workspace name. Leading and trailing whitespace is trimmed. The name must not match one you already own, compared case-insensitively. Names of workspaces shared with you do not conflict. Missing, non-string, or whitespace-only values are rejected with `400`. - `icon_url` (body, string) Optional URL of an icon for the workspace. Stored as given and not validated. Omit it and the workspace is created with `icon_url: null`. - `status` (body, string, default `active`) Optional. Defaults to `active` when omitted. The product understands `active` and `inactive`. The field is stored as given and is not validated against that list. ## Response - `id` (string) Workspace identifier. Use it as `workspace_id` when creating computers. - `name` (string) Workspace name, trimmed. - `user_id` (string) User ID of the owner, which is your account. - `status` (string) Workspace status. - `icon_url` (string) Icon URL, or `null` when none was sent. - `created_at` (string) ISO 8601 timestamp. The create response does not include `updated_at`, `owner_tier`, `owner_email`, or `desktops`. [Get workspace](https://docs.orgo.ai/api-reference/workspaces/get) returns those. ## Example ```bash cURL curl -X POST https://www.orgo.ai/api/workspaces \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "production"}' ``` ```python Python import os import requests api_key = os.environ["ORGO_API_KEY"] response = requests.post( "https://www.orgo.ai/api/workspaces", headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" }, json={"name": "production"} ) workspace = response.json() print(f"Created: {workspace['id']}") ``` ```javascript JavaScript const apiKey = process.env.ORGO_API_KEY; const response = await fetch('https://www.orgo.ai/api/workspaces', { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'production' }) }); const workspace = await response.json(); console.log(`Created: ${workspace.id}`); ``` ### Response ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "production", "user_id": "4d96f9a0-7727-4b63-889a-32544c206d7c", "status": "active", "icon_url": null, "created_at": "2026-04-07T10:30:00Z" } ``` ## Errors | Status | Body | Meaning | |---|---|---| | `400` | `{ "error": "Invalid request body" }` | The body was not valid JSON. | | `400` | `{ "error": "Project name is required" }` | `name` was missing, not a string, or whitespace only. | | `400` | `{ "error": "A project with this name already exists" }` | You already own a workspace with that name, compared case-insensitively. | | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `403` | `{ "error": "Workspace-scoped keys cannot create workspaces (workspace_scope_mismatch)." }` | The API key is scoped to a workspace. Use an account-wide key. | | `403` | `{ "error": "Sign up to create your own workspace.", "code": "GUEST_RESTRICTED" }` | The caller joined through a share link. Guests can work in the workspace they were invited to but cannot create their own. | | `500` | `{ "error": "Could not verify account", "code": "AUTH_LOOKUP_FAILED" }` | The account lookup that gates guests failed. Retry. | | `500` | `{ "error": "" }` | Unexpected server error. | | `503` | `{ "error": "Service temporarily unavailable. …" }` | Orgo could not verify the credential because of a server-side fault. The response carries `Retry-After: 5`. Retry after that delay. | Some error bodies on this endpoint carry a `code` alongside `error`. Branch on `code` where it is present. It is stable, and the `error` string is not. --- # List workspaces Source: https://docs.orgo.ai/api-reference/workspaces/list Endpoint: GET /workspaces Summary: List all workspaces accessible to the API key. Returns every workspace you own or belong to, each with its computers under `desktops`. Workspaces you own come first, oldest first, followed by workspaces shared with you. **Warning:** This call is not read-only. If the account has no workspaces yet, the first call with an account-wide key creates one named `'s workspace` and returns it. The name comes from your profile name, or from the part of your email before the `@` when no profile name is set. A workspace-scoped API key sees only the workspace it is scoped to, and never triggers the default-workspace creation. See [Authentication](https://docs.orgo.ai/api-reference/authentication). ## Response The same array is returned under two keys. - `workspaces` (array) Array of workspace objects, each with its `desktops`. Read this key. - `projects` (array) The identical array. `project` is the API's original name for a workspace. The key is kept for existing integrations. Do not read it in new code. For an account on the free plan, the body can also carry a `trial_snapshot` object. The dashboard reads it. It is not part of the documented contract. Each workspace object carries the fields listed on [Get workspace](https://docs.orgo.ai/api-reference/workspaces/get), including `role` and `permissions`, plus: - `member_count` (integer) People with access to the workspace, counting the owner. Returned by this endpoint only. - `owner_tier` (string) Subscription tier of the workspace owner. Computers are funded by the owner's plan, so this is the tier that sets the hardware ceiling for every computer in the workspace, including computers created by invited members. - `owner_email` (string) Email address of the workspace owner. Returned to every member of the workspace, not only the owner. - `desktops` (array) Computers in the workspace, in no guaranteed order. Ephemeral computers are excluded. Each entry is the stored computer record with credential fields removed, plus the same `permissions` object, so it carries more fields than the example below. Treat any field not documented here as internal and subject to change. ## Example ```bash cURL curl https://www.orgo.ai/api/workspaces \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os import requests api_key = os.environ["ORGO_API_KEY"] response = requests.get( "https://www.orgo.ai/api/workspaces", headers={"Authorization": f"Bearer {api_key}"} ) workspaces = response.json()["workspaces"] for ws in workspaces: n = len(ws.get("desktops") or []) print(f"{ws['name']}: {n} computers") ``` ```javascript JavaScript const apiKey = process.env.ORGO_API_KEY; const response = await fetch('https://www.orgo.ai/api/workspaces', { headers: { 'Authorization': `Bearer ${apiKey}` } }); const { workspaces } = await response.json(); workspaces.forEach(ws => { const n = (ws.desktops || []).length; console.log(`${ws.name}: ${n} computers`); }); ``` ### Response The response repeats the same array under `projects`, omitted here for length. ```json { "workspaces": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "production", "user_id": "4d96f9a0-7727-4b63-889a-32544c206d7c", "status": "active", "icon_url": null, "created_at": "2026-04-07T10:30:00Z", "updated_at": "2026-04-07T10:30:00Z", "member_count": 3, "owner_tier": "hacker", "owner_email": "owner@example.com", "role": "owner", "permissions": { "canView": true, "canWrite": true, "canManageAccess": true }, "desktops": [ { "id": "a3bb189e-8bf9-3888-9912-ace4e6543002", "name": "agent-1", "os": "linux", "ram": 4, "cpu": 1, "status": "running", "permissions": { "canView": true, "canWrite": true, "canManageAccess": true } } ] } ] } ``` ## Errors | Status | Body | Meaning | |---|---|---| | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `500` | `{ "error": "" }` | The listing query failed, or the default-workspace creation failed. | | `503` | `{ "error": "Service temporarily unavailable. …" }` | Orgo could not verify the credential because of a server-side fault. The response carries `Retry-After: 5`. Retry after that delay. | There is no `403` on this endpoint. It never rejects a caller who authenticated. --- # Get workspace Source: https://docs.orgo.ai/api-reference/workspaces/get Endpoint: GET /workspaces/{id} Summary: Retrieve a workspace by ID. Returns one workspace by ID, including its computers under `desktops`. Owners and invited members both have read access. A workspace-scoped API key can read only the workspace it is scoped to. Any other ID returns `403`. See [Authentication](https://docs.orgo.ai/api-reference/authentication). ## Path parameters - `id` (path, string, required) Workspace ID. Required. ## Response - `id` (string) Workspace identifier. - `name` (string) Workspace name. - `user_id` (string) User ID of the workspace owner. - `status` (string) Workspace status. `active` for a normal workspace. - `icon_url` (string) Icon URL, or `null` when none is set. - `created_at` (string) ISO 8601 timestamp. - `updated_at` (string) ISO 8601 timestamp. - `owner_tier` (string) Subscription tier of the workspace owner. Computers are funded by the owner's plan, so this is the tier that sets the hardware ceiling for every computer in the workspace. - `owner_email` (string) Email address of the workspace owner. Returned to every member of the workspace, not only the owner. - `role` (string) Your role in the workspace: `owner`, `admin` (can edit), or `member` (view only). - `permissions` (object) What your role allows, as three booleans: `canView`, `canWrite`, and `canManageAccess`. Only the owner has `canManageAccess`. - `desktops` (array) Computers in the workspace, in no guaranteed order. Ephemeral computers are excluded. Each entry is the stored computer record with credential fields removed, plus the same `permissions` object. It carries more fields than the example below. Treat any field not documented here as internal and subject to change. `member_count` is returned by [List workspaces](https://docs.orgo.ai/api-reference/workspaces/list) only, not by this endpoint. ## Example ```bash cURL curl https://www.orgo.ai/api/workspaces/$WORKSPACE_ID \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os import requests api_key = os.environ["ORGO_API_KEY"] workspace_id = os.environ["WORKSPACE_ID"] response = requests.get( f"https://www.orgo.ai/api/workspaces/{workspace_id}", headers={"Authorization": f"Bearer {api_key}"} ) workspace = response.json() print(f"Workspace: {workspace['name']}") for d in workspace.get("desktops", []): print(f" - {d['name']}: {d['status']}") ``` ```javascript JavaScript const apiKey = process.env.ORGO_API_KEY; const workspaceId = process.env.WORKSPACE_ID; const response = await fetch(`https://www.orgo.ai/api/workspaces/${workspaceId}`, { headers: { 'Authorization': `Bearer ${apiKey}` } }); const workspace = await response.json(); console.log(`Workspace: ${workspace.name}`); workspace.desktops?.forEach(d => { console.log(` - ${d.name}: ${d.status}`); }); ``` ### Response ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "production", "user_id": "4d96f9a0-7727-4b63-889a-32544c206d7c", "status": "active", "icon_url": null, "created_at": "2026-04-07T10:30:00Z", "updated_at": "2026-04-07T10:30:00Z", "owner_tier": "hacker", "owner_email": "owner@example.com", "role": "owner", "permissions": { "canView": true, "canWrite": true, "canManageAccess": true }, "desktops": [ { "id": "a3bb189e-8bf9-3888-9912-ace4e6543002", "name": "agent-1", "os": "linux", "ram": 4, "cpu": 1, "status": "running", "permissions": { "canView": true, "canWrite": true, "canManageAccess": true } }, { "id": "b2c3d4e5-f6a7-8901-bcde-f23456789012", "name": "agent-2", "os": "linux", "ram": 8, "cpu": 2, "status": "stopped", "permissions": { "canView": true, "canWrite": true, "canManageAccess": true } } ] } ``` ## Errors | Status | Body | Meaning | |---|---|---| | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `403` | `{ "error": "You do not have access to this workspace." }` | Your account neither owns nor belongs to the workspace, or no workspace has that ID. An unknown ID returns this `403`, not `404`. | | `403` | `{ "error": "This API key cannot access this workspace (workspace_scope_mismatch)." }` | A workspace-scoped key requested a different workspace. | | `500` | `{ "error": "" }` | Unexpected server error. | | `503` | `{ "error": "Service temporarily unavailable. …" }` | Orgo could not verify the credential because of a server-side fault. The response carries `Retry-After: 5`. Retry after that delay. | --- # Delete workspace Source: https://docs.orgo.ai/api-reference/workspaces/delete Endpoint: POST /workspaces/{id}/delete Summary: Delete a workspace and all its computers. Deletes a workspace and every computer in it. Only the workspace owner can call this. An invited member gets `401` or `403`, described in [Errors](#errors). The verb is `POST` and the path ends in `/delete`. There is no `DELETE` handler on `/workspaces/{id}`. Sending `DELETE` there returns `405`. **Warning:** This cannot be undone. Every computer in the workspace is terminated and its disk destroyed. ## Order of operations The endpoint runs these steps in this order: 1. Checks that you own the workspace. 2. Refuses with `409`, before touching anything, when the workspace holds a device or dedicated machine that Orgo manages, has a dedicated computer purchase, has hourly or dedicated computer billing records, has an OpenClaw enrollment, or when another operation is already working on it. 3. Terminates each computer on its host, one at a time. A host that answers `404`, or a computer with no recorded host address, does not stop the delete. A failed termination does not stop the loop either: every remaining computer is still tried. 4. Returns `500` if any termination failed. The workspace, its file records, and the computers that were not terminated stay in place. 5. Deletes the workspace's file records and the workspace row together. The database cascade then removes the computer records. **Warning:** A `500` does not mean the workspace is intact. Every computer whose termination succeeded is already destroyed, including any after the one that failed. A retry does not re-send the failed terminations. It returns `409` with `teardown_reconciliation_required` until Orgo support confirms what happened to them. Contact support to finish the delete. ## Path parameters - `id` (path, string, required) Workspace ID. Required. The request body is ignored. ## Response - `success` (boolean) `true` when the workspace and all of its computers were deleted. The endpoint never returns `false`. A failure comes back as a `4xx` or `5xx` instead. - `message` (string) Human-readable confirmation. Currently `"Project deleted successfully"`. ## Example ```bash cURL curl -X POST https://www.orgo.ai/api/workspaces/$WORKSPACE_ID/delete \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os import requests api_key = os.environ["ORGO_API_KEY"] workspace_id = os.environ["WORKSPACE_ID"] response = requests.post( f"https://www.orgo.ai/api/workspaces/{workspace_id}/delete", headers={"Authorization": f"Bearer {api_key}"} ) if response.json().get("success"): print("Workspace deleted") ``` ```javascript JavaScript const apiKey = process.env.ORGO_API_KEY; const workspaceId = process.env.WORKSPACE_ID; const response = await fetch( `https://www.orgo.ai/api/workspaces/${workspaceId}/delete`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}` } } ); const { success } = await response.json(); if (success) console.log('Workspace deleted'); ``` ### Response ```json { "success": true, "message": "Project deleted successfully" } ``` ## Errors | Status | Body | Meaning | |---|---|---| | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `401` | `{ "error": "You do not have access to this workspace." }` | Your account neither owns nor belongs to the workspace, or no workspace has that ID. An unknown ID returns this `401`, not `404`. | | `401` | `{ "error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)." }` | You are a view-only member of the workspace. | | `401` | `{ "error": "This API key cannot access this workspace (workspace_scope_mismatch)." }` | A workspace-scoped key targeted a different workspace. | | `403` | `{ "error": "Only project owner can delete project" }` | You are a member with edit access, not the owner. | | `409` | `{ "error": "workspace_holds_device", "message": "…" }` | The workspace holds a Mac, an iPhone, or a dedicated machine that Orgo manages. Ask support to release it first. Nothing was deleted. | | `409` | `{ "error": "compute_allocation_active", "detail": "…" }` | The workspace has a dedicated computer purchase on record. Manage it in Account → Usage first. Nothing was deleted. | | `409` | `{ "error": "compute_billing_records", "detail": "…" }` | The workspace has hourly or dedicated computer billing records that Orgo keeps. Contact support to delete it. Nothing was deleted, unless the records appeared while the delete was running: then the computers it already shut down stay shut down. | | `409` | `{ "error": "openclaw_enrollment_cleanup_required", "detail": "…" }` | The workspace has an OpenClaw enrollment. Revoke it first. Nothing was deleted. | | `409` | `{ "error": "teardown_in_flight", "detail": "…" }` | Another operation is already working on this workspace. Retry in a moment. | | `409` | `{ "error": "teardown_unavailable", "detail": "…" }` | The delete could not start right now. Retry in a moment. | | `409` | `{ "error": "teardown_membership_changed", "detail": "…" }` | A computer was added to the workspace while it was being deleted. Remove it, then retry. | | `409` | `{ "error": "teardown_reconciliation_required", "detail": "…" }` | An earlier attempt could not confirm that every computer shut down. Support has to reconcile it before the delete can finish. | | `500` | `{ "error": "Refusing to delete project. …" }` | One or more computers could not be terminated, so the workspace was not deleted. **Deletion is already partly done:** every computer whose termination succeeded is destroyed. See [Order of operations](#order-of-operations). | | `500` | `{ "error": "Failed to delete workspace" }` | Unexpected server error. | This endpoint answers every failed authorization check with `401`, including a valid key that lacks access. A server-side fault while verifying the credential also comes back as `401`, with `Service temporarily unavailable. …` as the message. Retry that one. --- # Create computer Source: https://docs.orgo.ai/api-reference/computers/create Endpoint: POST /computers Summary: Provision a new computer in a workspace. Creates a computer in a workspace and boots it. The response carries everything you need to connect, so no follow-up request is required. **Info:** A `template_ref` create restores the template's golden snapshot, which boots far faster than the plain base image. Without one the computer cold-boots from the base image. ## Request - `workspace_id` (body, string, required) ID of the workspace to create the computer in. Omitting it returns `400`. The older name `project_id` is also accepted; `workspace_id` wins when you send both. - `name` (body, string, required) Computer name. Must be unique within the workspace. A name already in use returns `409`, and omitting the field returns `400`. - `os` (body, string, default `linux`) Operating system: `linux`, `windows`, `macos`, `android`, or `ios`. Any other value returns `400`. `windows` needs a Scale plan or a purchased Windows licence, otherwise it returns `403` with code `WINDOWS_REQUIRES_SCALE`. `ios` is a physical handset rather than a virtual computer. Every value other than `linux` only succeeds where the fleet has a host that supports it, otherwise create returns `422` with code `OS_UNAVAILABLE`. - `ram` (body, integer, default `4`) RAM in GB: `4`, `8`, `16`, `32`, or `64`. With `os: "macos"`, `12` is also accepted. `0` is treated as `4`. Any other value returns `400`. A value above your plan's per-computer ceiling is rejected with `403`, not clamped. Ignored for `os: "ios"`, which always records `4`. - `cpu` (body, number, default `1`) vCPU: `0.5`, `1`, `2`, `4`, `8`, or `16`. `0` is treated as `1`. Any other value returns `400`. A value above your plan's per-computer ceiling is rejected with `403`, not clamped. Ignored for `os: "ios"`, which always records `1`. - `disk_size_gb` (body, integer) Disk size in GB. When omitted it falls back to the template's declared disk, or the workspace owner’s plan birth size when there is no template or the template declares none (`40` GB on current paid plans). `0` counts as omitted. A negative or non-numeric value, or one above your plan's per-computer ceiling, is rejected with `400` and code `disk_exceeds_quota`, not clamped. - `resolution` (body, string, default `1280x720x24`) Display resolution in `WIDTHxHEIGHTxDEPTH` format (e.g. `1024x768x24`, `1920x1080x24`). Omitted means `1280x720x24`, or `1920x1080x24` for `macos`. With a `template_ref`, omitting it keeps the template's own resolution. - `gpu` (body, string) vGPU slice to attach: `2q` (2 GB VRAM) or `4q` (4 GB VRAM). `"none"`, `""`, `false`, and `"false"` are treated as omitted. Any other value returns `400`. Linux only: combining it with another `os` returns `400` with code `GPU_REQUIRES_LINUX`. Omitted means a CPU-only computer. If no GPU host can take the create, the request either returns `503` `GPU_HOST_UNAVAILABLE` or falls back to a CPU computer, in which case the `201` body carries `gpu_downgraded` and `warning`. - `template_ref` (body, string) Launch from a [template](https://docs.orgo.ai/guides/templates/introduction)'s golden snapshot instead of a base image, so the computer boots fully configured. Format `namespace/name@version`, for example `system/claude-code@1.0.0` (curated) or `default/my-template@1.0.0` (your own). A malformed ref returns `400`; a template whose build is not `ready` returns `409` with code `TEMPLATE_NOT_READY`. Explicit `cpu`, `ram`, and `disk_size_gb` override the template's own hardware. The template's own `cpu` and `ram` are clamped to your plan's per-computer ceiling rather than rejected; its disk is not, so a template disk above your ceiling returns `400` with code `disk_exceeds_quota`. Omitted means a plain base-image computer. ### Common configurations | RAM | vCPU | Best for | |-----|------|----------| | 4 GB | 0.5 | Light, always-on agents | | 4 GB | 1 | Standard workflows (default) | | 8 GB | 2 | Heavier automation | | 16 GB | 2 | Development and browser automation | | 32 GB | 4 | Memory-intensive tasks | | 64 GB | 4 | The largest computer you can create | **One computer is capped at 4 vCPU, 64 GB RAM, and 300 GB of disk.** No plan on sale raises that cap, and add-ons only take you up to it. On every plan on sale, `cpu: 8` and `cpu: 16` therefore pass the value check and then return `403` with code `PER_COMPUTER_CPU_CAP`. Your own per-computer ceiling starts below the cap and depends on your plan. See https://orgo.ai/pricing. ## Response Returns the created computer. - `id` (string) Computer identifier (UUID). - `name` (string) Computer name. - `workspace_id` (string) Parent workspace ID. - `project_id` (string) Deprecated alias for `workspace_id`, carrying the same value. - `os` (string) Operating system. - `ram` (integer) RAM in GB. - `cpu` (number) vCPU. - `resolution` (string) Display resolution in `WIDTHxHEIGHTxDEPTH` format. - `status` (string) One of `creating`, `running`, `restarting`, `updating`, `suspended`, `frozen`, `stopped`, `error`, `deleted`. A successful create returns `running`. See [Get computer](https://docs.orgo.ai/api-reference/computers/get) for what each value means. - `url` (string) The computer's API address on its fleet host, as `http://{host}:{port}`. This is an internal fleet address, not a dashboard link and not the endpoint you connect to. Use `connection_url` to connect. It is rewritten whenever the computer restarts, is started again, or has its RAM resized. - `created_at` (string) ISO 8601 timestamp. - `instance_id` (string) Stable identifier for the underlying compute instance. Use it for connection URLs and to reference the computer across restarts. - `fly_instance_id` (string) Deprecated alias for `instance_id`, carrying the same value. - `hostname` (string) Same-origin host for the computer's connection endpoints: `www.orgo.ai`. - `connection_url` (string) Same-origin connection base (`https://www.orgo.ai/desktops/{instance_id}`). Append `/ws/websockify`, `/ws/terminal`, or `/ws/audio` for the WebSocket endpoints; HTTP Desktop API calls go to `https://www.orgo.ai/api/desktops/{instance_id}/proxy/{endpoint}`. - `vnc_password` (string) Token for the computer's WebSocket APIs (VNC, terminal, audio, events), sent as `?token=`, and Bearer token for its Desktop API proxy. **Rotates on every restart.** Do not persist it. Take a fresh value from `POST /computers` or `GET /computers/{id}`. `null` when the stored credential cannot be decrypted. - `instance_details` (object) The computer's placement and connection record: `provider`, `id`, `name`, `webUrl`, `vncHost`, `vncPort`, `apiPort`, `serverAddress`, `resolution`, and `fromPool` (always `false`), plus `hypervisor` and `templateTerminals` when they apply. - `hardware` (object) Present when the host reports how it launched the computer. What the computer runs on, in the shape [Get computer](https://docs.orgo.ai/api-reference/computers/get) returns. - `launch` (object) Present alongside `hardware`. The host's launch record for the new computer, without the host's name or the source its state was restored from. - `gpu_downgraded` (boolean) Present and `true` only when you asked for a `gpu` and no GPU host was available, so a CPU computer was created instead. - `warning` (string) Present only alongside `gpu_downgraded`, explaining the downgrade. **Tip:** **Fast path: 1 API call.** The response above contains everything needed to connect. No follow-up `GET /computers/{id}` or `GET /computers/{id}/vnc-password` is required. Poll `https://www.orgo.ai/api/desktops/{instance_id}/proxy/health` until it returns 200, then connect. ## Example ```bash cURL curl -X POST https://www.orgo.ai/api/computers \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "workspace_id": "'"$WORKSPACE_ID"'", "name": "agent-1", "os": "linux", "ram": 4, "cpu": 1 }' ``` ```python Python import requests response = requests.post( "https://www.orgo.ai/api/computers", headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" }, json={ "workspace_id": workspace_id, "name": "agent-1", "os": "linux", "ram": 4, "cpu": 1 } ) computer = response.json() print(f"Created: {computer['id']}") print(f"Connect at: {computer['connection_url']}") ``` ```javascript JavaScript const response = await fetch('https://www.orgo.ai/api/computers', { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ workspace_id: workspaceId, name: 'agent-1', os: 'linux', ram: 4, cpu: 1 }) }); const computer = await response.json(); console.log(`Created: ${computer.id}`); console.log(`Connect at: ${computer.connection_url}`); ``` ### Response ```json { "id": "a3bb189e-8bf9-3888-9912-ace4e6543002", "name": "agent-1", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "project_id": "550e8400-e29b-41d4-a716-446655440000", "os": "linux", "ram": 4, "cpu": 1, "resolution": "1280x720x24", "status": "running", "url": "http://162.43.189.25:8231", "created_at": "2026-04-07T10:35:00Z", "instance_id": "a3881618", "fly_instance_id": "a3881618", "hostname": "www.orgo.ai", "connection_url": "https://www.orgo.ai/desktops/a3881618", "vnc_password": "a06db12a8683df96", "instance_details": { "provider": "metal", "id": "a3881618", "name": "a3881618", "webUrl": "http://162.43.189.25:8231", "vncHost": "162.43.189.25", "vncPort": 8232, "apiPort": 8231, "serverAddress": "http://10.0.0.4:9000", "resolution": "1280x720x24", "fromPool": false } } ``` ## Errors Every error body carries `error`. Most also carry a machine-readable `code`, and some carry extra fields, listed below. | Status | `code` | When | |--------|--------|------| | `400` | none | `workspace_id` or `name` missing, `ram`/`cpu`/`os`/`gpu` outside the accepted values, `template_ref` malformed, or a body that is valid JSON but not an object (`A computer configuration is required.`). | | `400` | `GPU_REQUIRES_LINUX` | `gpu` was combined with a non-Linux `os`. | | `400` | `disk_exceeds_quota` | `disk_size_gb` is negative or not a number, or it (or the template's declared disk) is above your per-computer ceiling. Also carries `max_disk_gb` and `canManageCapacity`. | | `401` | none | The Bearer token starts with `sk_` but is not a known Orgo key. The body is `{ "error": "Invalid API key" }`. | | `401` | none | No `Authorization` header, or a Bearer token that is not an `sk_` key. The body is `{ "error": "Authentication required" }`. | | `401` | none | The workspace does not exist, or you are not its owner or a member: `You do not have access to this workspace.` A view-only member gets `This workspace is view-only. Ask the owner for write access (workspace_read_only).` A key scoped to another workspace gets `This API key cannot access this workspace (workspace_scope_mismatch).` These checks run during authentication, so they return `401`, not `403`. | | `401` | none | The credential store is unavailable: `Service temporarily unavailable. The database is not accepting requests. Retry shortly.` Retry; your key is not the problem. | | `403` | `GUEST_RESTRICTED` | Share-link guests cannot create computers. | | `403` | none | A grandfathered plan is over its computer or RAM limit. | | `403` | `WINDOWS_REQUIRES_SCALE` | `os: "windows"` with no Windows licence available. | | `403` | `UPGRADE_REQUIRED` | The workspace owner's plan funds no computers. | | `403` | `VM_SLOT_ADDON`, `RAM_ADDON`, `VCPU_ADDON` | The owner's computer, account-RAM, or per-computer ceiling is reached and an add-on would raise it. | | `403` | `PER_COMPUTER_RAM_CAP`, `PER_COMPUTER_CPU_CAP` | The request is past the universal per-computer hard cap, which no plan raises. | | `403` | `CHANGE_PLAN` | The owner's legacy plan is larger than every sold plan, so the fix is a plan change rather than an upgrade. | | `403` | `PLAN_LIMIT` | The owner has a plan negotiated with Orgo, and the create is past one of its limits: a computer count, a per-OS count, the RAM pool, or a per-computer RAM or vCPU ceiling. | | `409` | `NAME_TAKEN` / `name_taken` | A computer of that name already exists in the workspace. | | `409` | `TEMPLATE_NOT_READY` | The template's build is still running or has failed. | | `422` | `OS_UNAVAILABLE` | No eligible host supports the requested `os`. | | `500` | none | Unmapped failure, such as a database error or a body that is not valid JSON. | | `503` | `golden_unavailable` | The template's golden image could not be resolved on the host. | | `503` | `TEMPLATE_HOST_UNAVAILABLE` | No template-capable host could take the create. | | `503` | `TEMPLATE_READINESS_UNAVAILABLE` | The template's build status could not be read. Also carries `retryable: true`. | | `503` | `DEDICATED_CAPACITY_FULL`, `FLEET_CAPACITY_FULL`, `GPU_HOST_UNAVAILABLE` | Capacity is exhausted. Each also carries `retryable: true`. | | `503` | `BACKEND_UNAVAILABLE` | The fleet is unreachable. Retry. | The quota responses (`UPGRADE_REQUIRED`, `VM_SLOT_ADDON`, `RAM_ADDON`, `VCPU_ADDON`, `PER_COMPUTER_RAM_CAP`, `PER_COMPUTER_CPU_CAP`, `CHANGE_PLAN`, `PLAN_LIMIT`, and the code-less grandfathered case) also carry `canManageCapacity`, which tells you whether you are the workspace owner and can raise the limit yourself. Where a plan change would fix it, they also carry `upgradeTier`. `WINDOWS_REQUIRES_SCALE` carries `upgradeTier` alone. ```json { "error": "disk_size_gb must be between 1 and 28 (8 GB plan + 20 GB add-on)", "code": "disk_exceeds_quota", "max_disk_gb": 28, "canManageCapacity": true } ``` ## Unavailable operating systems An accepted OS value can still be unavailable in your workspace. When no eligible host supports it, create returns `422` with code `OS_UNAVAILABLE`. Failed provisioning removes its placeholder record, so you can retry the name. Temporary capacity failures remain retryable service errors. The 300 GB disk cap is a product maximum, not an included allowance. Current Scale includes a 150 GB per-computer ceiling; unused storage add-ons can raise it to 300 GB. Complimentary current Scale has the same resource limits as paid current Scale. --- # Get computer Source: https://docs.orgo.ai/api-reference/computers/get Endpoint: GET /computers/{id} Summary: Retrieve a computer by ID. Returns a computer's details, including its current status and everything needed to connect to it. ## Path parameters - `id` (path, string, required) Computer UUID or `instance_id`. Both resolve to the same computer. ## Response - `id` (string) Computer identifier (UUID). - `name` (string) Computer name. - `project_id` (string) ID of the parent workspace. - `project_name` (string) Name of the parent workspace. - `permissions` (object) What you can do in the computer's workspace: `canView`, `canWrite`, and `canManageAccess`, each a boolean. - `instance_details` (object) The computer's placement and connection record, with every field whose name contains `password`, `secret`, `token`, `api_key`, or `authorized_keys` removed. - `os` (string) Operating system: `linux`, `windows`, `macos`, `android`, or `ios`. - `ram` (integer) RAM in GB. - `cpu` (number) vCPU. Fractional for `0.5` vCPU computers. - `status` (string) One of `creating`, `running`, `restarting`, `updating`, `suspended`, `frozen`, `stopped`, `error`, `deleted`. See [Status values](#status-values). - `url` (string) The computer's API address on its fleet host, as `http://{host}:{port}`. This is an internal fleet address, not a dashboard link and not the endpoint you connect to. Use `connection_url` to connect. The value is rewritten whenever the computer restarts, is started again, or has its RAM resized. It still holds the last host's address after the computer is stopped, so it is only meaningful while the computer is `running`. - `fly_instance_id` (string) Stable identifier for the underlying compute instance. It is the same value `POST /computers` returns as `instance_id`. Use it to build `connection_url` and to reference the computer across restarts. `null` while the computer has no host (`frozen`). This endpoint returns the field under its legacy name only. - `created_at` (string) ISO 8601 timestamp. - `hostname` (string) Same-origin host for the computer's connection endpoints: `www.orgo.ai`. Empty while the computer has no instance id, such as when it is `frozen`. - `connection_url` (string) Same-origin connection base (`https://www.orgo.ai/desktops/{instance_id}`). Append `/ws/websockify`, `/ws/terminal`, or `/ws/audio` for the WebSocket endpoints; HTTP Desktop API calls go to `https://www.orgo.ai/api/desktops/{instance_id}/proxy/{endpoint}`. Empty while the computer has no instance id, such as when it is `frozen`. - `vnc_password` (string) Current VNC / WebSocket token. Rotates on restart, on start, and on a RAM resize. `null` when you have view-only access to the workspace, or when the stored credential cannot be decrypted. - `private_screens` (boolean) `true` when the computer's workspace keeps screens private, so the Orgo dashboard keeps the screen covered until someone chooses to show it. - `hardware` (object | null) What the computer runs on, from its current VM's launch record: `host_cpu` (`family`, `name`, and `signature`, or `null`), `cpu_model`, `hypervisor`, `qemu`, `boot`, `fallback`, and `launched_at`. `null` when the host has not reported a launch record for the computer's current VM. ### Status values | Status | Meaning | |--------|---------| | `creating` | The record exists and the computer is being provisioned. | | `running` | The computer is up on a host and reachable. | | `restarting` | A [restart](https://docs.orgo.ai/api-reference/computers/restart) is in flight. | | `updating` | An in-place OS image update is in flight. | | `suspended` | Paused on its host. It resumes on interaction, not through [start](https://docs.orgo.ai/api-reference/computers/start). | | `frozen` | Stopped. The disk is archived to object storage and no host is held. This is what [stop](https://docs.orgo.ai/api-reference/computers/stop) produces. | | `stopped` | The computer was not found on its host, or its host reported a terminal state. Like `frozen`, it can be started again. | | `error` | Its host reported the computer in an error state when a restart or OS update did not finish. Like `stopped`, it can be started again. | | `deleted` | Tombstoned after its archived disk passed the retention window. | ## Example ```bash cURL curl https://www.orgo.ai/api/computers/$COMPUTER_ID \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import requests response = requests.get( f"https://www.orgo.ai/api/computers/{computer_id}", headers={"Authorization": f"Bearer {api_key}"} ) computer = response.json() print(f"Status: {computer['status']}") ``` ```javascript JavaScript const response = await fetch(`https://www.orgo.ai/api/computers/${computerId}`, { headers: { 'Authorization': `Bearer ${apiKey}` } }); const computer = await response.json(); console.log(`Status: ${computer.status}`); ``` ### Response ```json { "id": "a3bb189e-8bf9-3888-9912-ace4e6543002", "project_id": "550e8400-e29b-41d4-a716-446655440000", "permissions": { "canView": true, "canWrite": true, "canManageAccess": true }, "instance_details": { "provider": "metal", "id": "a3881618", "name": "a3881618", "webUrl": "http://162.43.189.25:8231", "vncHost": "162.43.189.25", "vncPort": 8232, "apiPort": 8231, "serverAddress": "http://10.0.0.4:9000", "resolution": "1280x720x24", "fromPool": false }, "name": "agent-1", "project_name": "production", "os": "linux", "ram": 4, "cpu": 1, "status": "running", "url": "http://162.43.189.25:8231", "fly_instance_id": "a3881618", "created_at": "2026-04-07T10:35:00Z", "hostname": "www.orgo.ai", "connection_url": "https://www.orgo.ai/desktops/a3881618", "vnc_password": "a06db12a8683df96", "private_screens": false, "hardware": null } ``` ## Errors | Status | Body | When | |--------|------|------| | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `402` | `{ "error": "Choose a plan to continue." }` | The computer is a trial computer whose trial has ended. | | `403` | `{ "error": "You do not have access to this workspace." }` | You are neither the owner nor a member of the computer's workspace. | | `403` | `{ "error": "This API key cannot access this workspace (workspace_scope_mismatch)." }` | The API key is scoped to a different workspace. | | `404` | `{ "error": "Desktop not found" }` | No computer matches the id. | | `500` | `{ "error": "…" }` | Lookup failed. | | `503` | `{ "error": "Service temporarily unavailable. The database is not accepting requests. Retry shortly." }` | The credential store is unavailable. Carries `Retry-After: 5`. | ## Rename a computer `PATCH /computers/{id}` updates a computer's name and its mascot. It takes the same path parameter as `GET`, and requires that you **own** the workspace. A view-only member gets `403`, and a member with write access who is not the owner gets `404` with `Access denied`. - `name` (body, string) New computer name. Omitted leaves the name alone; an empty or non-string value returns `400`. - `bot_mascot` (body, object) The computer's mascot look, with all fields optional: `color` (`stone`, `pearl`, `graphite`, `ink`, `green`, `blue`, `red`, `orange`, `purple`, `cyan`, `pink`, `yellow`, `teal`, `coral`), `expression` (`deadpan`, `friendly`, `focused`, `thinking`, `excited`, `sleepy`, `surprised`, `skeptical`, `worried`, `mischievous`), and `accessory` (`none`, `beanie`, `tophat`, `party`, `glasses`, `sunglasses`, `bow`). Unrecognised fields and values are dropped rather than rejected, so a field you send with an unknown value falls back to the default. `null` clears the mascot; omitting the field leaves it alone; any other non-object value returns `400`. The response is the updated record, not the full computer object. It carries `id`, `name`, `bot_mascot` (`null` when unset), and `updated_at`. ```bash curl -X PATCH https://www.orgo.ai/api/computers/$COMPUTER_ID \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "agent-1-renamed", "bot_mascot": {"color": "teal", "expression": "focused"}}' ``` ```json { "id": "a3bb189e-8bf9-3888-9912-ace4e6543002", "name": "agent-1-renamed", "bot_mascot": { "color": "teal", "expression": "focused" }, "updated_at": "2026-04-07T11:02:14.000Z" } ``` | Status | Body | When | |--------|------|------| | `400` | `{ "error": "Computer name cannot be empty" }` | `name` is present but empty or not a string. | | `400` | `{ "error": "bot_mascot must be an object or null" }` | `bot_mascot` is neither an object nor `null`. | | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `402` | `{ "error": "Choose a plan to continue." }` | The computer is a trial computer whose trial has ended. | | `403` | `{ "error": "You do not have access to this workspace." }` | You are neither the owner nor a member of the workspace. | | `403` | `{ "error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)." }` | You are a view-only member. | | `403` | `{ "error": "This API key cannot access this workspace (workspace_scope_mismatch)." }` | The API key is scoped to a different workspace. | | `404` | `{ "error": "Desktop not found" }` or `{ "error": "Access denied" }` | No computer matches the id, or you are a member with write access but not the workspace owner. | | `500` | `{ "error": "…" }` | The update failed, for example because another computer in the workspace already has that `name`, or the body is not valid JSON. | | `503` | `{ "error": "Service temporarily unavailable. …" }` | Orgo could not verify the credential because of a server-side fault. The response carries `Retry-After: 5`. Retry after that delay. | --- # Clone computer Source: https://docs.orgo.ai/api-reference/computers/clone Endpoint: POST /computers/{id}/clone Summary: Duplicate a computer, preserving its disk state. Creates a copy of an existing computer with the same disk state, using your optional `name`, or a generated `(clone)` suffix. The clone lands in the source’s workspace and comes up `running`. **Info:** Cloning copies the full disk: installed software, files, browser sessions. The clone gets a new UUID and a new `instance_id`. Its auto-stop is forced to always-on regardless of the source’s setting. ## Clone or fork? | | Clone | [Fork](https://docs.orgo.ai/api-reference/computers/fork) | |---|---|---| | Copies | The disk: files, installed apps, browser sessions | The disk and live memory: open apps, windows, running processes | | The copy starts | From a fresh boot | Where the source was | | Source | Running or stopped | Running, Linux only | | Name | Your `name`, or `agent-1 (clone)` | `agent-1 (fork)` | Clone a computer to reuse its setup. Fork it to try two next steps from the same moment. ## Path parameters - `id` (path, string, required) The source computer’s UUID or `instance_id`. Both resolve to the same computer, as on the other computer endpoints. The `instance_id` is the value `POST /computers` returns as `instance_id`, and `GET /computers/{id}` as `fly_instance_id`. ## Response Returns `201` with the clone’s identifiers. - `id` (string) New computer UUID. - `name` (string) Your requested name, or a generated name such as `agent-1 (clone)` or `agent-1 (clone 2)`. - `status` (string) Always `running`: the response is sent after the clone has booted. - `fly_instance_id` (string) The clone’s `instance_id`. This is the handle to use for its own clone, connection, and proxy calls. ## Plan limits A clone is a new computer, so it is charged against the **workspace owner’s** plan. It draws on their computer slot, account RAM, per-computer CPU/RAM ceiling, and the storage the source’s disk requires. Any of those being exhausted returns `403`. See https://orgo.ai/pricing. ## Example ```bash cURL curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/clone \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import requests response = requests.post( f"https://www.orgo.ai/api/computers/{computer_id}/clone", headers={"Authorization": f"Bearer {api_key}"} ) clone = response.json() print(f"Clone created: {clone['name']} ({clone['id']})") ``` ```javascript JavaScript const response = await fetch(`https://www.orgo.ai/api/computers/${computerId}/clone`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}` } }); const clone = await response.json(); console.log(`Clone created: ${clone.name} (${clone.id})`); ``` ### Response ```json { "id": "b4cc290f-9bf9-3888-9912-ace4e6543003", "name": "agent-1 (clone)", "status": "running", "fly_instance_id": "b4cc290f" } ``` ## Errors Every error body carries `error` and `code`. | Status | `code` | When | |--------|--------|------| | `400` | `INVALID_REQUEST` | The body is not a JSON object, or `name` is not a string of 1-255 characters after trimming. | | `400` | `DEVICE_LIFECYCLE_UNSUPPORTED` | The source runs on a dedicated machine that Orgo manages directly. Contact support. | | `400` | `NOT_CLONABLE` | The source has no server address on record, so it was never fully provisioned. | | `400` | `NO_INSTANCE` | The source has no `instance_id` on record. It is `frozen` or still being created. | | `401` | `UNAUTHENTICATED` | The Bearer token starts with `sk_` but is not a known Orgo key. The body is `{ "error": "Invalid API key", "code": "UNAUTHENTICATED" }`. | | `401` | `UNAUTHENTICATED` | No `Authorization` header, or a Bearer token that is not an `sk_` key. The body is `{ "error": "Authentication required", "code": "UNAUTHENTICATED" }`. | | `401` | `UNAUTHENTICATED` | You are not the owner or a member of the source’s workspace (`You do not have access to this workspace.`), you are a view-only member (`This workspace is view-only. Ask the owner for write access (workspace_read_only).`), or the API key is scoped to another workspace (`This API key cannot access this workspace (workspace_scope_mismatch).`). These checks run during authentication, so they return `401`, not `403`. | | `401` | `UNAUTHENTICATED` | The credential store is unavailable: `Service temporarily unavailable. The database is not accepting requests. Retry shortly.` Retry; your key is not the problem. | | `402` | `NOT_A_MEMBER` | The source is a trial computer whose trial has ended. The body is `{ "error": "Choose a plan to continue.", "code": "NOT_A_MEMBER" }`. | | `403` | `GUEST_RESTRICTED` | Share-link guests cannot create computers. | | `403` | `DESKTOP_LIMIT`, `UPGRADE_REQUIRED`, `VM_SLOT_ADDON`, `RAM_ADDON`, `VCPU_ADDON`, `PER_COMPUTER_RAM_CAP`, `PER_COMPUTER_CPU_CAP`, `CHANGE_PLAN`, `PLAN_LIMIT` | The owner’s plan cannot fund another computer of this size. `PLAN_LIMIT` means a limit of a plan negotiated with Orgo. Also carries `upgradeTier` where a plan change would fix it. | | `403` | `DISK_QUOTA_EXCEEDED` | The source’s disk is larger than the per-computer storage the owner’s plan allows. Also carries `max_disk_gb`. | | `404` | `NOT_FOUND` | No computer matches the id, or its workspace no longer exists. | | `409` | `NAME_TAKEN` | A computer with the requested `name` already exists in the workspace. | | `409` | `HOURLY_QUOTE_REQUIRED` | The source is billed hourly or by its own subscription, so a copy needs its own purchase. | | `500` | `AUTH_LOOKUP_FAILED` | The account could not be verified. | | `500` | `INSERT_FAILED` | The clone record could not be saved. | | `500` | `CLONE_FAILED` | The copy failed on the host. The placeholder record is rolled back, so no half-made computer is left behind. | ```json { "error": "This computer can’t be cloned (no server address on record).", "code": "NOT_CLONABLE" } ``` ## Optional name - `name` (body, string) A unique name of 1-255 characters after trimming. Omit the body to generate a name. An empty or invalid name returns `400` with code `INVALID_REQUEST`; a name collision returns `409` with code `NAME_TAKEN`. Cloning is a disk copy, so stop applications that require an application-consistent backup before cloning. Guest flush and snapshot consistency depend on the computer backend. --- # Fork computer Source: https://docs.orgo.ai/api-reference/computers/fork Endpoint: POST /computers/{id}/fork Summary: Copy a running computer, memory included, so the copy carries on from the same moment. Creates a copy of a running Linux computer, including what is in its memory. The fork comes up `running` in the source’s workspace with the same apps, windows, and processes open, so an agent can carry on from the exact same point. ## Clone or fork? | | [Clone](https://docs.orgo.ai/api-reference/computers/clone) | Fork | |---|---|---| | Copies | The disk: files, installed apps, browser sessions | The disk and live memory: open apps, windows, running processes | | The copy starts | From a fresh boot | Where the source was | | Source | Running or stopped | Running, Linux only | | Name | Your `name`, or `agent-1 (clone)` | `agent-1 (fork)` | Clone a computer to reuse its setup. Fork it to try two next steps from the same moment. **Info:** The source pauses for about two seconds while its memory is copied, then carries on. The fork gets a new UUID and a new `instance_id`. Its auto-stop is forced to always-on regardless of the source’s setting. ## Path parameters - `id` (path, string, required) The source computer’s `instance_id`: the value `POST /computers` returns as `instance_id`, and `GET /computers/{id}` as `fly_instance_id`. Unlike clone, fork does not take the UUID: a UUID returns `404`. ## Response Returns `201` once the fork is running. The request has no body. - `id` (string) New computer UUID. - `name` (string) A generated name: `agent-1 (fork)`, then `agent-1 (fork 2)`. - `status` (string) Always `running`: the response is sent after the fork has resumed. - `fly_instance_id` (string) The fork’s `instance_id`. Use it for the fork’s own calls, including another fork. ## Plan limits A fork is a new computer, so it is charged against the **workspace owner’s** plan. It draws on their computer slot, account RAM, per-computer CPU/RAM ceiling, and the storage the source’s disk requires. Any of those being exhausted returns `403`. See https://orgo.ai/pricing. ## Example ```bash cURL curl -X POST https://www.orgo.ai/api/computers/$INSTANCE_ID/fork \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import requests response = requests.post( f"https://www.orgo.ai/api/computers/{instance_id}/fork", headers={"Authorization": f"Bearer {api_key}"} ) fork = response.json() print(f"Fork created: {fork['name']} ({fork['fly_instance_id']})") ``` ```javascript JavaScript const response = await fetch(`https://www.orgo.ai/api/computers/${instanceId}/fork`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}` } }); const fork = await response.json(); console.log(`Fork created: ${fork.name} (${fork.fly_instance_id})`); ``` ### Response ```json { "id": "7f3a9c21-5e8b-4d0a-b6c4-1e2f3a4b5c6d", "name": "agent-1 (fork)", "status": "running", "fly_instance_id": "7f3a9c21" } ``` ## Errors Every error body carries `error` and `code`. | Status | `code` | When | |--------|--------|------| | `400` | `DEVICE_LIFECYCLE_UNSUPPORTED` | The source runs on a dedicated machine that Orgo manages directly. | | `400` | `NOT_FORKABLE` | The source has no server address on record, so it was never fully provisioned. | | `401` | `UNAUTHENTICATED` | No valid API key, or the key can’t change the source’s workspace: you are not a member, you are view-only, or the key is scoped to another workspace. These checks run during authentication, so they return `401`, not `403`. | | `401` | `UNAUTHENTICATED` | The credential store is unavailable: `Service temporarily unavailable. The database is not accepting requests. Retry shortly.` Retry; your key is not the problem. | | `403` | `GUEST_RESTRICTED` | Share-link guests cannot create computers. | | `403` | `DESKTOP_LIMIT`, `UPGRADE_REQUIRED`, `VM_SLOT_ADDON`, `RAM_ADDON`, `VCPU_ADDON`, `PER_COMPUTER_RAM_CAP`, `PER_COMPUTER_CPU_CAP`, `CHANGE_PLAN`, `PLAN_LIMIT` | The owner’s plan cannot fund another computer of this size. `PLAN_LIMIT` means a limit of a plan negotiated with Orgo. Also carries `upgradeTier` where a plan change would fix it. | | `403` | `DISK_QUOTA_EXCEEDED` | The source’s disk is larger than the per-computer storage the owner’s plan allows. Also carries `max_disk_gb`. | | `404` | `NOT_FOUND` | No computer has this `instance_id`, or its workspace no longer exists. | | `409` | `NOT_FORKABLE` | The source is not running, is a Windows computer, or runs on an older VM type that cannot fork live. Create a new computer to fork, or contact support to migrate this one. | | `409` | `HOURLY_QUOTE_REQUIRED` | The source is billed hourly or by its own subscription, so a copy needs its own purchase. | | `500` | `AUTH_LOOKUP_FAILED` | The account could not be verified. | | `500` | `INSERT_FAILED` | The fork record could not be saved. | | `500` | `FORK_FAILED` | The copy failed on the host. The placeholder record is rolled back, so no half-made computer is left behind. | ```json { "error": "This computer must be running to fork (a fork copies its live memory).", "code": "NOT_FORKABLE" } ``` --- # Move computer Source: https://docs.orgo.ai/api-reference/computers/move Endpoint: PATCH /computers/{id}/move Summary: Move a computer to a different workspace while preserving its state and ID. Moves a computer to a different workspace. The computer keeps its state, configuration, and IDs. Only its parent workspace changes. You must own both the source and the destination workspace. Being a member of either is not enough. A workspace-scoped API key can never move a computer, because a move always crosses a workspace boundary. ## Path parameters - `id` (path, string, required) Computer UUID or `instance_id`. Both resolve to the same computer. ## Body parameters Send the destination workspace under `workspace_id`. - `workspace_id` (body, string, required) ID of the destination workspace. Must be owned by the same user, and must differ from the computer’s current workspace. **Note:** A request with no `workspace_id` returns `400`. The older name `project_id` is also accepted; `workspace_id` wins when you send both. ## Response - `success` (boolean) `true` when the move succeeded. - `workspace_id` (string) The destination workspace ID, echoed back. - `project_id` (string) The same destination workspace ID under its legacy name, for older clients. ## Example ```bash cURL curl -X PATCH https://www.orgo.ai/api/computers/$COMPUTER_ID/move \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"workspace_id": "'"$WORKSPACE_ID"'"}' ``` ```python Python import requests response = requests.patch( f"https://www.orgo.ai/api/computers/{computer_id}/move", headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" }, json={"workspace_id": target_workspace_id} ) print(response.json()) ``` ```javascript JavaScript const response = await fetch(`https://www.orgo.ai/api/computers/${computerId}/move`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ workspace_id: targetWorkspaceId }) }); console.log(await response.json()); ``` ### Response ```json { "success": true, "workspace_id": "550e8400-e29b-41d4-a716-446655440099", "project_id": "550e8400-e29b-41d4-a716-446655440099" } ``` ## Errors | Status | Body | When | |--------|------|------| | `400` | `{ "error": "workspace_id is required" }` | `workspace_id` is missing or empty. | | `400` | `{ "error": "Already in this workspace" }` | The computer already belongs to the destination workspace. | | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `401` | `{ "error": "You do not have access to this workspace." }` | You are not a member of the source or the destination workspace, or the destination does not exist. | | `401` | `{ "error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)." }` | You are a view-only member of the source or the destination workspace. | | `401` | `{ "error": "This API key cannot access this workspace (workspace_scope_mismatch)." }` | The API key is scoped to a single workspace. A move always crosses a workspace boundary, so a scoped key can never perform one. | | `401` | `{ "error": "Service temporarily unavailable. The database is not accepting requests. Retry shortly." }` | The credential store is unavailable. Retry; your key is not the problem. | | `403` | `{ "error": "Access denied" }` | You can edit both workspaces but do not own both of them. | | `404` | `{ "error": "Computer not found" }` | No computer matches the id. | | `409` | `{ "error": "computer_busy" }` | Another operation holds the computer. Retry shortly. | | `409` | `{ "error": "openclaw_enrollment_cleanup_required", "detail": "…" }` | The computer has an OpenClaw enrollment that the move would strand. Revoke and retire it first. | | `409` | `{ "error": "latitude_monitoring_active", "detail": "Turn off Latitude monitoring for this computer first." }` | Latitude monitoring is on for this computer, or one of its monitoring jobs is still running. | | `409` | `{ "error": "pool_mac_release_required", "message": "This Mac is managed by Orgo. Ask support to release it." }` | The computer is a Mac managed by Orgo. | | `500` | `{ "error": "Failed to move computer" }` | The update failed, or the request body is not valid JSON. | --- # Resize computer Source: https://docs.orgo.ai/api-reference/computers/resize Endpoint: PATCH /computers/{id}/resize Summary: Change CPU, RAM, disk, bandwidth, or auto-stop on a running computer. Resizes a running computer. Every field is optional. Send only what you want to change. **Warning:** `vcpus` and `bandwidth_limit_mbps` apply to the live computer. Disk growth applies live on Linux; a Windows computer restarts in place to grow its disk. A `mem_gb` change applies live on a Linux computer that supports live memory resize, and restarts any other computer in place. `connection_rotated` is `true` when the ports or VNC password changed. When it is, re-fetch [`GET /computers/{id}`](https://docs.orgo.ai/api-reference/computers/get) and reconnect. The computer must be `running`. The response reports the values the computer actually accepted, not the ones you asked for: a dimension that failed to apply comes back with its previous value. ## Path parameters - `id` (path, string, required) Computer UUID or `instance_id`. Both resolve to the same computer. ## Body parameters - `vcpus` (body, number) New vCPU count: `0.5`, `1`, `2`, `4`, `8`, or `16`, as a JSON number. Any other value returns `400`. A value above your per-computer ceiling returns `403`. Omitted leaves CPU alone. - `mem_gb` (body, integer) New RAM in GB: `4`, `8`, `16`, or `32`, as a JSON number. Any other value returns `400`. A resize tops out at the host's boot-memory ceiling, 32 GB by default, whatever your plan allows. `64` is accepted only on a fleet configured above that ceiling; otherwise a 64 GB computer is only reachable at create time. A value above your per-computer ceiling returns `403`. Omitted leaves RAM alone. - `disk_size_gb` (body, integer) New disk size in GB. **Disk can only grow**; a value below the current size returns `400`. A value below `1` or above this computer's ceiling also returns `400`, with `max_disk_gb` in the body. Omitted leaves the disk alone. Call `GET /computers/{id}/resize` to read the ceiling first. - `bandwidth_limit_mbps` (body, integer) New bandwidth cap in Mbps. Must be a non-negative number and no higher than the workspace owner's plan limit; both violations return `400`. `0` is accepted but leaves the cap unchanged. Omitted leaves the cap alone. - `auto_stop_minutes` (body, integer) Idle minutes before the computer suspends itself. `0` means always-on. Omitted leaves the current setting alone. This is applied separately from the four hardware dimensions and is not reflected in `results`. Orgo does not check the value before resizing: a value the host rejects, such as a negative number or more than `43200` (30 days), returns `500` after any hardware change has already applied. ## Response Returns the effective configuration after the resize attempt. - `vcpus` (number) Effective vCPU count. - `mem_gb` (integer) Effective RAM in GB. - `disk_size_gb` (integer) Effective disk size in GB. - `bandwidth_limit_mbps` (integer) Effective bandwidth limit in Mbps. - `auto_stop_minutes` (integer) The `auto_stop_minutes` you sent, echoed back. Absent when you did not send it. - `connection_rotated` (boolean) `true` when the computer's ports or VNC password changed. Re-fetch the computer's connection details before reconnecting. - `restarted` (boolean | null) `true` when the resize restarted the computer, `false` when it applied live. `null` when the host does not report it. - `identity_changed` (array) Names of connection values the host reports as changed: any of `api_port`, `novnc_port`, `vnc_password`, `ssh_host_port`, or `ip`. Empty when nothing changed or the host does not report changes. - `results` (array) One entry per attempted dimension: `{ "dimension": "vcpus" | "mem_mb" | "disk_gb" | "bandwidth_mbps", "requested": number, "applied": number, "ok": boolean, "error"?: string }`. This is an array, not an object keyed by dimension. Iterate it and switch on `dimension`. A dimension sent at its current value has no entry. Absent when the request included no hardware dimension. - `partial` (boolean) `true` when at least one dimension failed, `false` otherwise. It is `true` on both `207` and `422`, so use the HTTP status to tell a mixed result from a total failure. Absent when the request included no hardware dimension. ### Status codes - `200`: every requested dimension applied - `207`: some dimensions applied and others failed (check `results`) - `422`: every requested dimension failed ## Example ```bash cURL curl -X PATCH https://www.orgo.ai/api/computers/$COMPUTER_ID/resize \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "vcpus": 4, "mem_gb": 8, "disk_size_gb": 30 }' ``` ```python Python import requests response = requests.patch( f"https://www.orgo.ai/api/computers/{computer_id}/resize", headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" }, json={"vcpus": 4, "mem_gb": 8, "disk_size_gb": 30} ) result = response.json() print(f"Now at {result['vcpus']} vCPU, {result['mem_gb']} GB RAM") for r in result.get("results", []): if not r["ok"]: print(f"{r['dimension']} failed: {r.get('error')}") ``` ```javascript JavaScript const response = await fetch(`https://www.orgo.ai/api/computers/${computerId}/resize`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ vcpus: 4, mem_gb: 8, disk_size_gb: 30 }) }); const result = await response.json(); console.log(`Now at ${result.vcpus} vCPU, ${result.mem_gb} GB RAM`); for (const r of result.results ?? []) { if (!r.ok) console.log(`${r.dimension} failed: ${r.error}`); } ``` ### Response ```json { "vcpus": 4, "mem_gb": 8, "disk_size_gb": 30, "bandwidth_limit_mbps": 1000, "connection_rotated": false, "restarted": null, "identity_changed": [], "results": [ { "dimension": "vcpus", "requested": 4, "applied": 4, "ok": true }, { "dimension": "mem_mb", "requested": 8192, "applied": 8192, "ok": true }, { "dimension": "disk_gb", "requested": 30, "applied": 30, "ok": true } ], "partial": false } ``` ## Errors | Status | Body | When | |--------|------|------| | `400` | `{ "error": "vcpus must be 0.5, 1, 2, 4, 8, or 16 cores" }` | `vcpus` is not one of the accepted values. | | `400` | `{ "error": "mem_gb must be one of 4, 8, 16, 32 GB" }` | `mem_gb` is not one of the accepted values. | | `400` | `{ "error": "Storage can only be increased (current: 30 GB)" }` | `disk_size_gb` is below the current size. | | `400` | `{ "error": "disk_size_gb must be between 1 and 28 …", "max_disk_gb": 28 }` | `disk_size_gb` is below `1` or above this computer's ceiling. | | `400` | `{ "error": "bandwidth_limit_mbps must be a non-negative number" }` | `bandwidth_limit_mbps` is negative or not a number. | | `400` | `{ "error": "bandwidth_limit_mbps must not exceed 1000" }` | `bandwidth_limit_mbps` is above the plan's limit. Bandwidth over the plan limit is a `400`, not a `403`. | | `400` | `{ "error": "…", "code": "NOT_RESIZABLE" \| "RESIZE_UNSUPPORTED_OS" }` | The computer is a physical device, or its operating system does not support resize. | | `400` | `{ "error": "Server address not found for this computer" }` | The computer has no host on record. | | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `402` | `{ "error": "Choose a plan to resize this computer.", "code": "TRIAL_RESIZE_UNAVAILABLE" }` | The computer is a trial computer. | | `402` | `{ "error": "Choose a plan to continue." }` | The computer is a trial computer whose trial has ended. | | `403` | `{ "error": "You do not have access to this workspace." }` | You are not a member of the computer's workspace. | | `403` | `{ "error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)." }` | You have view-only access to the workspace. | | `403` | `{ "error": "This API key cannot access this workspace (workspace_scope_mismatch)." }` | The API key is scoped to a different workspace. | | `403` | `{ "error": "…", "code": "VCPU_ADDON" \| "PER_COMPUTER_CPU_CAP" \| "PLAN_LIMIT", "upgradeTier": "enterprise" }` | `vcpus` is above your per-computer ceiling, or past the universal per-computer hard cap. `PLAN_LIMIT` means the ceiling is a custom limit on your plan. | | `403` | `{ "error": "…", "code": "RAM_ADDON" \| "PLAN_LIMIT", "max_ram_gb": 16 }` | `mem_gb` is above your per-computer ceiling, or more than your other computers leave in the account's RAM pool. `PLAN_LIMIT` means the limit is a custom one on your plan. | | `404` | `{ "error": "Desktop not found" }` | No computer matches the id. | | `409` | `{ "error": "…", "code": "migrating" }` | The computer is being moved to another host. Retry in a few minutes. | | `409` | `{ "error": "Computer must be running to resize" }` | The computer is not `running`. | | `422` | The normal response body, with `partial: true` | Every requested dimension failed. | | `500` | `{ "error": "…" }` | The resize call itself failed, or the body is not valid JSON. | | `503` | `{ "error": "Service temporarily unavailable. …" }` | Orgo could not verify the credential because of a server-side fault. The response carries `Retry-After: 5`. Retry after that delay. | ## Read the ceilings before resizing `GET /computers/{id}/resize` returns this computer's headroom, so you can size a request before making it. It takes the same path parameter and no body. - `current_disk_gb` (integer) The computer's current disk size in GB. - `base_disk_gb` (integer) Per-computer disk included in the workspace owner's plan. - `blocks` (integer) Storage add-on blocks the owner has purchased. - `add_on_gb` (integer) Those blocks expressed in GB. - `max_disk_gb` (integer) The largest `disk_size_gb` this computer can be resized to. It is the plan's base plus whatever add-on storage the owner's other computers have not already claimed, up to 300 GB. - `max_ram_gb` (integer) The largest `mem_gb` this computer can be resized to, already bounded by the live-resize limit and by the RAM the owner's other computers leave in the account's pool. - `max_cpu` (number) The largest `vcpus` this computer can be resized to. - `max_bandwidth_mbps` (integer) The largest `bandwidth_limit_mbps` the workspace owner's plan allows. - `addon_ram_gb` (integer) Pooled RAM the owner has purchased, in GB. - `addon_vcpu` (number) Pooled vCPU the owner has purchased. - `live_mem_resize` (boolean) `true` when a `mem_gb` change applies without restarting the computer. Absent when the host cannot say, for example when the computer is not running. A computer that cannot be resized, such as a physical device or an operating system without resize support, returns `200` with a different shape: `resizable: false`, `reason`, `code`, and `current_disk_gb`, `max_disk_gb`, `max_ram_gb`, and `max_cpu` all set to its current size. ```bash curl https://www.orgo.ai/api/computers/$COMPUTER_ID/resize \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```json { "current_disk_gb": 8, "base_disk_gb": 8, "blocks": 1, "add_on_gb": 20, "max_disk_gb": 28, "max_ram_gb": 32, "max_cpu": 4, "max_bandwidth_mbps": 1000, "addon_ram_gb": 0, "addon_vcpu": 0 } ``` | Status | Body | When | |--------|------|------| | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `402` | `{ "error": "Choose a plan to continue." }` | The computer is a trial computer whose trial has ended. | | `403` | `{ "error": "You do not have access to this workspace." }` | You are not a member of the computer's workspace. | | `403` | `{ "error": "This API key cannot access this workspace (workspace_scope_mismatch)." }` | The API key is scoped to a different workspace. | | `404` | `{ "error": "Desktop not found" }` | No computer matches the id. | | `500` | `{ "error": "…" }` | The headroom could not be computed. | | `503` | `{ "error": "Service temporarily unavailable. …" }` | Orgo could not verify the credential because of a server-side fault. The response carries `Retry-After: 5`. Retry after that delay. | --- # Delete computer Source: https://docs.orgo.ai/api-reference/computers/delete Endpoint: DELETE /computers/{id} Summary: Permanently delete a computer. Permanently deletes a computer and all its data. **Warning:** This cannot be undone. The disk and everything on it is lost. Unless one of the `409` refusals listed in [Errors](#errors) applies, the record is always removed, even when the computer has no live host. An already-terminated computer, an unreachable host, and a `creating` record that never finished provisioning all delete cleanly. Tearing down the computer on its host is best-effort and never blocks the delete. ## Path parameters - `id` (path, string, required) Computer UUID or `instance_id`. Both resolve to the same computer. ## Response - `success` (boolean) `true` when the computer was deleted. ## Example ```bash cURL curl -X DELETE https://www.orgo.ai/api/computers/$COMPUTER_ID \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import requests response = requests.delete( f"https://www.orgo.ai/api/computers/{computer_id}", headers={"Authorization": f"Bearer {api_key}"} ) if response.json().get("success"): print("Computer deleted") ``` ```javascript JavaScript const response = await fetch(`https://www.orgo.ai/api/computers/${computerId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${apiKey}` } }); const { success } = await response.json(); if (success) console.log('Computer deleted'); ``` ### Response ```json { "success": true } ``` ## Errors | Status | Body | When | |--------|------|------| | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `403` | `{ "error": "You do not have access to this workspace." }` | You are neither the owner nor a member of the computer's workspace. | | `403` | `{ "error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)." }` | You are a view-only member. | | `403` | `{ "error": "This API key cannot access this workspace (workspace_scope_mismatch)." }` | The API key is scoped to a different workspace. | | `404` | `{ "error": "Desktop not found" }` | No computer matches the id. | | `409` | `{ "error": "This computer is being moved to another host. Try again in a few minutes.", "code": "migrating" }` | The computer is being migrated between hosts. Retry after the move. | | `409` | `{ "error": "pool_mac_release_required", "message": "…" }` | The computer is a Mac managed by Orgo. Ask support to release it. | | `409` | `{ "error": "runpod_managed", "message": "…" }` | The computer runs on a dedicated machine Orgo manages directly. Contact support to remove it. | | `409` | `{ "error": "openclaw_enrollment_cleanup_required", "detail": "…" }` | OpenClaw monitoring is still enrolled on the computer. Disable and revoke the enrollment first. | | `409` | `{ "error": "teardown_in_flight", "detail": "…" }` | Another operation is already working on the computer. The `error` can also be `teardown_membership_changed`, `teardown_reconciliation_required`, or `teardown_unavailable`; `detail` says what to do. | | `500` | `{ "error": "Failed to delete computer" }` | The record could not be removed. | | `503` | `{ "error": "Service temporarily unavailable. The database is not accepting requests. Retry shortly." }` | The credential store is unavailable. Carries `Retry-After: 5`. | --- # Get VNC password Source: https://docs.orgo.ai/api-reference/computers/vnc-password Endpoint: GET /computers/{id}/vnc-password Summary: Get the credentials used to connect to a computer. Returns the computer's VNC credential and its Desktop API token. **Warning:** These values grant direct access to the computer's display, shell, and Desktop API. Treat them as secrets. They can change when a computer is started again from its archive or when a resize reports `connection_rotated`, so fetch them fresh rather than caching them. ## Path parameters - `id` (path, string, required) Computer UUID. This endpoint accepts the UUID only. Unlike the lifecycle endpoints, it does not resolve an `instance_id`, and passing one returns `500`. ## Response - `password` (string) The VNC credential. Use it as the password for a VNC client and as the `token` query parameter on the VNC and terminal WebSockets. - `desktop_api_token` (string) The Bearer token for the computer's Desktop API: `/bash`, `/events`, and the terminal. Usually the same string as `password`, but a computer that carries its own stamped token returns that instead, so send this field rather than reusing `password`. ## Access control Workspace owners and editors can read these values. Viewers cannot obtain control credentials. A workspace-scoped API key can only read them for computers in its own workspace. This endpoint answers every one of these refusals with `401`, not `403`, as listed in [Errors](#errors). ## Example ```bash cURL curl https://www.orgo.ai/api/computers/$COMPUTER_ID/vnc-password \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import requests response = requests.get( f"https://www.orgo.ai/api/computers/{computer_id}/vnc-password", headers={"Authorization": f"Bearer {api_key}"} ) body = response.json() vnc_password = body["password"] desktop_api_token = body["desktop_api_token"] ``` ```javascript JavaScript const response = await fetch(`https://www.orgo.ai/api/computers/${computerId}/vnc-password`, { headers: { 'Authorization': `Bearer ${apiKey}` } }); const { password, desktop_api_token: desktopApiToken } = await response.json(); ``` ### Response ```json { "password": "0a017c595fa7689753a3", "desktop_api_token": "0a017c595fa7689753a3" } ``` ## Usage - **VNC connections**: pass `password` as the password in any websockify-compatible VNC client, such as noVNC. - **VNC and terminal WebSockets**: pass `password` as the `token` query parameter (see the [Terminal WebSocket](https://docs.orgo.ai/api-reference/computers/terminal)). - **Desktop API through Orgo**: send your API key or `password` as the Bearer token on `https://www.orgo.ai/api/desktops/{instance_id}/proxy/{endpoint}`. Orgo swaps an API key for the computer's credential before forwarding. **Tip:** For programmatic control, use the [Computer Actions](https://docs.orgo.ai/api-reference/computers/click) endpoints instead of VNC. For interactive shell access, use the [Terminal WebSocket](https://docs.orgo.ai/api-reference/computers/terminal). ## Errors | Status | Body | When | |--------|------|------| | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `401` | `{ "error": "You do not have access to this workspace." }` | You are not a member of the computer's workspace. | | `401` | `{ "error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)." }` | You have view-only access to the workspace. | | `401` | `{ "error": "This API key cannot access this workspace (workspace_scope_mismatch)." }` | The API key is scoped to a different workspace. | | `401` | `{ "error": "Service temporarily unavailable. …" }` | Orgo could not verify the credential because of a server-side fault. Retry; the key is fine. | | `402` | `{ "error": "Choose a plan to continue." }` | The computer is a trial computer whose trial has ended. | | `404` | `{ "error": "Desktop not found" }` | No computer matches the UUID. | | `404` | `{ "error": "Password not available" }` | The computer has no stored credential yet, such as a record still being provisioned. | | `500` | `{ "error": "Failed to retrieve password" }` | The lookup or decryption failed, including when `id` is not a UUID. | --- # Apply secrets Source: https://docs.orgo.ai/api-reference/computers/sync-secrets Endpoint: POST /computers/{id}/secrets/sync Summary: Apply selected vault secrets to an existing Linux computer. Send `{"names":["ANTHROPIC_API_KEY"]}` to apply those names from your vault to a running Linux computer. `names` takes 1 to 50 environment variable names made of letters, digits, and underscores, not starting with a digit, up to 64 characters each. Duplicates are ignored. Workspace values take precedence over account values. This requires write access to the computer's workspace. The response contains `success`, `names`, `restart_required: true`, and a `message`. The values are written to `/root/.orgo-secrets.env`, which `/root/.env`, `.bashrc`, and `.profile` load. Open a new terminal or restart the agent to load the updated environment. Existing processes keep their current environment. Applications using their own credential files may need to be configured separately. An invalid `names` list returns `400`. A missing secret returns `404` with `{ "error": "A requested secret is not available in your vault for this workspace." }`. A computer that is not running returns `409`; a different operating system returns `422`. A failure to write the values on the computer returns `502` with `{ "error": "Could not apply secrets. Check the computer connection and try again." }`. A missing or invalid API key returns `401`. An unknown computer returns `404` with `{ "error": "Desktop not found" }`. No access to the computer's workspace, view-only access, or an API key scoped to another workspace returns `403`. A trial computer whose trial has ended returns `402` with `{ "error": "Choose a plan to continue." }`. If Orgo cannot verify the credential because of a server-side fault, such as a database outage, the request returns `503` with `{ "error": "Service temporarily unavailable. …" }` and `Retry-After: 5`. Retry after that delay; do not rotate the key. Secret values are never included in the response. Saving or deleting a vault entry does not automatically change computers already running. Deleting a vault entry also does not erase a copy previously installed on a computer. --- # Start computer Source: https://docs.orgo.ai/api-reference/computers/start Endpoint: POST /computers/{id}/start Summary: Boot a stopped computer. Starts a computer that is `frozen` or `stopped`. A `frozen` computer's archived disk is restored, including files, installed software, and configuration, and it cold-boots on a host chosen for it. A `stopped` computer whose virtual machine is still on its host is booted there from its own disk instead. **Info:** A computer started from its archive gets a **new `instance_id`**, and it can land on a different host with a different address and ports. Orgo asks for the previous ports and VNC password back, but does not guarantee them. Re-fetch [`GET /computers/{id}`](https://docs.orgo.ai/api-reference/computers/get) afterwards. Only the disk is restored: processes and other in-memory state from before the stop are gone. The call can take considerably longer than a normal request while the saved disk is fetched and the computer boots. ## Path parameters - `id` (path, string, required) Computer UUID or `instance_id`. Both resolve to the same computer. A `frozen` computer has no `instance_id`, so address it by UUID. ## Query parameters - `async` (query, string, default `false`) `true` or `false`. `true` returns `202` immediately instead of waiting, as described in [Return immediately and poll](#return-immediately-and-poll). Any other value returns `400`. ## Response - `success` (boolean) `true` when the start succeeded or the computer was already running. - `reconciled` (boolean) Present and `true` only when the computer's record said `stopped` but the computer was still running on its host, so the record was corrected instead of a second copy being booted. No boot happened. - `restarted` (boolean) Present and `true` only when a `stopped` computer was booted on its existing host from its own disk, rather than restored from its archive. Starting is idempotent for an already-`running` computer: it returns `{ "success": true }` and changes nothing. Only `frozen` and `stopped` computers are startable. Every other status, including `suspended`, returns `409`. ## Example ```bash cURL curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/start \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import requests response = requests.post( f"https://www.orgo.ai/api/computers/{computer_id}/start", headers={"Authorization": f"Bearer {api_key}"} ) if response.json().get("success"): print("Computer starting") ``` ```javascript JavaScript const response = await fetch(`https://www.orgo.ai/api/computers/${computerId}/start`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}` } }); const { success } = await response.json(); if (success) console.log('Computer starting'); ``` ### Response ```json { "success": true } ``` ## Errors | Status | Body | When | |--------|------|------| | `400` | `{ "error": "Expected a JSON object with an optional boolean async field." }` | The body is not empty and is not a JSON object with an optional boolean `async`. | | `400` | `{ "error": "async must be true or false" }` | The `async` query parameter is neither `true` nor `false`. | | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `402` | `{ "error": "Your plan does not include running computers. Upgrade to start this computer.", "code": "upgrade_required" }` | The workspace owner's plan no longer funds running computers. The archived disk is kept; upgrading re-enables start. For a `stopped` computer booted on its host, the message ends "Upgrade to restart this computer." | | `402` | `{ "error": "Choose a plan to continue." }` | The computer is a trial computer whose trial has ended. | | `403` | `{ "error": "You do not have access to this workspace." }` | You are not a member of the computer's workspace. | | `403` | `{ "error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)." }` | You have view-only access to the workspace. | | `403` | `{ "error": "This API key cannot access this workspace (workspace_scope_mismatch)." }` | The API key is scoped to a different workspace. | | `404` | `{ "error": "Desktop not found" }` | No computer matches the id. | | `409` | `{ "error": "Computer cannot be started (status: creating)", "code": "not_startable" }` | The computer is in a status other than `frozen`, `stopped`, or `running`. | | `409` | `{ "error": "No saved state found for this computer", "code": "NO_SAVED_STATE" }` | The computer has no archived disk to restore. | | `409` | `{ "error": "…", "code": "SAVED_STATE_OUTDATED" }` | The computer's virtual machine is gone and its newest archive is older than its last disk, so starting would lose work. Contact support. | | `409` | `{ "error": "This computer is paused. Resume it to continue.", "code": "COMPUTER_SUSPENDED" }` | A `stopped` computer turned out to be suspended on its host. | | `409` | `{ "error": "…", "code": "NOT_ON_HOST" }` | The computer has no archive to start from (a macOS, iOS, or Android computer, or a device) and its host no longer has it. Contact support. | | `409` | `{ "error": "…", "code": "DEVICE_LIFECYCLE_UNSUPPORTED" }` | The computer runs on a dedicated machine Orgo manages directly. Contact support. | | `409` | `{ "error": "This Mac is managed by Orgo. Ask support to release it.", "code": "pool_mac_release_required" }` | The computer is a Mac that has been released back to Orgo. | | `409` | `{ "error": "…", "code": "migrating" }` | The computer is being moved to another host. Retry in a few minutes. | | `409` | `{ "error": "A different operation is already in progress.", "code": "OPERATION_IN_PROGRESS" }` | A stop is still pending for this computer. | | `409` | `{ "error": "The previous operation was interrupted and is being checked. Try again in a few minutes.", "code": "OPERATION_NEEDS_REVIEW" }` | An earlier start or stop is in `needs_review`. | | `500` | `{ "error": "…", "code": "…" }` | The start failed for an unclassified reason. Check the operation status before retrying; an uncertain result is fenced as `needs_review`. | | `503` | `{ "error": "…", "code": "HOST_UNREACHABLE" }` | The host of a `stopped` computer did not answer. Retry in a minute. | | `503` | `{ "error": "…", "code": "…" }` | No host could take the boot. `code` is one of `golden_unavailable`, `frozen_disabled`, `NO_TEMPLATE_HOST`, `FLEET_CAPACITY_FULL`, `OS_UNAVAILABLE`, `DEDICATED_CAPACITY_FULL`, or `GPU_HOST_UNAVAILABLE`. | | `503` | `{ "error": "Service temporarily unavailable. …" }` | Orgo could not verify the credential because of a server-side fault. The response carries `Retry-After: 5`. Retry after that delay. | A failure that happens while the operation runs also carries `operation_id` and `poll_url` in its body. ## Return immediately and poll Send `?async=true`, a JSON body `{ "async": true }`, or the header `Prefer: respond-async` to return `202` immediately. The response includes `id` (the computer UUID), `status` (`starting` or `stopping`), `operation_id`, `operation`, and `poll_url`, plus `Location` and `Retry-After: 2` headers. Repeating the same action while it is pending returns the same operation. An opposite action returns `409` with code `OPERATION_IN_PROGRESS`. Fetch `poll_url` with the same authentication until the operation reports `succeeded`, `failed`, or `needs_review`. A `needs_review` result means the host's outcome is uncertain. Orgo checks the computer and settles the operation on its own; until then, a new start or stop returns `409` with code `OPERATION_NEEDS_REVIEW`. See [Get computer operation](https://docs.orgo.ai/api-reference/computers/operation). Without the async option the request waits for completion, up to 300 seconds. If still pending, it returns `202` and the same polling fields. Both modes preserve disk data; stopping discards live processes and memory, and starting cold-boots with potentially different connection details. --- # Stop computer Source: https://docs.orgo.ai/api-reference/computers/stop Endpoint: POST /computers/{id}/stop Summary: Shut down a running computer. Stops a running computer. Its disk is archived to object storage, so files, installed software, and configuration are all kept. Its host and IP are released, so a stopped computer costs no compute. A successful stop leaves the computer in status `frozen`. That is the value [`GET /computers/{id}`](https://docs.orgo.ai/api-reference/computers/get) reports afterwards, and the state [start](https://docs.orgo.ai/api-reference/computers/start) restores from. **Info:** The archive is written and verified before the computer is torn down. If the response is lost or finalization fails, check the operation status: `needs_review` means the result must be reconciled before retrying. A stopped computer has no `instance_id` until it is started again, so address it by UUID. On [start](https://docs.orgo.ai/api-reference/computers/start) it gets a new `instance_id` and can land on a different host with a different address and ports. Processes and other in-memory state are **not** preserved. Only the disk survives. ## Path parameters - `id` (path, string, required) Computer UUID or `instance_id`. Both resolve to the same computer. ## Query parameters - `async` (query, string, default `false`) `true` or `false`. `true` returns `202` immediately instead of waiting, as described in [Return immediately and poll](#return-immediately-and-poll). Any other value returns `400`. ## Response - `success` (boolean) `true` when the computer was stopped, or was already `frozen`. Stopping is idempotent only for a computer that is already `frozen`. Any other non-`running` status returns `409` rather than succeeding. That includes `stopped`, which Orgo records when a computer's virtual machine has stopped or is gone from its host. Only Linux and Windows computers can be stopped. Any other operating system, or a physical device, returns `400` with code `STOP_UNSUPPORTED`. ## Example ```bash cURL curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/stop \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import requests response = requests.post( f"https://www.orgo.ai/api/computers/{computer_id}/stop", headers={"Authorization": f"Bearer {api_key}"} ) if response.json().get("success"): print("Computer stopped") ``` ```javascript JavaScript const response = await fetch(`https://www.orgo.ai/api/computers/${computerId}/stop`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}` } }); const { success } = await response.json(); if (success) console.log('Computer stopped'); ``` ### Response ```json { "success": true } ``` ## Errors | Status | Body | When | |--------|------|------| | `400` | `{ "error": "…", "code": "STOP_UNSUPPORTED" }` | The computer's operating system or hardware cannot be archived. `code` is `DEVICE_LIFECYCLE_UNSUPPORTED` for a device Orgo manages directly. | | `400` | `{ "error": "Expected a JSON object with an optional boolean async field." }` | The body is not empty and is not a JSON object with an optional boolean `async`. | | `400` | `{ "error": "async must be true or false" }` | The `async` query parameter is neither `true` nor `false`. | | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `402` | `{ "error": "Choose a plan to archive this computer.", "code": "TRIAL_ARCHIVE_UNAVAILABLE" }` | The computer is a trial computer. | | `402` | `{ "error": "Choose a plan to continue." }` | The computer is a trial computer whose trial has ended. | | `403` | `{ "error": "You do not have access to this workspace." }` | You are not a member of the computer's workspace. | | `403` | `{ "error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)." }` | You have view-only access to the workspace. | | `403` | `{ "error": "This API key cannot access this workspace (workspace_scope_mismatch)." }` | The API key is scoped to a different workspace. | | `404` | `{ "error": "Desktop not found" }` | No computer matches the id. | | `409` | `{ "error": "Computer is not running (status: stopped)", "code": "not_stoppable" }` | The computer is in any non-`running` status other than `frozen`. | | `409` | `{ "error": "…", "code": "migrating" }` | The computer is being moved to another host. Retry in a few minutes. | | `409` | `{ "error": "A different operation is already in progress.", "code": "OPERATION_IN_PROGRESS" }` | A start is still pending for this computer. | | `409` | `{ "error": "The previous operation was interrupted and is being checked. Try again in a few minutes.", "code": "OPERATION_NEEDS_REVIEW" }` | An earlier start or stop is in `needs_review`. | | `500` | `{ "error": "…", "code": "…" }` | The archive or teardown failed. Check the operation status before retrying; an uncertain result is fenced as `needs_review`. | | `503` | `{ "error": "Computer not ready. VMM has not assigned a server yet", "code": "NOT_READY" }` | The computer has no host on record yet. | | `503` | `{ "error": "…", "code": "frozen_disabled" }` | Archived-disk storage is not configured for this fleet. | | `503` | `{ "error": "Service temporarily unavailable. …" }` | Orgo could not verify the credential because of a server-side fault. The response carries `Retry-After: 5`. Retry after that delay. | A failure that happens while the operation runs also carries `operation_id` and `poll_url` in its body. A host that refuses the archive answers with its own `4xx` status and `code`. ## Return immediately and poll Send `?async=true`, a JSON body `{ "async": true }`, or the header `Prefer: respond-async` to return `202` immediately. The response includes `id` (the computer UUID), `status` (`starting` or `stopping`), `operation_id`, `operation`, and `poll_url`, plus `Location` and `Retry-After: 2` headers. Repeating the same action while it is pending returns the same operation. An opposite action returns `409` with code `OPERATION_IN_PROGRESS`. Fetch `poll_url` with the same authentication until the operation reports `succeeded`, `failed`, or `needs_review`. A `needs_review` result means the host's outcome is uncertain. Orgo checks the computer and settles the operation on its own; until then, a new start or stop returns `409` with code `OPERATION_NEEDS_REVIEW`. See [Get computer operation](https://docs.orgo.ai/api-reference/computers/operation). Without the async option the request waits for completion, up to 300 seconds. If still pending, it returns `202` and the same polling fields. Both modes preserve disk data; stopping discards live processes and memory, and starting cold-boots with potentially different connection details. --- # Restart computer Source: https://docs.orgo.ai/api-reference/computers/restart Endpoint: POST /computers/{id}/restart Summary: Reboot a running computer. Reboots a computer in place on the host it is already running on. The disk is untouched; memory and running processes are not. **Note:** A restart keeps the computer's ports and VNC password. Orgo rewrites the stored connection details from what the host reports afterwards, so re-fetch [`GET /computers/{id}`](https://docs.orgo.ai/api-reference/computers/get) if you need to be certain. Open VNC and terminal WebSockets drop during the reboot. A `frozen` computer has no host, so restarting it returns `500`. Use [start](https://docs.orgo.ai/api-reference/computers/start) instead. A `stopped` computer whose virtual machine is still on its host is booted there, which requires a plan that funds running computers. ## Path parameters - `id` (path, string, required) Computer UUID or `instance_id`. Both resolve to the same computer. ## Response - `success` (boolean) `true` when the computer restarted. The response is sent after the reboot completes, not when it is queued. ## Use cases - Recover from a hung or unresponsive computer - Reset the running environment ## Example ```bash cURL curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/restart \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import requests response = requests.post( f"https://www.orgo.ai/api/computers/{computer_id}/restart", headers={"Authorization": f"Bearer {api_key}"} ) if response.json().get("success"): print("Computer restarted") ``` ```javascript JavaScript const response = await fetch(`https://www.orgo.ai/api/computers/${computerId}/restart`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}` } }); const { success } = await response.json(); if (success) console.log('Computer restarted'); ``` ### Response ```json { "success": true } ``` ## Errors | Status | Body | When | |--------|------|------| | `400` | `{ "error": "…", "code": "DEVICE_LIFECYCLE_UNSUPPORTED" }` | The computer runs on a dedicated machine Orgo manages directly. Contact support. | | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `402` | `{ "error": "Your plan does not include running computers. Upgrade to restart this computer.", "code": "upgrade_required" }` | The computer is not `running` and the workspace owner's plan no longer funds running computers. | | `402` | `{ "error": "Choose a plan to continue." }` | The computer is a trial computer whose trial has ended. | | `403` | `{ "error": "You do not have access to this workspace." }` | You are not a member of the computer's workspace. | | `403` | `{ "error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)." }` | You have view-only access to the workspace. | | `403` | `{ "error": "This API key cannot access this workspace (workspace_scope_mismatch)." }` | The API key is scoped to a different workspace. | | `404` | `{ "error": "Desktop not found" }` | No computer matches the id. | | `409` | `{ "error": "This computer is paused. Resume it first, then restart it.", "code": "suspended" }` | The computer is `suspended`. | | `409` | `{ "error": "…", "code": "migrating" }` | The computer is being moved to another host. Retry in a few minutes. | | `409` | `{ "error": "…", "code": "NOT_ON_HOST" }` | The host no longer has the computer, and the computer has no archive to start from. Contact support. | | `409` | `{ "error": "pool_mac_release_required", "message": "This Mac is managed by Orgo. Ask support to release it." }` | The computer is a Mac that has been released back to Orgo. | | `500` | `{ "error": "…" }` | Every other failure: a computer with no live host, a host that refused or could not be reached, or an error during the reboot. The body carries the underlying message. | | `503` | `{ "error": "Service temporarily unavailable. …" }` | Orgo could not verify the credential because of a server-side fault. The response carries `Retry-After: 5`. Retry after that delay. | A failed restart changes the computer's status only to what its host confirms, such as `stopped` or `error`. When the host cannot be reached, the computer keeps the status it had before the call. --- # Get computer operation Source: https://docs.orgo.ai/api-reference/computers/operation Endpoint: GET /computers/{id}/operations/{operationId} Summary: Poll an asynchronous start or stop. Use the `poll_url` returned by an asynchronous [start](https://docs.orgo.ai/api-reference/computers/start) or [stop](https://docs.orgo.ai/api-reference/computers/stop). Authenticate each poll with the same API key. The response contains `id`, `desktop_id` (the computer UUID), `action` (`start` or `stop`), `status`, `result`, `error`, `created_at`, `updated_at`, and `poll_url`. `status` is `queued`, `running`, `succeeded`, `failed`, or `needs_review`. Poll every two seconds while queued or running; the response carries `Retry-After: 2`. A successful operation has a `result` such as `{ "success": true }`; failed operations have an `error` containing `error`, `code`, and `status`. `needs_review` means the host's answer was lost and the result is uncertain. Orgo checks the computer and settles the operation as `succeeded` or `failed` on its own, within about two hours at most. A second start or stop returns `409` until then, to avoid repeating an action that may already have completed. Operations are scoped to their computer and workspace. A missing or invalid API key returns `401` with `{ "error": "Authentication required" }` or `{ "error": "Invalid API key" }`. An unknown operation, an `operationId` that is not a UUID, or one belonging to another computer returns `404` with `{ "error": "Operation not found" }`. An unknown computer returns `404` with `{ "error": "Desktop not found" }`, and a workspace you cannot access returns `403`. A view-only member is also refused with `403` and `{ "error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)." }`: polling an operation counts as a write in the access check. A trial computer whose trial has ended returns `402` with `{ "error": "Choose a plan to continue." }`. If Orgo cannot verify the credential because of a server-side fault, such as a database outage, the poll returns `503` with `{ "error": "Service temporarily unavailable. …" }` and `Retry-After: 5`. Retry after that delay; do not rotate the key. --- # Take screenshot Source: https://docs.orgo.ai/api-reference/computers/screenshot Endpoint: GET /computers/{id}/screenshot Summary: Capture the display as a stored URL, inline base64, or image bytes. Captures the computer's display. By default it returns a stored image URL. Use `response_format=binary` for image bytes in the same response, or `response_format=base64` for inline JSON. Inline responses skip storage and a second download. ## Path parameters - `id` (path, string, required) Computer ID (UUID). An `instance_id` is not accepted here and fails with `500`. ## Query parameters - `screen` (query, string) Which [screen](https://docs.orgo.ai/api-reference/screens/list) to capture. Optional. Omit it to capture the screen the computer booted with. An unknown id returns `404` rather than falling back to the default screen. A Windows computer ignores this parameter. - `response_format` (query, string, default `url`) `url`, `base64`, or `binary`. Binary returns the image with its actual Content-Type. Base64 returns `{success, image, mime_type, width, height}`, where `image` is raw base64. - `format` (query, string) `png`, `jpeg`, or `webp`. Omit to preserve the captured format. - `quality` (query, integer, default `80`) An integer from 1 through 100. Applies when the image is re-encoded as JPEG or WebP, which happens when you request a `format` or a `scale` below 1. - `scale` (query, number, default `1`) Greater than 0 and at most 1. For example, 0.5 halves both dimensions. For a smaller response, request `?response_format=binary&format=webp&quality=75&scale=0.5`. ## URL response - `success` (boolean) Always `true` on a `200`. A failed capture is an error status, not `success: false`. - `image` (string) Path to the stored image, **relative to `https://www.orgo.ai`**. For example: `/api/storage/4d96f9a0-1c2b-4f3e-9a7d-8b5c6e0f1a2d/2026-04-20T12-00-00-000Z_9f2b7c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d.png`. It is not an absolute URL, so join it to the origin before fetching. The path does not expire, but it is not public: fetch it with the same `Authorization` header, as an account that can view the computer's workspace. A request without credentials returns `401`, and one without access returns `404`. A workspace-scoped key cannot fetch it at all (`403`), so use `response_format=base64` or `binary` with a scoped key. - `metadata` (object) Information about the stored image. **metadata fields** - `id` (string) Screenshot record ID (UUID). It also appears in `storage_path`, after the timestamp. - `timestamp` (string) ISO 8601 creation timestamp. - `size` (integer) Size of the stored image in bytes. - `storage_path` (string) Object key in storage: `/_.`, where the extension is `png`, `jpg`, or `webp`. `image` is this key served through `/api/storage/`. Stored filenames and Content-Type match the encoded image: `.png`, `.jpg`, or `.webp`. Inline responses use `Cache-Control: private, no-store`. ## Example ```bash cURL curl https://www.orgo.ai/api/computers/$COMPUTER_ID/screenshot \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import requests response = requests.get( f"https://www.orgo.ai/api/computers/{computer_id}/screenshot", headers={"Authorization": f"Bearer {api_key}"} ) data = response.json() # `image` is relative. Join it to the origin and send the same key. img = requests.get( "https://www.orgo.ai" + data["image"], headers={"Authorization": f"Bearer {api_key}"} ) with open("screenshot.png", "wb") as f: f.write(img.content) ``` ```javascript JavaScript import fs from 'node:fs'; const response = await fetch(`https://www.orgo.ai/api/computers/${computerId}/screenshot`, { headers: { 'Authorization': `Bearer ${apiKey}` } }); const { image } = await response.json(); // `image` is relative. Join it to the origin and send the same key. const img = await fetch(`https://www.orgo.ai${image}`, { headers: { 'Authorization': `Bearer ${apiKey}` } }); const buffer = Buffer.from(await img.arrayBuffer()); fs.writeFileSync('screenshot.png', buffer); ``` ### Response ```json { "success": true, "image": "/api/storage/4d96f9a0-1c2b-4f3e-9a7d-8b5c6e0f1a2d/2026-04-20T12-00-00-000Z_9f2b7c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d.png", "metadata": { "id": "9f2b7c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d", "timestamp": "2026-04-20T12:00:00.000Z", "size": 187342, "storage_path": "4d96f9a0-1c2b-4f3e-9a7d-8b5c6e0f1a2d/2026-04-20T12-00-00-000Z_9f2b7c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d.png" } } ``` ## Errors | Status | Meaning | |--------|---------| | `400` | Invalid response format, image format, quality, or scale. Also returned when the computer has no VM attached: `{"error": "Desktop instance not available"}`. Start it first. | | `401` | Invalid API key: `{"error": "Invalid API key"}`. A request with no `Authorization` header returns `{"error": "Authentication required"}`. Access failures also return `401` on this endpoint, not `403`: `{"error": "You do not have access to this workspace."}` when you are neither the owner nor a member of the computer's workspace, and `{"error": "This API key cannot access this workspace (workspace_scope_mismatch)."}` for a workspace-scoped key used outside its workspace. A server-side fault while verifying the credential also returns `401` here, with `{"error": "Service temporarily unavailable. …"}`. Retry that one; the key is fine. | | `402` | The computer was created as a free trial that is no longer active: `{"error": "Choose a plan to continue."}`. | | `404` | No computer with this id: `{"error": "Desktop not found"}`. Also returned when `?screen=` names a screen this computer does not have. | | `500` | The capture or the upload failed. | | `503` | The computer could not be reached: `{"error": "Could not reach the desktop. Try again in a moment.", "code": "ECONNREFUSED"}`. Safe to retry. A computer that never finished provisioning also returns `503`, with `{"error": "Computer not ready"}`. | Every failure raised after the request leaves the API layer also carries a `request_id`. Quote it in support requests. When the computer agent itself answered non-2xx, the body additionally carries `upstream_status`. ```json { "error": "Could not reach the desktop. Try again in a moment.", "request_id": "9f2b7c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d", "code": "ECONNREFUSED" } ``` --- # Click mouse Source: https://docs.orgo.ai/api-reference/computers/click Endpoint: POST /computers/{id}/click Summary: Click at given screen coordinates, optionally with modifiers or a repeat count. Moves the pointer to the given coordinates and clicks there. **Note:** On a Windows computer the request and response differ. The response is `{"ok": true}`. `double` uses the `button` you send rather than always the left one. An unrecognised `button`, a negative coordinate, or any `modifiers` or `repeat` field is refused with `400`, and the error body reads `Request failed with status code 400` rather than saying why. `?screen=` is ignored. ## Path parameters - `id` (path, string, required) Computer ID (UUID). An `instance_id` is not accepted here and fails with `500`. ## Query parameters - `screen` (query, string) Which [screen](https://docs.orgo.ai/api-reference/screens/list) to click on. Optional. Omit it to act on the screen the computer booted with. An unknown id returns `404` rather than falling back to the default screen. ## Body parameters - `x` (body, integer, required) X coordinate, in pixels from the left edge. Omitting it is not rejected. The click lands at `0`. - `y` (body, integer, required) Y coordinate, in pixels from the top edge. Omitting it is not rejected. The click lands at `0`. - `button` (body, string, default `left`) Which button to press: `left`, `right`, or `middle`. Any other value falls back to `left` without an error. - `double` (body, boolean, default `false`) Double-click instead of single-clicking. `double` always uses the left button, so `button` has no effect when it is set. The response still echoes the `button` you sent. - `modifiers` (body, string[]) Keys held down for the duration of the click: `shift` to extend a selection, `ctrl` to add to one. Optional; omit for a plain click. One to four of `ctrl` (alias `control`), `alt` (alias `option`), `shift`, and `super` (aliases `cmd`, `meta`, `win`). An unrecognised name rejects the whole request rather than dropping one modifier and performing a different gesture. Cannot be combined with `double` or with `repeat` above 1. - `repeat` (body, integer, default `1`) Number of clicks in rapid succession, so `3` is a triple-click. Optional; 1 through 10. Values of 1 or below behave as a single click. The repeat is counted on the computer, so the gap between presses stays under the toolkit's multi-click threshold no matter how slow the network is. Cannot be combined with `modifiers`. ## Response - `success` (boolean) Always `true` on a `200`. A refused or failed click is an error status. - `action` (string) `double_click` when `double` was set, `click` otherwise. - `details` (object) Echo of what was performed. Always carries `x`, `y`, and `button`. A plain click adds `double: false`; a repeat click adds `repeat`; a modified click adds `modifiers` with the names normalised (`option` comes back as `alt`). - `error` (null) Always `null` on a `200`. - `error_type` (null) Always `null` on a `200`. ## Example ```bash cURL # Left click at (100, 200) curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/click \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"x": 100, "y": 200}' # Right click curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/click \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"x": 100, "y": 200, "button": "right"}' # Double click curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/click \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"x": 100, "y": 200, "double": true}' # Shift-click to extend a selection curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/click \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"x": 100, "y": 200, "modifiers": ["shift"]}' # Triple click to select a line curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/click \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"x": 100, "y": 200, "repeat": 3}' ``` ```python Python import requests url = f"https://www.orgo.ai/api/computers/{computer_id}/click" headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} # Left click requests.post(url, headers=headers, json={"x": 100, "y": 200}) # Right click requests.post(url, headers=headers, json={"x": 100, "y": 200, "button": "right"}) # Double click requests.post(url, headers=headers, json={"x": 100, "y": 200, "double": True}) # Ctrl-click to add to a selection requests.post(url, headers=headers, json={"x": 100, "y": 200, "modifiers": ["ctrl"]}) # Triple click requests.post(url, headers=headers, json={"x": 100, "y": 200, "repeat": 3}) ``` ```javascript JavaScript const url = `https://www.orgo.ai/api/computers/${computerId}/click`; const headers = { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }; // Left click await fetch(url, { method: 'POST', headers, body: JSON.stringify({ x: 100, y: 200 }) }); // Right click await fetch(url, { method: 'POST', headers, body: JSON.stringify({ x: 100, y: 200, button: 'right' }) }); // Ctrl-click to add to a selection await fetch(url, { method: 'POST', headers, body: JSON.stringify({ x: 100, y: 200, modifiers: ['ctrl'] }) }); // Triple click await fetch(url, { method: 'POST', headers, body: JSON.stringify({ x: 100, y: 200, repeat: 3 }) }); ``` ### Response ```json { "success": true, "action": "click", "details": { "x": 100, "y": 200, "button": "left", "double": false }, "error": null, "error_type": null } ``` ## Errors | Status | Meaning | |--------|---------| | `400` | The computer has no VM attached: `{"error": "Desktop instance not available"}`. Also returned by the computer agent for `modifiers` combined with `double` or `repeat` above 1, for `repeat` above 10, for an unrecognised modifier name, for more than four modifiers, and when a body field has the wrong JSON type, such as `x` sent as a string rather than a number. | | `401` | Invalid API key: `{"error": "Invalid API key"}`. A request with no `Authorization` header returns `{"error": "Authentication required"}`. Access failures also return `401` on this endpoint, not `403`: `{"error": "You do not have access to this workspace."}` when you are neither the owner nor a member of the computer's workspace, `{"error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)."}` for a view-only member, and `{"error": "This API key cannot access this workspace (workspace_scope_mismatch)."}` for a workspace-scoped key used outside its workspace. A server-side fault while verifying the credential also returns `401`, with `{"error": "Service temporarily unavailable. The database is not accepting requests. Retry shortly."}`. Retry that one; the key is fine. | | `402` | The computer was created as a free trial that is no longer active: `{"error": "Choose a plan to continue."}`. | | `404` | No computer with this id: `{"error": "Desktop not found"}`. Also returned by the computer agent when `?screen=` names a screen this computer does not have, as in `{"error": "no screen \"2\"", "request_id": "…", "upstream_status": 404}`. | | `500` | The computer agent could not perform the click, or the request body is not valid JSON (with the parser's message in `error`). | | `503` | The computer could not be reached: `{"error": "Could not reach the desktop. Try again in a moment.", "code": "ECONNREFUSED"}`. Safe to retry. Also returned when the computer never finished provisioning: `{"error": "Computer not ready"}`. | Every failure raised after the request leaves the API layer also carries a `request_id`. Quote it in support requests. When the computer agent itself answered non-2xx, the body additionally carries `upstream_status`. ```json { "error": "double cannot be combined with modifiers", "request_id": "9f2b7c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d", "upstream_status": 400 } ``` --- # Move mouse Source: https://docs.orgo.ai/api-reference/computers/mouse-move Endpoint: POST /computers/{id}/mouse-move Summary: Move the pointer without pressing a button, to hover over menus, tooltips, and link previews. Moves the pointer to the given coordinates without pressing a button. Use it to hover: to open a menu that appears under the pointer, a tooltip, a link preview, or a toolbar that shows only on hover. [Click](https://docs.orgo.ai/api-reference/computers/click) and [drag](https://docs.orgo.ai/api-reference/computers/drag) also move the pointer, but both press a button. To see what the hover opened, take a [screenshot](https://docs.orgo.ai/api-reference/computers/screenshot) afterwards. **Note:** On a Windows computer the response is `{"ok": true}` and `?screen=` is ignored. A coordinate above 100000 is refused with `400`, and the error body reads `Request failed with status code 400` rather than saying why. This endpoint moves the pointer. To move a computer to another workspace, use [Move computer](https://docs.orgo.ai/api-reference/computers/move). ## Path parameters - `id` (path, string, required) Computer ID (UUID). An `instance_id` is not accepted here and fails with `500`. ## Query parameters - `screen` (query, string) Which [screen](https://docs.orgo.ai/api-reference/screens/list) to move the pointer on. Optional. Omit it to act on the screen the computer booted with. Each screen has its own pointer, so the pointer on every other screen stays where it is. An unknown id returns `404` rather than falling back to the default screen. ## Body parameters - `x` (body, integer, required) X coordinate, in pixels from the left edge. `0` is the left edge. - `y` (body, integer, required) Y coordinate, in pixels from the top edge. `0` is the top edge. Both coordinates are required and must be non-negative integers. A missing, negative, fractional, or non-numeric value, or a body that is not JSON, returns `400` before the computer is contacted. Any other field in the body is ignored. ## Response - `success` (boolean) Always `true` on a `200`. A failed move is an error status. - `action` (string) Always `mouse_move`. - `details` (object) Echo of the coordinates the pointer moved to: `x` and `y`. - `error` (null) Always `null` on a `200`. - `error_type` (null) Always `null` on a `200`. ## Example ```bash cURL # Hover at (640, 360) curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/mouse-move \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"x": 640, "y": 360}' # Hover on a second screen curl -X POST "https://www.orgo.ai/api/computers/$COMPUTER_ID/mouse-move?screen=screen-100" \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"x": 640, "y": 360}' ``` ```python Python import os import requests response = requests.post( f"https://www.orgo.ai/api/computers/{os.environ['COMPUTER_ID']}/mouse-move", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, json={"x": 640, "y": 360}, ) print(response.json()) ``` ```javascript JavaScript const response = await fetch( `https://www.orgo.ai/api/computers/${process.env.COMPUTER_ID}/mouse-move`, { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.ORGO_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ x: 640, y: 360 }), }, ); console.log(await response.json()); ``` ### Response ```json { "success": true, "action": "mouse_move", "details": { "x": 640, "y": 360 }, "error": null, "error_type": null } ``` ## Errors | Status | Meaning | |--------|---------| | `400` | The computer has no VM attached: `{"error": "Desktop instance not available"}`. A body that is not JSON, or an `x` or `y` that is missing or is not a non-negative integer, returns `{"error": "x and y are required non-negative integers"}`. | | `401` | Invalid API key: `{"error": "Invalid API key"}`. A request with no `Authorization` header returns `{"error": "Authentication required"}`. Access failures also return `401` on this endpoint, not `403`: `{"error": "You do not have access to this workspace."}` when you are neither the owner nor a member of the computer's workspace, `{"error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)."}` for a view-only member, and `{"error": "This API key cannot access this workspace (workspace_scope_mismatch)."}` for a workspace-scoped key used outside its workspace. A server-side fault while verifying the credential also returns `401`, with `{"error": "Service temporarily unavailable. The database is not accepting requests. Retry shortly."}`. Retry that one; the key is fine. | | `402` | The computer was created as a free trial that is no longer active: `{"error": "Choose a plan to continue."}`. A computer paid for by its own subscription or dedicated purchase whose payment has lapsed returns `{"error": "Manage this computer’s payment in Account → Usage."}`. | | `404` | No computer with this id: `{"error": "Desktop not found"}`. Also returned by the computer agent when `?screen=` names a screen this computer does not have, as in `{"error": "no screen \"screen-101\"", "request_id": "…", "upstream_status": 404}`. | | `500` | The computer agent could not move the pointer, with its message in `error` and `upstream_status: 500`. Also returned when `id` is not a computer UUID. | | `503` | The computer could not be reached: `{"error": "Could not reach the desktop. Try again in a moment.", "request_id": "…", "code": "ECONNREFUSED"}`. Safe to retry. Also returned when the computer never finished provisioning: `{"error": "Computer not ready", "request_id": "…"}`. | Every failure raised after the request leaves the API layer also carries a `request_id`. Quote it in support requests. When the computer agent itself answered non-2xx, the body additionally carries `upstream_status`. ```json { "error": "no screen \"screen-101\"", "request_id": "9f2b7c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d", "upstream_status": 404 } ``` --- # Drag mouse Source: https://docs.orgo.ai/api-reference/computers/drag Endpoint: POST /computers/{id}/drag Summary: Press the left button at one coordinate, move, and release at another. Presses the left mouse button at the start coordinate, moves to the end coordinate, and releases. The drag is one continuous motion; there is no way to set its speed, and on Linux the button is always the left one. **Note:** On a Windows computer the response is `{"ok": true}`, an optional `button` field (`left`, `right`, or `middle`) picks the button, and `?screen=` is ignored. ## Path parameters - `id` (path, string, required) Computer ID (UUID). An `instance_id` is not accepted here and fails with `500`. ## Query parameters - `screen` (query, string) Which [screen](https://docs.orgo.ai/api-reference/screens/list) to drag on. Optional. Omit it to act on the screen the computer booted with. An unknown id returns `404` rather than falling back to the default screen. ## Body parameters All four coordinates are required, and each must be a non-negative integer. `0` is a valid coordinate. A missing, negative, or non-integer coordinate returns `400`. - `start_x` (body, integer, required) X coordinate the drag starts at, in pixels from the left edge. - `start_y` (body, integer, required) Y coordinate the drag starts at, in pixels from the top edge. - `end_x` (body, integer, required) X coordinate the drag ends at, in pixels from the left edge. - `end_y` (body, integer, required) Y coordinate the drag ends at, in pixels from the top edge. ## Response - `success` (boolean) Always `true` on a `200`. A failed drag is an error status. - `action` (string) Always `drag`. - `details` (object) Echo of the four coordinates that were used. - `error` (null) Always `null` on a `200`. - `error_type` (null) Always `null` on a `200`. ## Example ```bash cURL curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/drag \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "start_x": 100, "start_y": 100, "end_x": 300, "end_y": 200 }' ``` ```python Python import requests requests.post( f"https://www.orgo.ai/api/computers/{computer_id}/drag", headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, json={ "start_x": 100, "start_y": 100, "end_x": 300, "end_y": 200 } ) ``` ```javascript JavaScript await fetch(`https://www.orgo.ai/api/computers/${computerId}/drag`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ start_x: 100, start_y: 100, end_x: 300, end_y: 200 }) }); ``` ### Response ```json { "success": true, "action": "drag", "details": { "start_x": 100, "start_y": 100, "end_x": 300, "end_y": 200 }, "error": null, "error_type": null } ``` ## Errors | Status | Meaning | |--------|---------| | `400` | A coordinate is missing, negative, or not an integer, including a number sent as a string: `{"error": "start_x, start_y, end_x, and end_y are required non-negative integers"}`. Also returned when the computer has no VM attached: `{"error": "Desktop instance not available"}`. | | `401` | Invalid API key: `{"error": "Invalid API key"}`. A request with no `Authorization` header returns `{"error": "Authentication required"}`. Access failures also return `401` on this endpoint, not `403`: `{"error": "You do not have access to this workspace."}` when you are neither the owner nor a member of the computer's workspace, `{"error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)."}` for a view-only member, and `{"error": "This API key cannot access this workspace (workspace_scope_mismatch)."}` for a workspace-scoped key used outside its workspace. A server-side fault while verifying the credential also returns `401`, with `{"error": "Service temporarily unavailable. The database is not accepting requests. Retry shortly."}`. Retry that one; the key is fine. | | `402` | The computer was created as a free trial that is no longer active: `{"error": "Choose a plan to continue."}`. | | `404` | No computer with this id: `{"error": "Desktop not found"}`. Also returned by the computer agent when `?screen=` names a screen this computer does not have, as in `{"error": "no screen \"2\"", "request_id": "…", "upstream_status": 404}`. | | `500` | The computer agent could not perform the drag, or the request body is not valid JSON (with the parser's message in `error`). | | `503` | The computer could not be reached: `{"error": "Could not reach the desktop. Try again in a moment.", "code": "ECONNREFUSED"}`. Safe to retry. Also returned when the computer never finished provisioning: `{"error": "Computer not ready"}`. | Every failure raised after the request leaves the API layer also carries a `request_id`. Quote it in support requests. When the computer agent itself answered non-2xx, the body additionally carries `upstream_status`. ```json { "error": "start_x, start_y, end_x, and end_y are required non-negative integers" } ``` --- # Type text Source: https://docs.orgo.ai/api-reference/computers/type Endpoint: POST /computers/{id}/type Summary: Type a string on the computer keyboard, one character at a time. Types text on the computer keyboard. Each character is sent as an individual keystroke, so unicode characters and printable symbols are supported. ## Path parameters - `id` (path, string, required) Computer ID (UUID). An `instance_id` is not accepted here and fails with `500`. ## Query parameters - `screen` (query, string) Which [screen](https://docs.orgo.ai/api-reference/screens/list) to type on. Optional. Omit it to act on the screen the computer booted with. An unknown id returns `404` rather than falling back to the default screen. A Windows computer ignores this parameter. ## Body parameters - `text` (body, string, required) Text to type. Unicode is supported. Nothing checks that it is present: omitting it types the empty string and still returns `200` (with `text_length: 0` on Linux), so a body that lost its `text` looks like a success. - `delay_ms` (body, integer, default `12`) Delay between keystrokes, in milliseconds. Optional. Omitting it, or sending `0`, uses 12. Lower values type faster; higher values help with inputs that drop fast keystrokes. A Windows computer refuses this field with `400`. **Note:** The API waits at most 30 seconds for the computer to finish typing. The text takes about `delay_ms` per character, so at the default of 12 ms anything over roughly 2,500 characters returns `503` with `code: "ECONNABORTED"`, and the text may be only partly typed. Split long text into several requests. **Note:** A Windows computer ignores `?screen=` and accepts only `text` in the body. Any other field, including `delay_ms`, returns `400`, and so does `text` longer than 20,000 bytes. A successful response is `{"ok": true}`. ## Response - `success` (boolean) Always `true` on a `200`. A failed keystroke is an error status. - `action` (string) Always `type`. - `details` (object) Carries `text_length`, the length of the typed text in **bytes**, not characters. A non-ASCII character counts as more than one. ## Example ```bash cURL curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/type \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "Hello, world!"}' ``` ```python Python import requests requests.post( f"https://www.orgo.ai/api/computers/{computer_id}/type", headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, json={"text": "Hello, world!"} ) ``` ```javascript JavaScript await fetch(`https://www.orgo.ai/api/computers/${computerId}/type`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ text: 'Hello, world!' }) }); ``` ### Response ```json { "success": true, "action": "type", "details": { "text_length": 13 }, "error": null, "error_type": null } ``` **Tip:** Use [Press key](https://docs.orgo.ai/api-reference/computers/key) for special keys like Enter, Tab, or keyboard shortcuts. ## Errors | Status | Meaning | |--------|---------| | `400` | The computer has no VM attached: `{"error": "Desktop instance not available"}`. Also returned by the computer agent when `text` is not a JSON string. A Windows computer also returns it for any field other than `text` or for `text` over 20,000 bytes, as `{"error": "Request failed with status code 400"}` with `upstream_status: 400`. A missing `text` is **not** a `400`: it is a `200` that types nothing. | | `401` | Invalid API key: `{"error": "Invalid API key"}`. A request with no `Authorization` header returns `{"error": "Authentication required"}`. Access failures also return `401` on this endpoint, not `403`: `{"error": "You do not have access to this workspace."}` when you are neither the owner nor a member of the computer's workspace, `{"error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)."}` for a view-only member, and `{"error": "This API key cannot access this workspace (workspace_scope_mismatch)."}` for a workspace-scoped key used outside its workspace. A server-side fault while verifying the credential also returns `401` here, with `{"error": "Service temporarily unavailable. …"}`. Retry that one; the key is fine. | | `402` | The computer was created as a free trial that is no longer active: `{"error": "Choose a plan to continue."}`. | | `404` | No computer with this id: `{"error": "Desktop not found"}`. Also returned when `?screen=` names a screen this computer does not have. | | `500` | The computer agent could not type the text, or the request body is not valid JSON. | | `503` | The computer could not be reached: `{"error": "Could not reach the desktop. Try again in a moment.", "code": "ECONNREFUSED"}`. Safe to retry. A computer that never finished provisioning also returns `503`, with `{"error": "Computer not ready"}`. Text that takes longer than 30 seconds to type also lands here, with `code: "ECONNABORTED"`; it may be only partly typed, so check before retrying. | Every failure raised after the request leaves the API layer also carries a `request_id`. Quote it in support requests. When the computer agent itself answered non-2xx, the body additionally carries `upstream_status`. ```json { "error": "typing text: exit status 1", "request_id": "9f2b7c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d", "upstream_status": 500 } ``` --- # Press key Source: https://docs.orgo.ai/api-reference/computers/key Endpoint: POST /computers/{id}/key Summary: Press a single key or a key combination like ctrl+c. Presses a key or key combination. Join a combination with `+`, for example `ctrl+shift+t`. **Note:** On a Windows computer the response is `{"ok": true}`. Key names are not case-sensitive and follow Windows naming, so `Page_Up` and `Page_Down` are spelled `pageup` and `pagedown`; the other keys in the tables below work as written. A missing or unrecognised key returns `400` rather than `500`, and the error body reads `Request failed with status code 400` rather than saying why. `?screen=` is ignored. ## Path parameters - `id` (path, string, required) Computer ID (UUID). An `instance_id` is not accepted here and fails with `500`. ## Query parameters - `screen` (query, string) Which [screen](https://docs.orgo.ai/api-reference/screens/list) to press the key on. Optional. Omit it to act on the screen the computer booted with. An unknown id returns `404` rather than falling back to the default screen. ## Body parameters - `key` (body, string, required) Key or key combination to press. On Linux the value is passed through to the computer's keyboard driver unchanged, so it must be an X11 keysym name. Keysym names are case-sensitive. Nothing validates the field before it gets there: a missing or misspelled key surfaces as a `500`, not a `400`. ### Common keys Use the keysym name, not the label printed on the key. `Return` and `BackSpace` are the two that most often catch people out. | Key | Description | |-----|-------------| | `Return` | Enter/Return key | | `Tab` | Tab key | | `Escape` | Escape key | | `BackSpace` | Backspace key | | `Delete` | Forward delete | | `space` | Space bar | | `Up`, `Down`, `Left`, `Right` | Arrow keys | | `Home`, `End` | Home/End keys | | `Page_Up`, `Page_Down` | Page navigation | | `F1`-`F12` | Function keys | ### Common combinations | Combination | Description | |-------------|-------------| | `ctrl+c` | Copy | | `ctrl+v` | Paste | | `ctrl+a` | Select all | | `ctrl+s` | Save | | `alt+Tab` | Switch windows | | `alt+F4` | Close window | | `ctrl+shift+t` | Reopen closed tab | The modifier names are `ctrl`, `alt`, `shift`, and `super`. ## Response - `success` (boolean) Always `true` on a `200`. A key the computer could not press is an error status. - `action` (string) Always `key_press`. - `details` (object) Carries `key`, an echo of what was sent. - `error` (null) Always `null` on a `200`. - `error_type` (null) Always `null` on a `200`. ## Example ```bash cURL # Press Return curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/key \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"key": "Return"}' # Ctrl+C curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/key \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"key": "ctrl+c"}' ``` ```python Python import requests url = f"https://www.orgo.ai/api/computers/{computer_id}/key" headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} # Press Return requests.post(url, headers=headers, json={"key": "Return"}) # Ctrl+C requests.post(url, headers=headers, json={"key": "ctrl+c"}) ``` ```javascript JavaScript const url = `https://www.orgo.ai/api/computers/${computerId}/key`; const headers = { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }; // Press Return await fetch(url, { method: 'POST', headers, body: JSON.stringify({ key: 'Return' }) }); // Ctrl+C await fetch(url, { method: 'POST', headers, body: JSON.stringify({ key: 'ctrl+c' }) }); ``` ### Response ```json { "success": true, "action": "key_press", "details": { "key": "Return" }, "error": null, "error_type": null } ``` ## Errors | Status | Meaning | |--------|---------| | `400` | The computer has no VM attached: `{"error": "Desktop instance not available"}`. Also returned by the computer agent when `key` is not a JSON string. A missing or unrecognised `key` is **not** a `400`: it is a `500`. | | `401` | Invalid API key: `{"error": "Invalid API key"}`. A request with no `Authorization` header returns `{"error": "Authentication required"}`. Access failures also return `401` on this endpoint, not `403`: `{"error": "You do not have access to this workspace."}` when you are neither the owner nor a member of the computer's workspace, `{"error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)."}` for a view-only member, and `{"error": "This API key cannot access this workspace (workspace_scope_mismatch)."}` for a workspace-scoped key used outside its workspace. A server-side fault while verifying the credential also returns `401`, with `{"error": "Service temporarily unavailable. The database is not accepting requests. Retry shortly."}`. Retry that one; the key is fine. | | `402` | The computer was created as a free trial that is no longer active: `{"error": "Choose a plan to continue."}`. | | `404` | No computer with this id: `{"error": "Desktop not found"}`. Also returned by the computer agent when `?screen=` names a screen this computer does not have, as in `{"error": "no screen \"2\"", "request_id": "…", "upstream_status": 404}`. | | `500` | The computer could not press the key. A missing, misspelled, or non-keysym value lands here. Also returned when the request body is not valid JSON, with the parser's message in `error`. | | `503` | The computer could not be reached: `{"error": "Could not reach the desktop. Try again in a moment.", "code": "ECONNREFUSED"}`. Safe to retry. Also returned when the computer never finished provisioning: `{"error": "Computer not ready"}`. | Every failure raised after the request leaves the API layer also carries a `request_id`. Quote it in support requests. When the computer agent itself answered non-2xx, the body additionally carries `upstream_status`. ```json { "error": "pressing key \"Enter\": exit status 1", "request_id": "9f2b7c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d", "upstream_status": 500 } ``` --- # Scroll Source: https://docs.orgo.ai/api-reference/computers/scroll Endpoint: POST /computers/{id}/scroll Summary: Move the pointer to a coordinate and scroll the wheel up or down there. Moves the pointer to `x`, `y` and scrolls the wheel there. A Windows computer scrolls at the pointer's current position instead. **Warning:** On Linux, always send `x` and `y`. The pointer is moved before every scroll, and omitting either coordinate moves it to `0` on that axis. A request of `{"direction": "down", "amount": 3}` warps the pointer to the top-left corner and scrolls whatever happens to be under it. On Linux there is no way to scroll at the pointer's current position through this endpoint. **Note:** A Windows computer scrolls at the pointer's current position and ignores `?screen=`. Send it only `direction` and `amount`, both required: `direction` must be `up` or `down` and `amount` from `1` to `50`. Any other field, including `x` and `y`, or a value outside those rules returns `400`. A successful response is `{"ok": true}`. ## Path parameters - `id` (path, string, required) Computer ID (UUID). An `instance_id` is not accepted here and fails with `500`. ## Query parameters - `screen` (query, string) Which [screen](https://docs.orgo.ai/api-reference/screens/list) to scroll on. Optional. Omit it to act on the screen the computer booted with. An unknown id returns `404` rather than falling back to the default screen. A Windows computer ignores this parameter. ## Body parameters - `x` (body, integer, required) X coordinate to move the pointer to before scrolling, in pixels from the left edge. Nominally optional, but omitting it moves the pointer to `x = 0`. See the warning above. A Windows computer refuses this field with `400`. - `y` (body, integer, required) Y coordinate to move the pointer to before scrolling, in pixels from the top edge. Nominally optional, but omitting it moves the pointer to `y = 0`. See the warning above. A Windows computer refuses this field with `400`. - `direction` (body, string, default `down`) Scroll direction: `up` or `down`. Optional on Linux: omitting it scrolls down, and so does any value other than the exact string `up`. Required on Windows. - `amount` (body, integer, default `1`) Number of wheel clicks. Optional on Linux: omitting it, or sending `0`, scrolls once, and a negative value moves the pointer, scrolls nothing, and still returns `200`. Required on Windows, from `1` to `50`. ## Response - `success` (boolean) Always `true` on a `200`. A failed scroll is an error status. - `action` (string) Always `scroll`. - `details` (object) Carries `direction` and `amount` after defaults are applied. The coordinates the pointer was moved to are not echoed back. ## Example ```bash cURL # Scroll down 3 clicks over (640, 400) curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/scroll \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"x": 640, "y": 400, "direction": "down", "amount": 3}' # Scroll up 10 clicks over the same point curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/scroll \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"x": 640, "y": 400, "direction": "up", "amount": 10}' ``` ```python Python import requests requests.post( f"https://www.orgo.ai/api/computers/{computer_id}/scroll", headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, json={"x": 640, "y": 400, "direction": "down", "amount": 3} ) ``` ```javascript JavaScript await fetch(`https://www.orgo.ai/api/computers/${computerId}/scroll`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ x: 640, y: 400, direction: 'down', amount: 3 }) }); ``` ### Response ```json { "success": true, "action": "scroll", "details": { "direction": "down", "amount": 3 }, "error": null, "error_type": null } ``` ## Errors | Status | Meaning | |--------|---------| | `400` | The computer has no VM attached: `{"error": "Desktop instance not available"}`. Also returned by the computer agent when a body field has the wrong JSON type, such as `amount` sent as a string rather than a number. A Windows computer also returns it for a body outside its rules, as `{"error": "Request failed with status code 400"}` with `upstream_status: 400`. | | `401` | Invalid API key: `{"error": "Invalid API key"}`. A request with no `Authorization` header returns `{"error": "Authentication required"}`. Access failures also return `401` on this endpoint, not `403`: `{"error": "You do not have access to this workspace."}` when you are neither the owner nor a member of the computer's workspace, `{"error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)."}` for a view-only member, and `{"error": "This API key cannot access this workspace (workspace_scope_mismatch)."}` for a workspace-scoped key used outside its workspace. A server-side fault while verifying the credential also returns `401` here, with `{"error": "Service temporarily unavailable. …"}`. Retry that one; the key is fine. | | `402` | The computer was created as a free trial that is no longer active: `{"error": "Choose a plan to continue."}`. | | `404` | No computer with this id: `{"error": "Desktop not found"}`. Also returned when `?screen=` names a screen this computer does not have. | | `500` | The computer agent could not move the pointer or scroll, or the request body is not valid JSON. | | `503` | The computer could not be reached: `{"error": "Could not reach the desktop. Try again in a moment.", "code": "ECONNREFUSED"}`. Safe to retry. A computer that never finished provisioning also returns `503`, with `{"error": "Computer not ready"}`. | Every failure raised after the request leaves the API layer also carries a `request_id`. Quote it in support requests. When the computer agent itself answered non-2xx, the body additionally carries `upstream_status`. ```json { "error": "moving mouse for scroll: exit status 1", "request_id": "9f2b7c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d", "upstream_status": 500 } ``` --- # Wait Source: https://docs.orgo.ai/api-reference/computers/wait Endpoint: POST /computers/{id}/wait Summary: Pause on the computer for a fixed number of seconds before the next action. Pauses on the computer for the given duration. The request blocks until the wait completes, so the round trip is at least as long as the pause you asked for. ## Path parameters - `id` (path, string, required) Computer ID (UUID). An `instance_id` is not accepted here and fails with `500`. ## Body parameters - `seconds` (body, number, required) How long to pause, in seconds. Must be greater than `0` and no more than `60`. Fractional values are allowed. Omitting it is the same as sending `0` and returns `400`. There is no default. - `duration` (body, number) Deprecated alias for `seconds`, accepted for backwards compatibility. It is read only when `seconds` is absent; if you send both, `seconds` wins and `duration` is discarded. Same range and same validation. **Warning:** Windows computers do not support `wait`. Their agent has no wait endpoint, so the request returns `404` with `upstream_status: 404`. Pause on your side instead. **Note:** `wait` takes no `?screen=` parameter. Unlike the pointer and keyboard actions, sending one here is silently ignored rather than rejected. The pause is not tied to a screen. ## Response - `success` (boolean) Always `true` on a `200`. - `action` (string) Always `wait`. - `details` (object) Carries `seconds`, an echo of the duration that was waited. ## Example ```bash cURL curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/wait \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"seconds": 2.0}' ``` ```python Python import requests # Wait 2 seconds requests.post( f"https://www.orgo.ai/api/computers/{computer_id}/wait", headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, json={"seconds": 2.0} ) ``` ```javascript JavaScript // Wait 2 seconds await fetch(`https://www.orgo.ai/api/computers/${computerId}/wait`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ seconds: 2.0 }) }); ``` ### Response ```json { "success": true, "action": "wait", "details": { "seconds": 2 }, "error": null, "error_type": null } ``` **Tip:** Use waits sparingly. Prefer polling for the condition you actually care about, such as window focus or a file change, over fixed delays. ## Errors | Status | Meaning | |--------|---------| | `400` | `seconds` is missing, `0`, or negative: `{"error": "wait duration must be positive"}`. Over 60 seconds returns `{"error": "wait duration 1m5s exceeds maximum of 1m0s"}`. Also returned when the computer has no VM attached (`{"error": "Desktop instance not available"}`), and by the computer agent when a body field has the wrong JSON type, such as `seconds` sent as a string rather than a number. | | `401` | Invalid API key: `{"error": "Invalid API key"}`. A request with no `Authorization` header returns `{"error": "Authentication required"}`. Access failures also return `401` on this endpoint, not `403`: `{"error": "You do not have access to this workspace."}` when you are neither the owner nor a member of the computer's workspace, `{"error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)."}` for a view-only member, and `{"error": "This API key cannot access this workspace (workspace_scope_mismatch)."}` for a workspace-scoped key used outside its workspace. A server-side fault while verifying the credential also returns `401` here, with `{"error": "Service temporarily unavailable. …"}`. Retry that one; the key is fine. | | `402` | The computer was created as a free trial that is no longer active: `{"error": "Choose a plan to continue."}`. | | `404` | No computer with this id: `{"error": "Desktop not found"}`. A Windows computer also returns `404` for every wait, with `upstream_status: 404`. | | `500` | The request body is not valid JSON, or the request failed for a reason the API could not classify. | | `503` | The computer could not be reached: `{"error": "Could not reach the desktop. Try again in a moment.", "code": "ECONNREFUSED"}`. Safe to retry. A computer that never finished provisioning also returns `503`, with `{"error": "Computer not ready"}`. | Every failure raised after the request leaves the API layer also carries a `request_id`. Quote it in support requests. When the computer agent itself answered non-2xx, the body additionally carries `upstream_status`. ```json { "error": "wait duration must be positive", "request_id": "9f2b7c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d", "upstream_status": 400 } ``` --- # Execute bash Source: https://docs.orgo.ai/api-reference/computers/bash Endpoint: POST /computers/{id}/bash Summary: Run a bash command on the computer and get its output. On Linux, runs Bash and returns combined stdout and stderr. Linux commands run with `HOME=/root`, `DISPLAY=:99`, and `PATH=/usr/local/bin:/usr/bin:/bin`. `:99` is the boot screen. To run on another screen, pass `?screen=`, described in [Query parameters](#query-parameters). On Windows, this endpoint runs PowerShell, despite its name. Use PowerShell syntax. The command runs from the user's Desktop folder and honors `timeout` the same way. The response carries `stdout`, `stderr`, `exit_code`, and `output` (stdout followed directly by stderr) instead of the fields listed under [Response](#response), plus `error` when the command could not run. A command that runs out of time returns `200` with `exit_code: 124`, `error: "timeout"`, and the output captured so far. ## Path parameters - `id` (path, string, required) Computer ID (UUID). ## Query parameters - `screen` (query, string) Which [screen](https://docs.orgo.ai/api-reference/screens/list) to run the command on. Optional. Omit it, or send `default`, to run on the screen the computer booted with. For any other screen, the command runs with `DISPLAY` set to that screen's display, so a window it opens appears on that screen. An unknown id returns `404` and nothing runs. Only a Linux computer has more than one screen: on any other computer, every id except `default` returns `404`. **Note:** For a screen other than `default`, Orgo first checks that the screen exists, then prefixes your command with `export DISPLAY=:N;` for that screen's display: `:100` for `screen-100`. The rest of the command runs exactly as you sent it, and the `command` echoed in the response is yours, without the prefix. The body is checked before the screen, so an invalid `command` returns `400` even when the screen does not exist. ## Body parameters - `command` (body, string, required) A non-empty command string. Bash on Linux; PowerShell on Windows. - `timeout` (body, integer, default `200`) How long the command may run, in seconds. Optional, and clamped to 1-300 when you send it: a larger value silently becomes 300, and `0` or a non-numeric value becomes 10. Omitting it entirely allows the command 200 seconds. **Note:** Omitting `timeout` allows 200 seconds. The API proxy waits 30 seconds longer than the command timeout. Send an explicit timeout up to 300 seconds for a longer command. This is synchronous execution; it does not return a background job ID. ## Response - `success` (boolean) `true` whenever the command ran at all, regardless of what it exited with. Read `exit_code` to find out whether the command itself succeeded. - `action` (string) Always `bash`. - `command` (string) Echo of the command you sent. With `?screen=`, the `DISPLAY` prefix Orgo adds is left out. - `output` (string) Combined stdout and stderr, in that order, joined by a newline when both are non-empty. - `exit_code` (integer) The command's exit status, or `-1` when it was killed by a signal. A command killed by `timeout` returns `-1`. There is no separate timeout flag: a `-1` with truncated output is the only signal that the time ran out. - `error` (string | null) Always `null` on a `200`. A command that fails is reported through `exit_code` and `output`, not here. - `error_type` (string | null) Always `null` on a `200`. ## Example ```bash cURL curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/bash \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command": "ls -la /home/user"}' # Run on a second screen, where $DISPLAY is :100 curl -X POST "https://www.orgo.ai/api/computers/$COMPUTER_ID/bash?screen=screen-100" \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command": "echo $DISPLAY"}' ``` ```python Python import requests response = requests.post( f"https://www.orgo.ai/api/computers/{computer_id}/bash", headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, json={"command": "ls -la /home/user"} ) result = response.json() print(result["output"]) print(f"Exit code: {result['exit_code']}") ``` ```javascript JavaScript const response = await fetch(`https://www.orgo.ai/api/computers/${computerId}/bash`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ command: 'ls -la /home/user' }) }); const { output, exit_code } = await response.json(); console.log(output); console.log(`Exit code: ${exit_code}`); ``` ### Response ```json { "success": true, "action": "bash", "command": "ls -la /home/user", "output": "total 32\ndrwxr-xr-x 4 user user 4096 Jan 15 10:30 .\ndrwxr-xr-x 3 root root 4096 Jan 15 10:00 ..\n-rw-r--r-- 1 user user 220 Jan 15 10:00 .bashrc\ndrwxr-xr-x 2 user user 4096 Jan 15 10:30 Desktop\n", "exit_code": 0, "error": null, "error_type": null } ``` **Tip:** For Python code execution, use [Execute Python](https://docs.orgo.ai/api-reference/computers/exec) instead. ## Errors | Status | Meaning | |--------|---------| | `400` | The computer has no VM attached: `{"error": "Desktop instance not available"}`. A body that is not JSON returns `{"error": "Expected a JSON object"}`. A missing, empty, or non-string `command` returns `{"error": "command must be a non-empty string"}`. | | `401` | Invalid API key: `{"error": "Invalid API key"}`. A request with no `Authorization` header returns `{"error": "Authentication required"}`. Access failures also return `401` on this endpoint, not `403`: `{"error": "You do not have access to this workspace."}` when you are neither the owner nor a member of the computer's workspace, `{"error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)."}` for a view-only member, and `{"error": "This API key cannot access this workspace (workspace_scope_mismatch)."}` for a workspace-scoped key used outside its workspace. A server-side fault while verifying the credential also returns `401`, with `{"error": "Service temporarily unavailable. The database is not accepting requests. Retry shortly."}`. Retry that one; the key is fine. | | `402` | The computer is a trial computer whose trial is not active: `{"error": "Choose a plan to continue."}`. | | `404` | No computer with this id: `{"error": "Desktop not found"}`, plus a `hint` explaining that computer ids do not survive a rebuild. Also returned when `?screen=` names a screen this computer does not have, and then nothing runs: `{"error": "no screen \"screen-101\"", "request_id": "…", "upstream_status": 404}`. | | `500` | The command could not be started at all, with the computer's own message in `error` and `upstream_status: 500`. | | `503` | The computer could not be reached. The body is `{"error": "Could not reach the desktop. Try again in a moment."}` with a `code` and a diagnostic: `vm_status`, `desired_status`, `host_reachable`, `desktop_api_reachable`, and a `hint`. Safe to retry. Also returned when the computer never finished provisioning: `{"error": "Computer not ready"}`. | | `504` | The command outlived the time the API hop allows: `{"error": "The command exceeded the allowed time and was aborted.", "code": "timeout"}`. No output comes back. | A command that merely exits non-zero is **not** an error status: it is a `200` with a non-zero `exit_code`. Every failure raised after the request leaves the API layer carries a `request_id`. Quote it in support requests. When the computer agent itself answered non-2xx, the body additionally carries `upstream_status`. ```json { "error": "The command exceeded the allowed time and was aborted.", "request_id": "9f2b7c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d", "code": "timeout" } ``` --- # Execute Python Source: https://docs.orgo.ai/api-reference/computers/exec Endpoint: POST /computers/{id}/exec Summary: Run a Python snippet on the computer and capture its output. Runs Python code on the computer and returns its output. Each call is a fresh interpreter, so nothing carries over between requests. On Linux the code runs with `python3 -c`, `HOME=/root`, `DISPLAY=:99`, and `PATH=/usr/local/bin:/usr/bin:/bin`. On Windows, the code is written to a temporary file and run with `C:\Python312\python.exe` from the user's Desktop folder. The response carries `stdout`, `stderr`, `exit_code`, and `output` instead of the fields listed under [Response](#response), plus `error` when the code could not run. Code that runs out of time returns `200` with `exit_code: 124` and `error: "timeout"`. ## Path parameters - `id` (path, string, required) Computer ID (UUID). ## Body parameters - `code` (body, string, required) Python code to run. Missing, empty, or not a string returns `400`. - `timeout` (body, integer, default `10`) How long the code may run, in seconds. Optional, and always clamped to 1-300: a larger value silently becomes 300, and `0` or a non-numeric value becomes 10. Out-of-range values are never rejected. **Note:** `exec` takes no `?screen=` parameter. Unlike the pointer and keyboard actions, sending one here is silently ignored rather than rejected. The code always runs against the computer's default display. ## Response - `success` (boolean) `true` only when the interpreter exited zero. Code that raises returns `200` with `success: false`, not an error status. - `action` (string) Always `exec`. - `output` (string) Combined stdout and stderr, in that order, joined by a newline when both are non-empty. An uncaught exception's traceback arrives here, and it is the only place it appears. - `timeout` (boolean) Always `false`. Code killed by the timeout also returns `false`, so this field carries no information. Infer a timeout from `success: false` with truncated output instead. - `error` (string | null) Always `null` on a `200`, including when the code raises. Read `output` for the traceback. - `error_type` (string | null) Always `null` on a `200`. It never carries a Python exception class. Parse `output` if you need the exception name. ## Example ```bash cURL curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/exec \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "import os\nprint(os.getcwd())", "timeout": 10 }' ``` ```python Python import requests response = requests.post( f"https://www.orgo.ai/api/computers/{computer_id}/exec", headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, json={ "code": "import os\nprint(os.getcwd())", "timeout": 10 } ) result = response.json() # `output` carries the traceback on failure; `error` is always null here. print(result["output"]) if not result["success"]: print("Script exited non-zero") ``` ```javascript JavaScript const response = await fetch(`https://www.orgo.ai/api/computers/${computerId}/exec`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ code: 'import os\nprint(os.getcwd())', timeout: 10 }) }); const result = await response.json(); // `output` carries the traceback on failure; `error` is always null here. console.log(result.output); if (!result.success) { console.error('Script exited non-zero'); } ``` ### Successful response ```json { "success": true, "action": "exec", "output": "/home/user\n", "timeout": false, "error": null, "error_type": null } ``` ### Response when the code raises Still a `200`. The exception is in `output`; `error` and `error_type` stay `null`. ```json { "success": false, "action": "exec", "output": "Traceback (most recent call last):\n File \"\", line 1, in \nNameError: name 'undefined_var' is not defined\n", "timeout": false, "error": null, "error_type": null } ``` **Tip:** For shell commands, use [Execute bash](https://docs.orgo.ai/api-reference/computers/bash) instead. ## Errors | Status | Meaning | |--------|---------| | `400` | `code` is missing, empty, or not a string: `{"error": "Code is required and must be a string"}`. Also returned when the computer has no VM attached: `{"error": "Desktop instance not available"}`. | | `401` | Invalid API key: `{"error": "Invalid API key"}`. A request with no `Authorization` header returns `{"error": "Authentication required"}`. Access failures also return `401` on this endpoint, not `403`: `{"error": "You do not have access to this workspace."}` when you are neither the owner nor a member of the computer's workspace, `{"error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)."}` for a view-only member, and `{"error": "This API key cannot access this workspace (workspace_scope_mismatch)."}` for a workspace-scoped key used outside its workspace. A server-side fault while verifying the credential also returns `401`, with `{"error": "Service temporarily unavailable. The database is not accepting requests. Retry shortly."}`. Retry that one; the key is fine. | | `402` | The computer is a trial computer whose trial is not active: `{"error": "Choose a plan to continue."}`. | | `404` | No computer with this id: `{"error": "Desktop not found"}`, plus a `hint` explaining that computer ids do not survive a rebuild. | | `500` | The interpreter could not be started at all (with `upstream_status: 500`), or the request body is not valid JSON (with the parser's message in `error`). | | `501` | The computer is an iPhone, or a Mac whose agent has no `/exec`: `{"error": "…", "code": "EXEC_UNSUPPORTED_ON_DEVICE"}`. On a Mac, `error` says how to run the code through [Execute bash](https://docs.orgo.ai/api-reference/computers/bash) instead. | | `503` | The computer could not be reached. The body is `{"error": "Could not reach the desktop. Try again in a moment."}` with a `code` and a diagnostic: `vm_status`, `desired_status`, `host_reachable`, `desktop_api_reachable`, and a `hint`. Safe to retry. Also returned when the computer never finished provisioning: `{"error": "Computer not ready"}`. | | `504` | The call outlived the time the API hop allows: `{"error": "The command exceeded the allowed time and was aborted.", "code": "timeout"}`. No output comes back. | Code that raises is **not** an error status: it is a `200` with `success: false`. Every failure raised after the request leaves the API layer carries a `request_id`. Quote it in support requests. When the computer agent itself answered non-2xx, the body additionally carries `upstream_status`. ```json { "error": "Code is required and must be a string" } ``` --- # Terminal WebSocket Source: https://docs.orgo.ai/api-reference/computers/terminal Summary: Interactive PTY shell over WebSocket with real-time bidirectional terminal I/O. Connect to an interactive terminal session on a computer over WebSocket. You get a full PTY, so anything that works in a real shell works here: colours, curses apps, job control. ## Connection URL ```text wss://www.orgo.ai/desktops/{instance_id}/ws/terminal?token={password} ``` `{instance_id}` is the computer's `instance_id`, returned by [Create computer](https://docs.orgo.ai/api-reference/computers/create). It is also the last path segment of the computer's `connection_url`. ### Authentication Pass the computer's password as the `token` query parameter. It is the same password used for VNC. Retrieve it from [Get VNC password](https://docs.orgo.ai/api-reference/computers/vnc-password) before connecting. That endpoint takes the computer's UUID, not its `instance_id`. You need both identifiers: the UUID to fetch the password, the `instance_id` to build this URL. Send the password in `?token=`, from a server or a browser. An `Authorization` header does not work on this socket, whether it carries an API key or the password: orgo-web accepts it, but it is not forwarded to the computer, which authenticates only `?token=`. The connection opens and then closes with `1000`. An API key in `?token=` fails the same way, and from a browser (any connection with an `Origin` header) it is refused with `4001`. Never put an account key in browser code. A connection with no usable credential is closed with code `4001`. See [Close codes](#close-codes). ### Query parameters - `token` (query, string, required) Computer password, from [Get VNC password](https://docs.orgo.ai/api-reference/computers/vnc-password). Optional only when the request carries a signed-in orgo.ai session cookie, in which case orgo-web supplies the password for you. - `cols` (query, number, default `80`) Terminal width in columns. A missing or unparseable value falls back to 80. - `rows` (query, number, default `24`) Terminal height in rows. A missing or unparseable value falls back to 24. - `session` (query, string) Name of a persistent shell session. Must be 1-32 characters of `A-Z`, `a-z`, `0-9`, `-` or `_`. With a valid `session`, the shell survives disconnection: reconnecting with the same name re-attaches to the running session, with any long job still going. Omit it, or send a name that fails validation, and you get an ephemeral shell (`bash -l` on Linux, PowerShell on Windows) that is killed when the socket closes. ## Message protocol Messages are JSON text frames in both directions. ### Client → server messages **input** Send keyboard input to the terminal. ```json { "type": "input", "data": "ls -la\r" } ``` - `type` (body, string, required) Must be `"input"`. - `data` (body, string, required) The input string to send. Use `\r` for the Enter key. **resize** Resize the terminal dimensions. ```json { "type": "resize", "cols": 120, "rows": 40 } ``` - `type` (body, string, required) Must be `"resize"`. - `cols` (body, number, required) New number of columns. The resize is ignored unless both `cols` and `rows` are above `0`. - `rows` (body, number, required) New number of rows. The resize is ignored unless both `cols` and `rows` are above `0`. **ping** Ask for a `pong`. ```json { "type": "ping" } ``` **Note:** A text frame that is not valid JSON is written to the PTY verbatim, as raw keystrokes. This keeps older raw-mode clients working. A frame that parses as JSON but carries an unrecognised `type` is discarded silently. ### Server → client messages **output** Terminal output data. ```json { "type": "output", "data": "root@computer:~# " } ``` - `type` (string) Always `"output"`. - `data` (string) The terminal output. May contain ANSI escape codes for colours and formatting. **error** The shell could not be started. ```json { "type": "error", "message": "failed to start terminal: fork/exec /bin/bash: no such file or directory" } ``` - `type` (string) Always `"error"`. - `message` (string) Human-readable error description. This frame is followed immediately by a normal (`1000`) close, not by an error close code. Watch for it in your message handler. A client that only inspects close codes sees a clean disconnect and never learns why. **exit** The shell process has exited. ```json { "type": "exit", "code": 0 } ``` - `type` (string) Always `"exit"`. - `code` (number) Exit code of the shell process, or `-1` when it could not be determined. **pong** Response to a `ping`. ```json { "type": "pong" } ``` ## Examples ```javascript JavaScript // COMPUTER_ID is the computer's UUID. INSTANCE_ID is its instance_id, which // fills the /desktops/{instance_id} slot. Create computer returns both. const computerId = process.env.COMPUTER_ID; const instanceId = process.env.INSTANCE_ID; const apiKey = process.env.ORGO_API_KEY; // Step 1: Get the computer password. This endpoint takes the UUID. const res = await fetch( `https://www.orgo.ai/api/computers/${computerId}/vnc-password`, { headers: { 'Authorization': `Bearer ${apiKey}` } } ); const { password } = await res.json(); // Step 2: Connect with the password as token const ws = new WebSocket( `wss://www.orgo.ai/desktops/${instanceId}/ws/terminal?token=${password}&cols=80&rows=24` ); ws.onopen = () => { console.log('Connected to terminal'); // Example: run a command once the socket is open sendCommand('echo "Hello, World!"'); }; ws.onmessage = (event) => { const message = JSON.parse(event.data); switch (message.type) { case 'output': // Print the terminal output process.stdout.write(message.data); break; case 'error': console.error('Terminal error:', message.message); break; case 'exit': console.log('Shell exited with code:', message.code); break; } }; ws.onclose = (e) => { if (e.code === 4008) console.log('Disconnected for inactivity. Reconnect to resume'); }; // Send a command function sendCommand(command) { ws.send(JSON.stringify({ type: 'input', data: command + '\r' // \r for Enter key })); } // Resize terminal function resizeTerminal(cols, rows) { ws.send(JSON.stringify({ type: 'resize', cols, rows })); } ``` ```python Python import asyncio import websockets import json import os import requests async def connect_terminal(computer_id: str, instance_id: str, api_key: str, cols: int = 80, rows: int = 24): # Step 1: Get the computer password. This endpoint takes the UUID. res = requests.get( f"https://www.orgo.ai/api/computers/{computer_id}/vnc-password", headers={"Authorization": f"Bearer {api_key}"} ) password = res.json()["password"] # Step 2: Connect with the password as token. Adding &session=build gives a # shell that survives a reconnect. url = f"wss://www.orgo.ai/desktops/{instance_id}/ws/terminal?token={password}&cols={cols}&rows={rows}" async with websockets.connect(url) as ws: # Handle incoming messages async def receive_messages(): async for message in ws: data = json.loads(message) if data["type"] == "output": print(data["data"], end="", flush=True) elif data["type"] == "error": print(f"Error: {data['message']}") elif data["type"] == "exit": print(f"Shell exited with code: {data['code']}") break # Send a command async def send_command(command: str): await ws.send(json.dumps({ "type": "input", "data": command + "\r" })) # Start receiving messages in background receive_task = asyncio.create_task(receive_messages()) # Send commands await send_command("echo 'Hello, World!'") await send_command("ls -la") # Wait for output await asyncio.sleep(2) receive_task.cancel() # COMPUTER_ID is the UUID. INSTANCE_ID fills the /desktops/{instance_id} slot. asyncio.run(connect_terminal( os.environ["COMPUTER_ID"], os.environ["INSTANCE_ID"], os.environ["ORGO_API_KEY"] )) ``` ```typescript TypeScript interface TerminalMessage { type: 'output' | 'error' | 'exit' | 'pong'; data?: string; message?: string; code?: number; } class TerminalConnection { private ws!: WebSocket; static async connect(computerId: string, instanceId: string, apiKey: string, cols = 80, rows = 24) { const conn = new TerminalConnection(); // Step 1: Get the computer password. This endpoint takes the UUID. const res = await fetch( `https://www.orgo.ai/api/computers/${computerId}/vnc-password`, { headers: { 'Authorization': `Bearer ${apiKey}` } } ); const { password } = await res.json(); // Step 2: Connect with the password as token const url = `wss://www.orgo.ai/desktops/${instanceId}/ws/terminal?token=${password}&cols=${cols}&rows=${rows}`; conn.ws = new WebSocket(url); conn.ws.onmessage = (event) => { const message: TerminalMessage = JSON.parse(event.data); conn.handleMessage(message); }; // Wait for the socket to open, so send() can be called right away await new Promise((resolve, reject) => { conn.ws.onopen = () => resolve(); conn.ws.onerror = () => reject(new Error('Terminal connection failed')); }); return conn; } private handleMessage(message: TerminalMessage) { switch (message.type) { case 'output': console.log(message.data); break; case 'error': console.error('Error:', message.message); break; case 'exit': console.log('Exited with code:', message.code); break; } } send(data: string) { this.ws.send(JSON.stringify({ type: 'input', data })); } resize(cols: number, rows: number) { this.ws.send(JSON.stringify({ type: 'resize', cols, rows })); } ping() { this.ws.send(JSON.stringify({ type: 'ping' })); } close() { this.ws.close(); } } // COMPUTER_ID is the UUID. INSTANCE_ID fills the /desktops/{instance_id} slot. const terminal = await TerminalConnection.connect( process.env.COMPUTER_ID!, process.env.INSTANCE_ID!, process.env.ORGO_API_KEY! ); terminal.send('echo "Hello!"\r'); ``` ## Integration with xterm.js For browser-based terminal UIs, use [xterm.js](https://xtermjs.org/): ```javascript import { Terminal } from '@xterm/xterm'; import { FitAddon } from '@xterm/addon-fit'; import '@xterm/xterm/css/xterm.css'; // Initialize xterm.js const terminal = new Terminal({ cursorBlink: true, fontFamily: 'monospace', fontSize: 14, }); const fitAddon = new FitAddon(); terminal.loadAddon(fitAddon); terminal.open(document.getElementById('terminal')); fitAddon.fit(); // Step 1: Get the computer password. // // In a browser, have YOUR server call GET /api/computers/{id}/vnc-password with // the Orgo API key and hand the page only the password. That endpoint takes the // computer's UUID; the socket URL below takes its instance_id. An `sk_` key in // ?token= is refused whenever the request carries an Origin header, and it is a // full account credential besides. const computerId = 'a3bb189e-8bf9-3888-9912-ace4e6543002'; const instanceId = 'a3881618'; const { password } = await fetchPasswordFromYourBackend(computerId); // Step 2: Connect with the password as token const ws = new WebSocket( `wss://www.orgo.ai/desktops/${instanceId}/ws/terminal?token=${password}&cols=${terminal.cols}&rows=${terminal.rows}` ); // Handle output from server ws.onmessage = (event) => { const message = JSON.parse(event.data); if (message.type === 'output') { terminal.write(message.data); } }; // Send user input to server terminal.onData((data) => { ws.send(JSON.stringify({ type: 'input', data })); }); // Handle terminal resize window.addEventListener('resize', () => { fitAddon.fit(); ws.send(JSON.stringify({ type: 'resize', cols: terminal.cols, rows: terminal.rows })); }); ``` ## Best practices **Heartbeat** Send a `ping` every 30 seconds to detect a dead connection early. Each `ping` is a message you send, so it also counts as activity for the idle timeout. **Reconnection** Reconnect with exponential backoff, starting at 2 seconds. Pair it with a `session` name so the shell you reconnect to is the one you left. **Resize events** Send a `resize` whenever the terminal container changes size, so text wraps correctly. **ANSI support** Output contains ANSI escape codes. Use a library like xterm.js that handles them. **Note:** The terminal WebSocket gives direct root shell access. To run one command programmatically, use [Execute bash](https://docs.orgo.ai/api-reference/computers/bash) instead. ## Idle disconnect An idle terminal is closed with code `4008`. Traffic in either direction counts as activity, so a long build streaming output keeps the socket open even with nobody typing. Activity is shared per computer and caller, so input you send over a VNC connection to the same computer keeps this one alive too. In production the window is 30 minutes. Reconnect to resume; with a `session` name your shell is still there. ## Close codes | Code | Meaning | |------|---------| | `1000` | Normal closure. Also what you get after an `error` or `exit` frame, when the shell process ends, and when the computer rejects the credential (see [Authentication](#authentication)). | | `1001` | orgo-web is draining for a restart or deploy. Reconnect. | | `1006` | Dropped with no close frame. The server terminates a socket that stops answering its 30-second protocol ping. | | `4001` | Authentication required. No session cookie and no valid `token`, a cookie-authenticated browser connecting from a disallowed origin, or a caller whose workspace role is view-only. | | `4003` | The computer exists but is not running or not ready, or its workspace is not active. Also sent to an open connection when your workspace access changes or a trial ends. Start the computer, then reconnect. | | `4004` | No computer with that id. | | `4006` | The API key is valid but scoped to a different workspace. | | `4007` | MFA is required for this session. Re-challenge, then reconnect. | | `4008` | Idle timeout. See [Idle disconnect](#idle-disconnect). | | `4500` | Internal proxy error. | | `4502` | orgo-web has no route to the computer's host. Retry; if it persists, contact support. | | `4503` | orgo-web could not look up the computer. Retry. | A URL that is not a `/desktops/{instance_id}/ws/...` path is refused before the WebSocket opens, so it gets no close code. --- # Audio WebSocket Source: https://docs.orgo.ai/api-reference/computers/audio Summary: Stream a computer's speaker over WebSocket as low-latency PCM frames. Stream real-time audio from a computer's virtual speaker over WebSocket. Audio is captured from the computer's speaker output (PulseAudio on Linux, WASAPI loopback on Windows) and delivered as raw PCM in 20 ms frames. That is low-latency enough for live monitoring. ## Connection URL ```text wss://www.orgo.ai/desktops/{instance_id}/ws/audio?token={password} ``` `{instance_id}` is the computer's `instance_id`, returned by [Create computer](https://docs.orgo.ai/api-reference/computers/create). It is also the last path segment of the computer's `connection_url`. ### Authentication Pass the computer's password as the `token` query parameter. Retrieve it from [Get VNC password](https://docs.orgo.ai/api-reference/computers/vnc-password) before connecting. That endpoint takes the computer's UUID, not its `instance_id`. You need both identifiers: the UUID to fetch the password, the `instance_id` to build this URL. Send the password in `?token=`, from a server or a browser. An `Authorization` header does not work on this socket, whether it carries an API key or the password: orgo-web accepts it, but it is not forwarded to the computer, which authenticates only `?token=`. The connection opens and then closes with `1000`. An API key in `?token=` fails the same way, and from a browser (any connection with an `Origin` header) it is refused with `4001`. Never put an account key in browser code. A connection with no usable credential is closed with code `4001`. See [Close codes](#close-codes). ### Query parameters - `token` (query, string, required) Computer password, from [Get VNC password](https://docs.orgo.ai/api-reference/computers/vnc-password). Optional only when the request carries a signed-in orgo.ai session cookie, in which case orgo-web supplies the password for you. - `sample_rate` (query, number, default `24000`) Capture sample rate in Hz. On Linux any positive integer is accepted. On Windows only 8000 to 48000 is accepted. A missing, unparseable, or out-of-range value falls back to 24000. - `channels` (query, number, default `1`) Number of channels. Only `1` (mono) and `2` (stereo) are accepted; any other value is ignored and the default of 1 is used. ## Audio format | Property | Value | |----------|-------| | Encoding | `s16le` (signed 16-bit little-endian PCM) | | Sample rate | 24,000 Hz by default | | Channels | 1 (mono) by default | | Frame duration | 20 ms | | Frame size | 960 bytes at the defaults | | Bitrate | ~48 KB/s at the defaults | Raw PCM: no codec, no container. Frame size follows the settings you request: `sample_rate × channels × 2 × 0.02` bytes. ## Protocol ### Connection flow 1. Client connects with `?token=`. 2. Server validates the credential. 3. Server sends a JSON text frame echoing the configuration actually in use: ```json { "type": "started", "sample_rate": 24000, "channels": 1 } ``` 4. Server starts capture and sends **binary frames** of raw PCM. If capture fails to start, an `error` text frame arrives instead and the connection closes normally. 5. Client decodes and plays via the Web Audio API or any PCM-capable player. ### Client → server messages **stop** Stop capture and close the connection. ```json { "type": "stop" } ``` **ping** Ask for a `pong`. ```json { "type": "ping" } ``` **Note:** A frame that parses as JSON but carries an unrecognised `type` is discarded silently. On Linux, a frame that is not valid JSON gets an `error` reply with the message `invalid JSON`, and the connection stays open. On Windows it is discarded silently. ### Server → client messages **started** Sent immediately after the connection opens, before capture begins. Echoes the configuration in use, which is your requested values after validation. ```json { "type": "started", "sample_rate": 24000, "channels": 1 } ``` - `type` (string) Always `"started"`. - `sample_rate` (number) Sample rate in Hz. - `channels` (number) Number of channels. **Binary frames** Raw PCM audio data. Each binary frame contains signed 16-bit little-endian samples. At the default 24 kHz mono, each 20 ms frame is 960 bytes (480 samples × 2 bytes per sample). **pong** Response to a `ping`. ```json { "type": "pong" } ``` **error** Capture could not be started, or (on Linux) a client frame was not valid JSON. ```json { "type": "error", "message": "failed to start audio capture: starting parec: executable file not found in $PATH" } ``` - `type` (string) Always `"error"`. - `message` (string) Human-readable error description. `invalid JSON` for an unparseable client frame, on Linux only. When capture could not start, `failed to start audio capture: …` on Linux and `audio capture unavailable: …` on Windows. A capture failure is followed immediately by a normal (`1000`) close, not by an error close code. Watch for this frame in your message handler. A client that only inspects close codes sees a clean disconnect and never learns why. An `invalid JSON` reply does not close the connection. ## Examples ```javascript JavaScript (Web Audio API) // COMPUTER_ID is the computer's UUID. INSTANCE_ID is its instance_id, which // fills the /desktops/{instance_id} slot. Create computer returns both. const computerId = process.env.COMPUTER_ID; const instanceId = process.env.INSTANCE_ID; const apiKey = process.env.ORGO_API_KEY; // Step 1: Get the computer password. This endpoint takes the UUID. const res = await fetch( `https://www.orgo.ai/api/computers/${computerId}/vnc-password`, { headers: { 'Authorization': `Bearer ${apiKey}` } } ); const { password } = await res.json(); // Step 2: Connect to the audio stream const ws = new WebSocket( `wss://www.orgo.ai/desktops/${instanceId}/ws/audio?token=${password}` ); ws.binaryType = 'arraybuffer'; // Step 3: Set up Web Audio API playback const ctx = new AudioContext({ sampleRate: 24000 }); let nextPlayTime = 0; ws.onmessage = (event) => { // Text frames are JSON control messages if (typeof event.data === 'string') { const msg = JSON.parse(event.data); if (msg.type === 'started') { console.log(`Audio stream: ${msg.sample_rate}Hz, ${msg.channels}ch`); } if (msg.type === 'error') { console.error('Audio error:', msg.message); } return; } // Binary frames are raw PCM audio const pcm = new Int16Array(event.data); const floats = new Float32Array(pcm.length); for (let i = 0; i < pcm.length; i++) { floats[i] = pcm[i] / 32768; } const buffer = ctx.createBuffer(1, floats.length, 24000); buffer.getChannelData(0).set(floats); const source = ctx.createBufferSource(); source.buffer = buffer; source.connect(ctx.destination); // Schedule with drift correction const now = ctx.currentTime; if (nextPlayTime < now || nextPlayTime > now + 0.15) { nextPlayTime = now + 0.02; } source.start(nextPlayTime); nextPlayTime += buffer.duration; }; ``` ```python Python import asyncio import websockets import json import os import struct import requests async def stream_audio(computer_id: str, instance_id: str, api_key: str): # Step 1: Get the computer password. This endpoint takes the UUID. res = requests.get( f"https://www.orgo.ai/api/computers/{computer_id}/vnc-password", headers={"Authorization": f"Bearer {api_key}"} ) password = res.json()["password"] # Step 2: Connect to the audio stream url = f"wss://www.orgo.ai/desktops/{instance_id}/ws/audio?token={password}" async with websockets.connect(url) as ws: async for message in ws: if isinstance(message, str): data = json.loads(message) if data["type"] == "started": print(f"Audio: {data['sample_rate']}Hz, {data['channels']}ch") elif data["type"] == "error": print(f"Error: {data['message']}") break else: # Binary frame: raw s16le PCM samples = struct.unpack(f"<{len(message)//2}h", message) # Process samples (save to file, play back, analyze, etc.) print(f"Received {len(samples)} audio samples") # COMPUTER_ID is the UUID. INSTANCE_ID fills the /desktops/{instance_id} slot. asyncio.run(stream_audio( os.environ["COMPUTER_ID"], os.environ["INSTANCE_ID"], os.environ["ORGO_API_KEY"] )) ``` ```typescript TypeScript interface AudioConfig { type: 'started'; sample_rate: number; channels: number; } class AudioStream { private ws!: WebSocket; private ctx!: AudioContext; private nextPlayTime = 0; static async connect(computerId: string, instanceId: string, apiKey: string) { const stream = new AudioStream(); // Get the computer password. This endpoint takes the UUID. const res = await fetch( `https://www.orgo.ai/api/computers/${computerId}/vnc-password`, { headers: { 'Authorization': `Bearer ${apiKey}` } } ); const { password } = await res.json(); // Connect stream.ctx = new AudioContext({ sampleRate: 24000 }); stream.ws = new WebSocket( `wss://www.orgo.ai/desktops/${instanceId}/ws/audio?token=${password}` ); stream.ws.binaryType = 'arraybuffer'; stream.ws.onmessage = (event) => { if (typeof event.data === 'string') { const msg = JSON.parse(event.data); if (msg.type === 'started') { console.log(`Audio: ${msg.sample_rate}Hz, ${msg.channels}ch`); } if (msg.type === 'error') { console.error('Audio error:', msg.message); } return; } stream.playPCM(event.data); }; return stream; } private playPCM(data: ArrayBuffer) { const pcm = new Int16Array(data); const floats = new Float32Array(pcm.length); for (let i = 0; i < pcm.length; i++) { floats[i] = pcm[i] / 32768; } const buffer = this.ctx.createBuffer(1, floats.length, 24000); buffer.getChannelData(0).set(floats); const source = this.ctx.createBufferSource(); source.buffer = buffer; source.connect(this.ctx.destination); const now = this.ctx.currentTime; if (this.nextPlayTime < now || this.nextPlayTime > now + 0.15) { this.nextPlayTime = now + 0.02; } source.start(this.nextPlayTime); this.nextPlayTime += buffer.duration; } stop() { this.ws.send(JSON.stringify({ type: 'stop' })); this.ws.close(); this.ctx.close(); } } // COMPUTER_ID is the UUID. INSTANCE_ID fills the /desktops/{instance_id} slot. const audio = await AudioStream.connect( process.env.COMPUTER_ID!, process.env.INSTANCE_ID!, process.env.ORGO_API_KEY! ); // Later: stop streaming audio.stop(); ``` ## Playback tips **Browser autoplay** Browsers require a user gesture before `AudioContext` can play. Create the context inside a click handler, or call `ctx.resume()` after a user interaction. **Drift correction** Schedule buffers slightly ahead of real time and reset when drift exceeds ~150 ms. This prevents gaps and keeps latency low. **Heartbeat** Send a `ping` every 30 seconds to detect a dead connection early. Each message you send, `ping` included, also counts as activity for the idle timeout. **Sample rate** The default 24 kHz mono suits voice and system sounds. Pass `sample_rate=48000` for higher fidelity, and size your playback buffers from the `started` frame rather than assuming the default. **Note:** Audio streaming requires the computer to be running; a stopped computer is closed with code `4003`. Audio is captured from the computer's speaker output, so any sound the computer produces is streamed: browser media, system alerts, application audio. ## Idle disconnect An idle audio socket is closed with code `4008`. The audio the server streams does not count as activity. Messages you send on this socket do, including `ping`. It also borrows the activity of the other connections you hold to the same computer: a VNC or terminal session you are actively using keeps it open. In production the window is 30 minutes. Reconnect to resume. ## Close codes | Code | Meaning | |------|---------| | `1000` | Normal closure. You sent `stop` or disconnected, capture failed to start after an `error` frame, or the computer rejected the credential (see [Authentication](#authentication)). | | `1001` | orgo-web is draining for a restart or deploy. Reconnect. | | `1006` | Dropped with no close frame. The server terminates a socket that stops answering its 30-second protocol ping. | | `4001` | Authentication required. No session cookie and no valid `token`, or a cookie-authenticated browser connecting from a disallowed origin. | | `4003` | The computer exists but is not running or not ready, or its workspace is not active. Also sent to an open connection when your workspace access changes or a trial ends. Start the computer, then reconnect. | | `4004` | No computer with that id. | | `4006` | The API key is valid but scoped to a different workspace. | | `4007` | MFA is required for this session. Re-challenge, then reconnect. | | `4008` | Idle timeout. See [Idle disconnect](#idle-disconnect). | | `4500` | Internal proxy error. | | `4502` | orgo-web has no route to the computer's host. Retry; if it persists, contact support. | | `4503` | orgo-web could not look up the computer. Retry. | --- # Events WebSocket Source: https://docs.orgo.ai/api-reference/computers/events Summary: Subscribe to desktop events: window focus, clipboard, processes, screen size, and idle state. Subscribe to real-time desktop events over WebSocket. Window focus changes, clipboard updates, process lifecycle, screen resolution changes, and idle/active state are pushed to your client as they happen. File change events are defined but not delivered on Linux today; see [File changes](#file-changes). ## Connection URL ```text wss://www.orgo.ai/desktops/{instance_id}/ws/events?token={password} ``` `{instance_id}` is the computer's `id` from [Create computer](https://docs.orgo.ai/api-reference/computers/create). Its instance id also resolves. ### Authentication Pass the computer's password as the `token` query parameter. Retrieve it from [Get VNC password](https://docs.orgo.ai/api-reference/computers/vnc-password) before connecting. Always authenticate with the computer password. orgo-web also accepts an API key, sent as `Authorization: Bearer $ORGO_API_KEY` or as `?token=`, but it does not forward the key to the computer. The computer then refuses the stream, and the connection closes with code `1000` right after it opens. An `sk_` key in the query string is refused outright, with `4001`, for any connection that carries an `Origin` header. A connection with no usable credential is closed with code `4001`. See [Close codes](#close-codes). ### Query parameters - `token` (query, string, required) Computer password, from [Get VNC password](https://docs.orgo.ai/api-reference/computers/vnc-password). ## Event catalog ### Available event types | Type | Description | Detection | |------|-------------|-----------| | `window_focus` | Active window changed | Polled every 100 ms | | `window_open` | New window opened | Polled every 100 ms | | `window_close` | Window closed | Polled every 100 ms | | `clipboard` | Clipboard content changed | Polled every 250 ms | | `file_change` | File created, modified, or deleted under a watched directory | `inotifywait -m -r` | | `screen_change` | Display resolution changed | Polled every 1 s | | `audio_stream_start` | An audio stream started playing | Polled every 500 ms | | `audio_stream_stop` | An audio stream stopped | Polled every 500 ms | | `process_start` | New process started | Polled every 2 s | | `process_stop` | Process exited | Polled every 2 s | | `idle` | No user input for 30 seconds | Polled every 1 s | | `active` | User input resumed after idle | Polled every 1 s | Watchers run for the life of the computer, independent of any connection. An event describes a change, never current state, so connecting does not replay what you missed. **Note:** The catalog, detection, and payloads on this page are a Linux computer's. A Windows computer emits only `window_focus`, `clipboard`, and `file_change`. There, `window_id` is a hexadecimal window handle such as `0x1a2b3c`, `clipboard` carries `text` and `length` instead of `content` and `truncated`, and `file_change` carries `action` (`created`, `deleted`, `modified`, `renamed_from`, `renamed_to`, or `unknown`), `path`, `directory`, `name`, and `is_hidden`. Windows `file_change` events are delivered, for the Administrator's Desktop and Downloads folders. ## Subscription model New connections receive **no events** until they send a `subscribe` message. This lets you opt in to only the event types you care about, reducing noise and bandwidth. Subscriptions are: - **Per-connection**: each WebSocket connection has its own subscription set - **Additive**: subscribe to more types at any time - **Selective**: unsubscribe from specific types without disconnecting **Warning:** Event type names are not validated. Subscribing to a name that does not exist, such as a typo or a type from a newer version, returns `subscribed` / `ok` and then silently delivers nothing. Check your names against the [catalog](#available-event-types). ## Message protocol ### Client → server messages **subscribe** Start receiving specific event types. Call it as many times as you like to add more. ```json { "type": "subscribe", "event_types": ["window_focus", "clipboard", "file_change"] } ``` - `type` (body, string, required) Must be `"subscribe"`. - `event_types` (body, string[], required) Event type names to add to this connection's filter. See the [event catalog](#available-event-types). Names are not validated. See the warning above. Omitting the field adds nothing and still returns `subscribed`. **unsubscribe** Stop receiving specific event types. ```json { "type": "unsubscribe", "event_types": ["clipboard"] } ``` - `type` (body, string, required) Must be `"unsubscribe"`. - `event_types` (body, string[], required) Event type names to remove from this connection's filter. Names not currently subscribed are ignored. **ping** Ask for a `pong`. ```json { "type": "ping" } ``` **Note:** A frame that parses as JSON but carries an unrecognised `type` is discarded silently. You get no reply and no error. ### Server → client messages **event** A desktop event matching one of your subscribed types. ```json { "type": "event", "event": { "type": "window_focus", "timestamp": "2026-03-03T12:00:00.123Z", "data": { "window_id": "52428804", "window_name": "Google Chrome" } } } ``` - `type` (string) Always `"event"`. - `event` (object) The event payload. - `event.type` (string) Event type name, for example `"window_focus"` or `"clipboard"`. - `event.timestamp` (string) ISO 8601 timestamp of when the event was published. - `event.data` (object) Event-specific data. See [Event data schemas](#event-data-schemas) below. **subscribed** Confirmation that your subscription was updated. Sent even when every name you passed was unknown. ```json { "type": "subscribed", "message": "ok" } ``` **unsubscribed** Confirmation that event types were removed from your subscription. ```json { "type": "unsubscribed", "message": "ok" } ``` **pong** Response to a `ping`. ```json { "type": "pong" } ``` **error** Your last frame was not valid JSON. This is the only `error` this endpoint sends, and the connection stays open. ```json { "type": "error", "message": "invalid JSON" } ``` - `type` (string) Always `"error"`. - `message` (string) Always `"invalid JSON"`. ## Event data schemas Each event carries a `data` object with type-specific fields. ### Window events ```json // window_focus { "window_id": "52428804", "window_name": "Google Chrome" } // window_open { "window_id": "52428808", "window_name": "Terminal" } // window_close { "window_id": "52428808" } ``` `window_id` is the X11 window id as a decimal string, as printed by `xdotool`. `window_name` is the X11 window title, as `xdotool getwindowname` prints it. `window_close` carries no name. The window is already gone by the time the change is noticed. ### Clipboard ```json // clipboard { "content": "copied text from the clipboard", "truncated": false } ``` - `content` (string) Clipboard text, trimmed of surrounding whitespace and capped at 1024 bytes. - `truncated` (boolean) `true` when the real clipboard was longer than 1024 bytes and `content` holds only its first 1024. Check this before treating `content` as the whole clipboard. ### File changes ```json // file_change { "path": "/root/Desktop/report.pdf", "operation": "CREATE" } { "path": "/root/Downloads/data.csv", "operation": "CLOSE_WRITE,CLOSE" } { "path": "/root/Desktop/old.txt", "operation": "DELETE" } ``` - `path` (string) Absolute path of the file that changed. - `operation` (string) The raw inotify event name in uppercase, for example `CREATE`, `MODIFY`, `DELETE`, `CLOSE_WRITE`, `MOVED_TO`, `MOVED_FROM`. inotify reports several flags for one change, in which case they arrive comma-joined with no spaces, as in `CLOSE_WRITE,CLOSE`. Match on substrings rather than on equality. Monitored directories are `/root/Desktop` and `/root/Downloads`, watched recursively. The desktop runs as root, so these are the desktop and downloads folders a user of the computer sees. Detection is via Linux inotify. **Warning:** On Linux, `file_change` events are not delivered today. The computer buffers the watcher's output until the watcher exits, and it never exits, so a `file_change` subscription receives nothing. To detect a finished download, poll the directory with [Execute bash](https://docs.orgo.ai/api-reference/computers/bash) instead. ### Screen ```json // screen_change { "width": 1920, "height": 1080 } ``` ### Audio ```json // audio_stream_start { "count": 1 } // audio_stream_stop { "count": 0 } ``` - `count` (integer) Number of active PulseAudio streams **after** the change. `audio_stream_start` fires when the count rises, `audio_stream_stop` when it falls. A second stream starting while one is already playing sends `audio_stream_start` with `count: 2`. ### Process lifecycle ```json // process_start { "pid": "1234", "name": "chrome" } // process_stop { "pid": "1234", "name": "chrome" } ``` `pid` is a string, not a number. `name` is the short command name from `ps -eo comm`, not a full command line. When that name contains a space, only its first word is sent. ### Idle / active ```json // idle { "idle_ms": 30000 } // active { "idle_ms": 15 } ``` - `idle_ms` (integer) Milliseconds since the last user input, as reported by `xprintidle` at the moment the transition was noticed. Present on **both** events: `idle` carries the elapsed idle time that crossed the 30-second threshold, and `active` carries the residual reading at the moment input resumed. There is no `idle_seconds` field. ## Examples ```javascript JavaScript // The computer's id fills the /desktops/{instance_id} slot. const instanceId = process.env.COMPUTER_ID; const apiKey = process.env.ORGO_API_KEY; // Step 1: Get the computer password const res = await fetch( `https://www.orgo.ai/api/computers/${instanceId}/vnc-password`, { headers: { 'Authorization': `Bearer ${apiKey}` } } ); const { password } = await res.json(); // Step 2: Connect to the events stream const ws = new WebSocket( `wss://www.orgo.ai/desktops/${instanceId}/ws/events?token=${password}` ); ws.onopen = () => { // Subscribe to the events you care about ws.send(JSON.stringify({ type: 'subscribe', event_types: ['window_focus', 'clipboard', 'file_change', 'idle', 'active'] })); }; ws.onmessage = (event) => { const msg = JSON.parse(event.data); switch (msg.type) { case 'event': const { type, timestamp, data } = msg.event; console.log(`[${timestamp}] ${type}:`, data); // React to specific events if (type === 'window_focus') { console.log(`Focus moved to: ${data.window_name}`); } if (type === 'clipboard') { console.log(`Clipboard: ${data.content}${data.truncated ? '…' : ''}`); } if (type === 'idle') { console.log(`Desktop idle for ${data.idle_ms / 1000}s`); } break; case 'subscribed': console.log('Subscription confirmed'); break; case 'error': console.error('Event error:', msg.message); break; } }; // Later: add more subscriptions ws.send(JSON.stringify({ type: 'subscribe', event_types: ['process_start', 'process_stop'] })); // Or unsubscribe from some ws.send(JSON.stringify({ type: 'unsubscribe', event_types: ['idle', 'active'] })); ``` ```python Python import asyncio import websockets import json import os import requests async def monitor_events(instance_id: str, api_key: str): # Step 1: Get the computer password res = requests.get( f"https://www.orgo.ai/api/computers/{instance_id}/vnc-password", headers={"Authorization": f"Bearer {api_key}"} ) password = res.json()["password"] # Step 2: Connect to the events stream url = f"wss://www.orgo.ai/desktops/{instance_id}/ws/events?token={password}" async with websockets.connect(url) as ws: # Subscribe to events await ws.send(json.dumps({ "type": "subscribe", "event_types": ["window_focus", "clipboard", "file_change"] })) # Listen for events async for message in ws: data = json.loads(message) if data["type"] == "event": event = data["event"] print(f"[{event['timestamp']}] {event['type']}: {event['data']}") if event["type"] == "file_change": d = event["data"] print(f" {d['operation']} on {d['path']}") elif data["type"] == "subscribed": print("Subscribed successfully") elif data["type"] == "error": print(f"Error: {data['message']}") # The computer's id fills the /desktops/{instance_id} slot. asyncio.run(monitor_events(os.environ["COMPUTER_ID"], os.environ["ORGO_API_KEY"])) ``` ```typescript TypeScript interface DesktopEvent { type: string; timestamp: string; data: Record; } interface EventMessage { type: 'event' | 'subscribed' | 'unsubscribed' | 'pong' | 'error'; event?: DesktopEvent; message?: string; } class EventStream { private ws!: WebSocket; private handlers: Map void> = new Map(); static async connect(instanceId: string, apiKey: string) { const stream = new EventStream(); // Get the computer password const res = await fetch( `https://www.orgo.ai/api/computers/${instanceId}/vnc-password`, { headers: { 'Authorization': `Bearer ${apiKey}` } } ); const { password } = await res.json(); // Connect stream.ws = new WebSocket( `wss://www.orgo.ai/desktops/${instanceId}/ws/events?token=${password}` ); stream.ws.onmessage = (event) => { const msg: EventMessage = JSON.parse(event.data); if (msg.type === 'event' && msg.event) { const handler = stream.handlers.get(msg.event.type); handler?.(msg.event); } }; return stream; } on(eventType: string, handler: (event: DesktopEvent) => void) { this.handlers.set(eventType, handler); // Auto-subscribe this.ws.send(JSON.stringify({ type: 'subscribe', event_types: [eventType] })); return this; } off(eventType: string) { this.handlers.delete(eventType); this.ws.send(JSON.stringify({ type: 'unsubscribe', event_types: [eventType] })); return this; } close() { this.ws.close(); } } // Usage: the computer's id fills the /desktops/{instance_id} slot. const events = await EventStream.connect( process.env.COMPUTER_ID!, process.env.ORGO_API_KEY! ); events .on('window_focus', (e) => console.log('Window:', e.data.window_name)) .on('clipboard', (e) => console.log('Clipboard:', e.data.content)) .on('file_change', (e) => console.log('File:', e.data.operation, e.data.path)) .on('idle', (e) => console.log('Idle for', Number(e.data.idle_ms) / 1000, 'seconds')); ``` ## Use cases **Agent awareness** Subscribe to `window_focus` and `idle` to give your agent context about what is on screen and when to act. **File monitoring** `file_change` is meant to detect completed downloads and saved documents, but it is not delivered on Linux today. See the warning under [File changes](#file-changes). **Clipboard sync** Monitor `clipboard` to mirror clipboard content between the computer and your application. **Process tracking** Use `process_start` and `process_stop` to track application lifecycle: when Chrome launches, when a build finishes. ## Best practices **Subscribe selectively** Only subscribe to what you need. It reduces message volume and keeps your handler simple. **Heartbeat** Send a `ping` every 30 seconds to detect a dead connection early. **Handle backpressure** Events are dropped, not queued, for slow consumers. The per-connection buffer is 256 events. Process quickly or offload to a queue. **Reconnection** Reconnect with exponential backoff and re-send your `subscribe` message. Subscriptions are per-connection and are not restored. **Note:** Events require the computer to be running; a stopped computer is closed with code `4003`. Unlike the terminal and audio streams, the events stream has no idle timeout, so a long-lived monitoring connection is not disconnected for inactivity. ## Close codes | Code | Meaning | |------|---------| | `1000` | Normal closure. Also sent right after opening when the computer refuses the credential, as happens when you authenticate with an API key instead of the computer password. | | `1001` | orgo-web is draining for a restart or deploy. Reconnect. | | `1006` | Dropped with no close frame. The server terminates a socket that stops answering its 30-second protocol ping. | | `4001` | Authentication required. No session cookie and no valid `token`, an `sk_` key in the query string from a browser, a view-only workspace role, or a cookie-authenticated browser connecting from a disallowed origin. | | `4003` | The computer exists but is not running, its workspace is not active, or its trial has ended. Start it, then reconnect. An open stream authenticated as a user is also closed with `4003` when that user's workspace access changes, which orgo-web checks every 15 seconds. | | `4004` | No computer with that id. | | `4006` | The API key is valid but scoped to a different workspace. | | `4007` | MFA is required for this session. Re-challenge, then reconnect. | | `4500` | Internal proxy error. | | `4502` | orgo-web has no route to the computer's host. Retry; if it persists, contact support. | | `4503` | orgo-web could not look the computer up. Retry. | --- # List screens Source: https://docs.orgo.ai/api-reference/screens/list Endpoint: GET /computers/{id}/screens Summary: Every screen on a computer. A Linux computer boots with one screen and can run up to four. Each screen is its own X server, with its own root window, its own cursor, and its own window manager. An agent working on one screen cannot disturb another. **Note:** Extra screens are a Linux feature. A macOS computer or an iPhone has exactly one screen, and this endpoint lists it as `default` with a `display` that is not an X display. A Windows computer has no screens endpoints, so it answers `404`. ## Screens after a restart Extra screens come back after a restart. Each time a [create](https://docs.orgo.ai/api-reference/screens/create), [resize](https://docs.orgo.ai/api-reference/screens/resize), or [destroy](https://docs.orgo.ai/api-reference/screens/delete) succeeds, Orgo records the computer's extra screens, with their ids and sizes, before it answers. After a [restart](https://docs.orgo.ai/api-reference/computers/restart), a [start](https://docs.orgo.ai/api-reference/computers/start), or a [resize](https://docs.orgo.ai/api-reference/computers/resize) that restarted the computer (`restarted: true`), Orgo makes them again with the same ids, so the same displays and ports. A [stop](https://docs.orgo.ai/api-reference/computers/stop) keeps the record with the computer's archive, so the start that follows brings them back too. A screen comes back empty. A restart ends every process, so the windows that were open on it are gone. The recreated screen has a window manager and nothing else, like a newly created one. Recreating screens is best effort: - It happens in the background. The restart or start answers without waiting for it. Orgo then waits up to three minutes for the computer to come up, and makes each screen it is missing. List the screens to see which are back. - A screen that cannot be made again is skipped, and the rest still come back. - If the computer stops running before it comes up, nothing is made. Screens are not made again after an OS update, after a backup restore, or after a reboot that did not go through Orgo, such as `reboot` run on the computer itself. A computer that Orgo stops because its owner's plan was downgraded does not keep them either. The boot screen is never recorded, so a size you give `default` with [resize](https://docs.orgo.ai/api-reference/screens/resize) lasts only until the next restart, when it is back at the size the computer boots with. ## Path parameters - `id` (path, string, required) Computer ID (UUID). The computer must be up. One that is still being created, or that has been frozen, returns `400`. ## Response - `screens` (array) Every screen on the computer, the boot screen first and the rest in display order. Never empty while the computer is up. **Screen** - `id` (string) Screen identifier. The boot screen is always `default`. A screen you create is named after its display, so `:100` is `screen-100`. Pass this value as `?screen=` on an action to target the screen. - `display` (string) X display. The boot screen is `:99`. Each created screen takes the lowest free display number from `:100` up, so a number freed by a destroyed screen is reused. - `width` (integer): Width in pixels. - `height` (integer): Height in pixels. - `vnc_port` (integer) VNC port. Derived from the display rather than looked up: 5900 plus the display number, so `:99` is 5999 and `:100` is 6000. - `ws_port` (integer) WebSocket port bridging that VNC stream: 5981 plus the display number, so `:99` is 6080 and `:100` is 6081. - `default` (boolean) `true` for the screen the computer booted with. It cannot be destroyed. ## Errors Failures raised before the request reaches the computer carry `error` alone. Failures relayed back from the computer add `request_id` and `upstream_status`. | Status | When | Body | |--------|------|------| | `400` | The computer has nothing running behind it: still being created, or frozen. | `{ "error": "Computer not available" }` | | `401` | The Bearer token starts with `sk_` but is not a known Orgo key. | `{ "error": "Invalid API key" }` | | `401` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | `{ "error": "Authentication required" }` | | `401` | You do not own the computer's workspace and are not a member of it. | `{ "error": "You do not have access to this workspace." }` | | `401` | The API key is scoped to a different workspace. | `{ "error": "This API key cannot access this workspace (workspace_scope_mismatch)." }` | | `401` | Orgo could not verify the credential because of a server-side fault. Retry. | `{ "error": "Service temporarily unavailable. …" }` | | `402` | The computer is a free trial computer that can no longer run. | `{ "error": "Choose a plan to continue." }` | | `402` | The computer is paid for by its own subscription or dedicated purchase, and that payment has lapsed. | `{ "error": "Manage this computer’s payment in Account → Usage." }` | | `404` | No computer with that ID. | `{ "error": "Desktop not found" }` | | `404` | The computer runs Windows, whose agent has no screens endpoint. | `{ "error": "…", "request_id": "…", "upstream_status": 404 }` | | `500` | Unexpected failure. | `{ "error": "…", "request_id": "…" }` | | `501` | A macOS computer or an iPhone whose agent cannot list screens, when Orgo has no screen size on file for it. | `{ "error": "…", "code": "SCREENS_UNSUPPORTED_OS" }` | | `503` | The computer could not be reached. | `{ "error": "Could not reach the desktop. Try again in a moment.", "request_id": "…", "code": "ECONNREFUSED" }` | ```bash cURL curl https://www.orgo.ai/api/computers/$COMPUTER_ID/screens \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```json 200 { "screens": [ { "id": "default", "display": ":99", "width": 1920, "height": 1080, "vnc_port": 5999, "ws_port": 6080, "default": true }, { "id": "screen-100", "display": ":100", "width": 1920, "height": 1080, "vnc_port": 6000, "ws_port": 6081, "default": false } ] } ``` --- # Create a screen Source: https://docs.orgo.ai/api-reference/screens/create Endpoint: POST /computers/{id}/screens Summary: Start another screen on the same computer. Starts another screen on a computer that is already running. Use a second screen when two agents need to work at the same time. X11 has a single pointer per display, so two agents sharing one screen are two hands on one mouse. **Note:** A Linux computer supports at most four screens. Each one is an X server plus a window manager plus whatever runs on it, so a computer runs out of memory long before it runs out of screens. **Note:** Extra screens are a Linux feature. A macOS computer or an iPhone has exactly one screen, so this endpoint answers `409` there. A Windows computer has no screens endpoints, so it answers `404`. ## Path parameters - `id` (path, string, required) Computer ID (UUID). The computer must be up. One that is still being created, or that has been frozen, returns `400`. ## Body parameters - `width` (body, integer) Width in pixels. Send `width` and `height` together or send neither. - `height` (body, integer) Height in pixels. Send `width` and `height` together or send neither. **Warning:** If either dimension is missing or zero, **both** fall back to the size of the boot screen. The value you did send is discarded and no error is returned. On a 1920×1080 computer, `{"width": 1280}` returns a 1920×1080 screen, not a 1280×1080 one. Send an empty body to match the screen the computer booted with. That is almost always the size you want for a second screen. ## Response Returns the new [screen](https://docs.orgo.ai/api-reference/screens/list) with `201`. Pass its `id` as `?screen=` to target it from [screenshot](https://docs.orgo.ai/api-reference/computers/screenshot), [click](https://docs.orgo.ai/api-reference/computers/click), [move mouse](https://docs.orgo.ai/api-reference/computers/mouse-move), [type](https://docs.orgo.ai/api-reference/computers/type), [key](https://docs.orgo.ai/api-reference/computers/key), [scroll](https://docs.orgo.ai/api-reference/computers/scroll), [drag](https://docs.orgo.ai/api-reference/computers/drag) and [bash](https://docs.orgo.ai/api-reference/computers/bash). An action sent without the parameter acts on the boot screen. Orgo records the new screen before it answers, so a restart or start brings it back with the same id, as described in [Screens after a restart](https://docs.orgo.ai/api-reference/screens/list#screens-after-a-restart). **Note:** A created screen gets a window manager and nothing else: no panel, no wallpaper, no icons. That is deliberate. Each of those costs memory on every screen, and an agent reads none of it. A screenshot of a fresh screen is an empty root window, which is what a working screen looks like before anything has been opened on it. ## Errors Failures raised before the request reaches the computer carry `error` alone. Failures relayed back from the computer add `request_id` and `upstream_status`. | Status | When | Body | |--------|------|------| | `400` | The computer has nothing running behind it: still being created, or frozen. | `{ "error": "Computer not available" }` | | `400` | `width` or `height` is not an integer. | `{ "error": "invalid JSON: …", "request_id": "…", "upstream_status": 400 }` | | `401` | The Bearer token starts with `sk_` but is not a known Orgo key. | `{ "error": "Invalid API key" }` | | `401` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | `{ "error": "Authentication required" }` | | `401` | You do not own the computer's workspace and are not a member of it. | `{ "error": "You do not have access to this workspace." }` | | `401` | You have view-only access to the workspace. | `{ "error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)." }` | | `401` | The API key is scoped to a different workspace. | `{ "error": "This API key cannot access this workspace (workspace_scope_mismatch)." }` | | `401` | Orgo could not verify the credential because of a server-side fault. Retry. | `{ "error": "Service temporarily unavailable. …" }` | | `402` | The computer is a free trial computer that can no longer run. | `{ "error": "Choose a plan to continue." }` | | `402` | The computer is paid for by its own subscription or dedicated purchase, and that payment has lapsed. | `{ "error": "Manage this computer’s payment in Account → Usage." }` | | `404` | No computer with that ID. | `{ "error": "Desktop not found" }` | | `404` | The computer runs Windows, whose agent has no screens endpoint. | `{ "error": "…", "request_id": "…", "upstream_status": 404 }` | | `409` | The computer is already at four screens, or the new screen failed to start. A macOS computer or an iPhone always answers this, because it has one screen. | `{ "error": "…", "request_id": "…", "upstream_status": 409 }` | | `500` | Unexpected failure. | `{ "error": "…", "request_id": "…" }` | | `503` | The computer could not be reached. | `{ "error": "Could not reach the desktop. Try again in a moment.", "request_id": "…", "code": "ECONNREFUSED" }` | ```bash cURL curl -X POST https://www.orgo.ai/api/computers/$COMPUTER_ID/screens \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ```json 201 { "id": "screen-100", "display": ":100", "width": 1920, "height": 1080, "vnc_port": 6000, "ws_port": 6081, "default": false } ``` ```json 409 { "error": "this VM already has 4 screens, which is the limit", "request_id": "7c1e0f4a-9b2d-4a51-8f0c-2d6b1e93a4c7", "upstream_status": 409 } ``` --- # Get a screen Source: https://docs.orgo.ai/api-reference/screens/get Endpoint: GET /computers/{id}/screens/{screenId} Summary: Read one screen on a computer. Returns one [screen](https://docs.orgo.ai/api-reference/screens/list): its display, size, ports, and whether it is the screen the computer booted with. ## Path parameters - `id` (path, string, required) Computer ID (UUID). The computer must be up. One that is still being created, or that has been frozen, returns `400`. - `screenId` (path, string, required) Screen ID, or `default` for the boot screen. ## Response Returns the [screen](https://docs.orgo.ai/api-reference/screens/list). A screen ID that does not exist returns `404`. It is never resolved to the boot screen, because acting on the wrong screen puts a click on another agent's screen. ## Errors Failures raised before the request reaches the computer carry `error` alone. Failures relayed back from the computer add `request_id` and `upstream_status`. Both a missing computer and a missing screen answer `404`, and the body tells them apart. | Status | When | Body | |--------|------|------| | `400` | The computer has nothing running behind it: still being created, or frozen. | `{ "error": "Computer not available" }` | | `401` | The Bearer token starts with `sk_` but is not a known Orgo key. | `{ "error": "Invalid API key" }` | | `401` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | `{ "error": "Authentication required" }` | | `401` | You do not own the computer's workspace and are not a member of it. | `{ "error": "You do not have access to this workspace." }` | | `401` | You have view-only access to the workspace. | `{ "error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)." }` | | `401` | The API key is scoped to a different workspace. | `{ "error": "This API key cannot access this workspace (workspace_scope_mismatch)." }` | | `401` | Orgo could not verify the credential because of a server-side fault. Retry. | `{ "error": "Service temporarily unavailable. …" }` | | `402` | The computer is a free trial computer that can no longer run. | `{ "error": "Choose a plan to continue." }` | | `402` | The computer is paid for by its own subscription or dedicated purchase, and that payment has lapsed. | `{ "error": "Manage this computer’s payment in Account → Usage." }` | | `404` | No computer with that ID. | `{ "error": "Desktop not found" }` | | `404` | No screen with that ID on this computer. | `{ "error": "404 page not found\n", "request_id": "…", "upstream_status": 404 }` | | `404` | The computer runs Windows, whose agent has no screens endpoint. Every screen ID answers this. | `{ "error": "…", "request_id": "…", "upstream_status": 404 }` | | `500` | Unexpected failure. | `{ "error": "…", "request_id": "…" }` | | `503` | The computer could not be reached. | `{ "error": "Could not reach the desktop. Try again in a moment.", "request_id": "…", "code": "ECONNREFUSED" }` | ```bash cURL curl https://www.orgo.ai/api/computers/$COMPUTER_ID/screens/screen-100 \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```json 200 { "id": "screen-100", "display": ":100", "width": 1920, "height": 1080, "vnc_port": 6000, "ws_port": 6081, "default": false } ``` --- # Resize a screen Source: https://docs.orgo.ai/api-reference/screens/resize Endpoint: PATCH /computers/{id}/screens/{screenId} Summary: Change a screen’s resolution. Resizes a screen in place with `xrandr`, so the X server is not restarted and every window already open on the screen is kept. ## Path parameters - `id` (path, string, required) Computer ID (UUID). The computer must be up. One that is still being created, or that has been frozen, returns `400`. - `screenId` (path, string, required) Screen ID, or `default` for the boot screen. ## Body parameters - `width` (body, integer, required) New width in pixels. Must be `1` or greater. - `height` (body, integer, required) New height in pixels. Must be `1` or greater. Both are required here, unlike [create](https://docs.orgo.ai/api-reference/screens/create), which falls back to the size of the boot screen. A resize is an explicit change, so there is no fallback. A missing, zero, or negative dimension is refused with `400` and the message `width and height are required`. ## Response Returns the [screen](https://docs.orgo.ai/api-reference/screens/list) with its new `width` and `height`. Every other field is unchanged: resizing does not move a screen to a different display, and its `vnc_port` and `ws_port` stay where they were. An extra screen's new size is recorded before the response, so the screen comes back at that size after a restart, as described in [Screens after a restart](https://docs.orgo.ai/api-reference/screens/list#screens-after-a-restart). The boot screen's size is not recorded: after a restart, `default` is back at the size the computer boots with. ## Errors Failures raised before the request reaches the computer carry `error` alone. Failures relayed back from the computer add `request_id` and `upstream_status`. | Status | When | Body | |--------|------|------| | `400` | The computer has nothing running behind it: still being created, or frozen. | `{ "error": "Computer not available" }` | | `400` | `width` or `height` is missing, zero or negative. | `{ "error": "width and height are required", "request_id": "…", "upstream_status": 400 }` | | `400` | `width` or `height` is not an integer. | `{ "error": "invalid JSON: …", "request_id": "…", "upstream_status": 400 }` | | `401` | The Bearer token starts with `sk_` but is not a known Orgo key. | `{ "error": "Invalid API key" }` | | `401` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | `{ "error": "Authentication required" }` | | `401` | You do not own the computer's workspace and are not a member of it. | `{ "error": "You do not have access to this workspace." }` | | `401` | You have view-only access to the workspace. | `{ "error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)." }` | | `401` | The API key is scoped to a different workspace. | `{ "error": "This API key cannot access this workspace (workspace_scope_mismatch)." }` | | `401` | Orgo could not verify the credential because of a server-side fault. Retry. | `{ "error": "Service temporarily unavailable. …" }` | | `402` | The computer is a free trial computer that can no longer run. | `{ "error": "Choose a plan to continue." }` | | `402` | The computer is paid for by its own subscription or dedicated purchase, and that payment has lapsed. | `{ "error": "Manage this computer’s payment in Account → Usage." }` | | `404` | No computer with that ID. | `{ "error": "Desktop not found" }` | | `404` | The computer runs Windows, whose agent has no screens endpoint. | `{ "error": "…", "request_id": "…", "upstream_status": 404 }` | | `500` | The resize failed on the computer, or the screen ID does not exist. | `{ "error": "…", "request_id": "…", "upstream_status": 500 }` | | `503` | The computer could not be reached. | `{ "error": "Could not reach the desktop. Try again in a moment.", "request_id": "…", "code": "ECONNREFUSED" }` | **Warning:** Resizing a screen ID that does not exist answers `500` with `no screen ""`, not `404`. [Get](https://docs.orgo.ai/api-reference/screens/get) is the reliable existence check: it answers `404` for an unknown screen. ```bash cURL curl -X PATCH https://www.orgo.ai/api/computers/$COMPUTER_ID/screens/screen-100 \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"width": 1280, "height": 720}' ``` ```json 200 { "id": "screen-100", "display": ":100", "width": 1280, "height": 720, "vnc_port": 6000, "ws_port": 6081, "default": false } ``` --- # Destroy a screen Source: https://docs.orgo.ai/api-reference/screens/delete Endpoint: DELETE /computers/{id}/screens/{screenId} Summary: Stop a screen and everything running on it. Stops the screen's X server, its window manager, and every window on it. This is not recoverable. The display number the screen was using is freed. The next screen you [create](https://docs.orgo.ai/api-reference/screens/create) takes the lowest free display number from `:100` up, so it reuses this one when no lower number is free. **Warning:** The boot screen (`default`) cannot be destroyed. A computer with no screen is a computer nobody can see, so the request is refused with `409`. ## Path parameters - `id` (path, string, required) Computer ID (UUID). The computer must be up. One that is still being created, or that has been frozen, returns `400`. - `screenId` (path, string, required) Screen ID. Must not be `default`. ## Response `204 No Content` with an empty body. There is nothing left to describe once the screen is gone. The screen is also dropped from the record Orgo keeps of the computer's screens, so a restart does not bring it back. That record is described in [Screens after a restart](https://docs.orgo.ai/api-reference/screens/list#screens-after-a-restart). ## Errors Failures raised before the request reaches the computer carry `error` alone. Failures relayed back from the computer add `request_id` and `upstream_status`. This differs from [get](https://docs.orgo.ai/api-reference/screens/get): a screen ID that does not exist answers `409` here, not `404`. Destroying the boot screen and destroying a screen that was never there are both refusals. Both come back as `409`, and the message tells them apart. | Status | When | Body | |--------|------|------| | `400` | The computer has nothing running behind it: still being created, or frozen. | `{ "error": "Computer not available" }` | | `401` | The Bearer token starts with `sk_` but is not a known Orgo key. | `{ "error": "Invalid API key" }` | | `401` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | `{ "error": "Authentication required" }` | | `401` | You do not own the computer's workspace and are not a member of it. | `{ "error": "You do not have access to this workspace." }` | | `401` | You have view-only access to the workspace. | `{ "error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)." }` | | `401` | The API key is scoped to a different workspace. | `{ "error": "This API key cannot access this workspace (workspace_scope_mismatch)." }` | | `401` | Orgo could not verify the credential because of a server-side fault. Retry. | `{ "error": "Service temporarily unavailable. …" }` | | `402` | The computer is a free trial computer that can no longer run. | `{ "error": "Choose a plan to continue." }` | | `402` | The computer is paid for by its own subscription or dedicated purchase, and that payment has lapsed. | `{ "error": "Manage this computer’s payment in Account → Usage." }` | | `404` | No computer with that ID. | `{ "error": "Desktop not found" }` | | `404` | The computer runs Windows, whose agent has no screens endpoint. | `{ "error": "…", "request_id": "…", "upstream_status": 404 }` | | `409` | `screenId` is `default`. | `{ "error": "the default screen cannot be destroyed", "request_id": "…", "upstream_status": 409 }` | | `409` | No screen with that ID on this computer. | `{ "error": "no screen \"screen-101\"", "request_id": "…", "upstream_status": 409 }` | | `500` | Unexpected failure. | `{ "error": "…", "request_id": "…" }` | | `503` | The computer could not be reached. | `{ "error": "Could not reach the desktop. Try again in a moment.", "request_id": "…", "code": "ECONNREFUSED" }` | ```bash cURL curl -X DELETE https://www.orgo.ai/api/computers/$COMPUTER_ID/screens/screen-100 \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```http 204 HTTP/1.1 204 No Content ``` ```json 409 { "error": "the default screen cannot be destroyed", "request_id": "7c1e0f4a-9b2d-4a51-8f0c-2d6b1e93a4c7", "upstream_status": 409 } ``` --- # Upload file Source: https://docs.orgo.ai/api-reference/files/upload Endpoint: POST /files/upload Summary: Upload a file to a workspace and push it to running computers. Uploads a file to a workspace and pushes it to the computers that are running at that moment. Passing `destPath` switches the endpoint to a second mode. That mode drops the file into one exact folder on one computer and records nothing. The two modes return different bodies. See [Folder-targeted upload](#folder-targeted-upload). ## Request Send a `multipart/form-data` request. - `file` (body, file, required) The file to upload. Maximum 10MB. A larger file returns 400. The name is preserved as `filename`. The copy in object storage is keyed under a sanitised form of it. - `workspaceId` (body, string, required) Workspace ID to upload into. Omitting it returns 400. - `desktopId` (body, string) Computer ID to associate the file with. When omitted, the file is recorded against the workspace with a `desktop_id` of `null`. Association does not change where the file is pushed. Every running computer in the workspace receives it either way. Required when `destPath` is set. - `destPath` (body, string) Absolute folder path on the computer, such as `/root/Documents` or, on Windows, `C:\Users\Public`. The literal value `desktop` means the desktop folder of the computer's user, which the computer resolves itself. When set, the file is placed at `/` on the computer named by `desktopId`, and nothing else happens: no file record is written, no other computer receives it, and the response body is different. On Linux, a folder that does not exist is created. When omitted, the file is stored in the workspace and pushed to running computers. ## Response Without `destPath`, the whole stored row is returned under `file`. - `file` (object) The stored file record. - `file.id` (string) File ID. Pass it as `id` to the download and delete endpoints. - `file.workspace_id` (string) Workspace the file belongs to. - `file.project_id` (string) The same value as `workspace_id`, under the workspace's older name. Read `workspace_id` in new code. - `file.desktop_id` (string | null) The `desktopId` you supplied, or `null`. - `file.user_id` (string) ID of the account that uploaded the file. - `file.filename` (string) Original file name. - `file.storage_key` (string) Internal object-storage key. It is not a URL and cannot be fetched directly. Use the download endpoint to get a signed URL. - `file.size_bytes` (string) Size in bytes as a decimal string, such as `"102400"`. The value is a 64-bit integer, so it is serialized as a string. - `file.content_type` (string) MIME type reported by the client, or `application/octet-stream` when the client sent none. - `file.created_at` (string) ISO 8601 creation timestamp. - `file.source` (string) Always `upload` for this endpoint. ## Example ```bash cURL curl -X POST https://www.orgo.ai/api/files/upload \ -H "Authorization: Bearer $ORGO_API_KEY" \ -F "file=@./document.pdf" \ -F "workspaceId=$WORKSPACE_ID" \ -F "desktopId=$COMPUTER_ID" ``` ```python Python import os import requests with open("document.pdf", "rb") as f: response = requests.post( "https://www.orgo.ai/api/files/upload", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, files={"file": f}, data={ "workspaceId": os.environ["WORKSPACE_ID"], "desktopId": os.environ["COMPUTER_ID"], # optional }, ) file_info = response.json()["file"] print(f"Uploaded: {file_info['filename']} ({file_info['id']})") ``` ```javascript JavaScript import { openAsBlob } from 'node:fs'; const formData = new FormData(); formData.append('file', await openAsBlob('document.pdf'), 'document.pdf'); formData.append('workspaceId', process.env.WORKSPACE_ID); formData.append('desktopId', process.env.COMPUTER_ID); // optional const response = await fetch('https://www.orgo.ai/api/files/upload', { method: 'POST', headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` }, body: formData, }); const { file } = await response.json(); console.log(`Uploaded: ${file.filename} (${file.id})`); ``` ### Response ```json { "file": { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "project_id": "550e8400-e29b-41d4-a716-446655440000", "desktop_id": "a3bb189e-8bf9-3888-9912-ace4e6543002", "user_id": "9f8e7d6c-5b4a-3210-fedc-ba9876543210", "filename": "document.pdf", "storage_key": "550e8400-e29b-41d4-a716-446655440000/uploads/1705314600000-document.pdf", "size_bytes": "102400", "content_type": "application/pdf", "created_at": "2024-01-15T10:30:00Z", "source": "upload" } } ``` ## How the file reaches a computer **Info:** The file is stored in the workspace and pushed to the desktop folder (`/root/Desktop` on Linux) of every computer that is running at the time of upload. Computers that are stopped, or created afterwards, do not receive it. Upload again to reach them. The push is best-effort and runs in the background without being awaited, so it does not affect the status you get back. Each push sends the workspace's full set of uploaded files. A computer skips any file whose name already exists in that folder rather than overwriting it. ## Folder-targeted upload Setting `destPath` places the file at one exact path on one computer. The response is a different shape and no file record is created. The file does not appear in the list endpoint, and it cannot be downloaded or deleted through this API. - `ok` (boolean) `true` when the file was placed on the computer. - `placedIn` (string) The folder the file was placed in: your `destPath` with surrounding whitespace trimmed, or `the desktop` when you sent `desktop`. The file name is not appended. ```bash cURL curl -X POST https://www.orgo.ai/api/files/upload \ -H "Authorization: Bearer $ORGO_API_KEY" \ -F "file=@./document.pdf" \ -F "workspaceId=$WORKSPACE_ID" \ -F "desktopId=$COMPUTER_ID" \ -F "destPath=/root/Documents" ``` ```json { "ok": true, "placedIn": "/root/Documents" } ``` ## Errors Every error carries an `error` message. The scope-mismatch `403` adds `code`, `key_workspace_id`, and `target_workspace_id`. | Status | Body | Cause | |--------|------|-------| | `400` | `{"error": "No file provided"}` | The `file` part was absent. | | `400` | `{"error": "workspaceId is required"}` | The `workspaceId` field was absent. | | `400` | `{"error": "File too large (max 10MB)"}` | The file exceeded 10MB. | | `400` | `{"error": "destPath requires desktopId"}` | `destPath` was set without `desktopId`. | | `400` | `{"error": "Invalid destination folder"}` | `destPath` was neither `desktop` nor an absolute path (starting with `/`, or with a drive letter such as `C:\`), or it contained a null byte. | | `400` | `{"error": "Invalid file name"}` | The file name resolved to nothing, `.`, or `..`. | | `401` | `{"error": "Invalid API key"}` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{"error": "Authentication required"}` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `401` | `{"error": "Service temporarily unavailable. …"}` | Orgo could not verify the credential because of a server-side fault. Retry. | | `403` | `{"error": "…", "code": "workspace_scope_mismatch", "key_workspace_id": "…", "target_workspace_id": "…"}` | A workspace-scoped key named a different workspace. | | `403` | `{"error": "Access denied"}` | The caller neither owns the workspace nor has write access to it. A view-only member gets this too, and so does a workspace ID that does not exist. This endpoint never returns 404. | | `500` | `{"error": "Failed to save file"}` | The upload reached storage but no file record came back. | | `500` | `{"error": "Upload failed"}` | The upload failed. The message may instead carry the underlying error. | | `502` | `{"error": "Could not place document.pdf in /root/Documents."}` | `destPath` mode only. The computer did not confirm it had written the file, or the request to it failed or ran past 130 seconds. | | `503` | `{"error": "Computer could not be reached."}` | `destPath` mode only. The computer is not addressable: it has no connection details, or `desktopId` names no computer in this workspace. | --- # Export file Source: https://docs.orgo.ai/api-reference/files/export Endpoint: POST /files/export Summary: Copy a file off a computer into workspace storage. Not currently available. **Warning:** **This endpoint is not currently available.** Every call fails with 404. The API accepts the request and forwards it to the computer. The software running on the computer implements no export handler, so the request has nowhere to land. No set of arguments makes it succeed. This page records the intended contract and the behaviour you will observe today. Do not build against it. Intended to copy a file off a computer's filesystem into workspace storage and return a download URL. ## Request Send a JSON body. - `desktopId` (body, string, required) Computer ID to export from. Omitting it returns 400. - `path` (body, string, required) Path to the file on the computer. Omitting it returns 400. The value is forwarded to the computer unchanged. How it would be resolved is not defined, because no handler on the computer reads it. ## Response There is no success response. The request is rejected before any file is read. Calls that get as far as the computer return: ```json { "error": "VM export failed: 404" } ``` with HTTP status 404. ## Example The call below is well-formed against a running computer and still returns 404. ```bash cURL curl -X POST https://www.orgo.ai/api/files/export \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"desktopId\": \"$COMPUTER_ID\", \"path\": \"/root/Desktop/results.txt\"}" ``` ```python Python import os import requests response = requests.post( "https://www.orgo.ai/api/files/export", headers={ "Authorization": f"Bearer {os.environ['ORGO_API_KEY']}", "Content-Type": "application/json", }, json={ "desktopId": os.environ["COMPUTER_ID"], "path": "/root/Desktop/results.txt", }, ) print(response.status_code, response.json()) # 404 {'error': 'VM export failed: 404'} ``` ```javascript JavaScript const response = await fetch('https://www.orgo.ai/api/files/export', { method: 'POST', headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ desktopId: process.env.COMPUTER_ID, path: '/root/Desktop/results.txt', }), }); console.log(response.status, await response.json()); // 404 { error: 'VM export failed: 404' } ``` ## Getting a file off a computer today Read the file over the computer's shell and write it where you want it. To send a file the other way, use the [upload endpoint](https://docs.orgo.ai/api-reference/files/upload): its `destPath` field places a file at an exact path on a computer. ## Errors Every error carries the shape `{"error": ""}`. The checks below run in order, so the first one that fails is the status you get. | Status | Body | Cause | |--------|------|-------| | `401` | `{"error": "Invalid API key"}` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{"error": "Authentication required"}` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `401` | `{"error": "You do not have access to this workspace."}` | The caller neither owns the computer's workspace nor belongs to it. | | `401` | `{"error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)."}` | The caller has view-only access to the computer's workspace. | | `401` | `{"error": "This API key cannot access this workspace (workspace_scope_mismatch)."}` | A workspace-scoped key named a computer in a different workspace. | | `500` | `{"error": "…"}` | The body is not valid JSON. The message is the parser's. | | `400` | `{"error": "Missing desktopId or path"}` | Either field was absent or empty. | | `404` | `{"error": "Desktop not found"}` | No computer has that ID. A `desktopId` that is not a UUID returns `500` at this step instead. | | `402` | `{"error": "Choose a plan to continue."}` | The computer is a free trial computer that can no longer run. | | `402` | `{"error": "Manage this computer’s payment in Account → Usage."}` | The computer is paid for by its own subscription or dedicated purchase, and that payment has lapsed. | | `400` | `{"error": "Desktop is not running"}` | The computer's status is anything other than `running`. | | `400` | `{"error": "Desktop has no instance ID"}` | The computer record carries no instance ID. | | `503` | `{"error": "Computer not ready"}` | The computer has no connection details recorded, so it cannot be addressed. | | `404` | `{"error": "VM export failed: 404"}` | The computer has no export handler. Every request that reaches this point ends here. | | `500` | `{"error": "Export failed"}` | Unexpected failure. The message may instead carry the underlying error. | A failure reported by the computer is relayed with the computer's own status code and its own error message, so statuses outside this list are possible if the software on the computer changes. --- # List files Source: https://docs.orgo.ai/api-reference/files/list Endpoint: GET /files Summary: List the files stored in a workspace. Lists the files stored in a workspace, newest first. Pass `desktopId` to filter to a single computer. ## Query parameters - `workspaceId` (query, string, required) Workspace ID. Omitting it returns 400. - `desktopId` (query, string) Computer UUID to filter by. When omitted, every file in the workspace is returned, whichever computer it belongs to. A value that is not a UUID, such as an instance id, returns 500. ## Response - `files` (array) File objects, ordered by `created_at` descending, newest first. An empty array when the workspace holds no files. - `files[].id` (string) File ID. Pass it as `id` to the download and delete endpoints. - `files[].filename` (string) Original file name. - `files[].size_bytes` (string | null) Size in bytes as a decimal string, such as `"1024"`, or `null` when no size was recorded. The value is a 64-bit integer, so it is serialized as a string. Parse it before doing arithmetic. - `files[].content_type` (string | null) MIME type, or `null` when none was recorded. - `files[].source` (string) How the file entered the workspace: `upload` or `export`. - `files[].desktop_id` (string | null) Computer the file is associated with, or `null` when it is not tied to one. - `files[].created_at` (string) ISO 8601 creation timestamp. ## Example ```bash cURL # Every file in the workspace curl "https://www.orgo.ai/api/files?workspaceId=$WORKSPACE_ID" \ -H "Authorization: Bearer $ORGO_API_KEY" # Only the files tied to one computer curl "https://www.orgo.ai/api/files?workspaceId=$WORKSPACE_ID&desktopId=$COMPUTER_ID" \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os import requests response = requests.get( "https://www.orgo.ai/api/files", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, params={ "workspaceId": os.environ["WORKSPACE_ID"], "desktopId": os.environ["COMPUTER_ID"], # optional }, ) files = response.json()["files"] for f in files: print(f"{f['filename']} ({f['size_bytes']} bytes, {f['source']})") ``` ```javascript JavaScript const params = new URLSearchParams({ workspaceId: process.env.WORKSPACE_ID, desktopId: process.env.COMPUTER_ID, // optional }); const response = await fetch(`https://www.orgo.ai/api/files?${params}`, { headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` }, }); const { files } = await response.json(); files.forEach((f) => { console.log(`${f.filename} (${f.size_bytes} bytes, ${f.source})`); }); ``` ### Response ```json { "files": [ { "id": "c3d4e5f6-a7b8-9012-cdef-345678901234", "filename": "results.txt", "size_bytes": "1024", "created_at": "2024-01-15T11:00:00Z", "desktop_id": "a3bb189e-8bf9-3888-9912-ace4e6543002", "content_type": "text/plain", "source": "export" }, { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "filename": "document.pdf", "size_bytes": "102400", "created_at": "2024-01-15T10:30:00Z", "desktop_id": null, "content_type": "application/pdf", "source": "upload" } ] } ``` ## Errors Every error carries the shape `{"error": ""}`. | Status | Body | Cause | |--------|------|-------| | `400` | `{"error": "workspaceId is required"}` | The `workspaceId` query parameter was absent. | | `401` | `{"error": "Invalid API key"}` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{"error": "Authentication required"}` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `401` | `{"error": "You do not have access to this workspace."}` | The caller neither owns the workspace nor belongs to it, or `desktopId` names a computer in such a workspace. A workspace ID that does not exist also returns this. This endpoint never returns 404. | | `401` | `{"error": "This API key cannot access this workspace (workspace_scope_mismatch)."}` | A workspace-scoped key named a different workspace, or a computer in one. | | `401` | `{"error": "Service temporarily unavailable. …"}` | Orgo could not verify the credential because of a server-side fault. Retry. | | `500` | `{"error": "Failed to list files"}` | The lookup failed, for example because `desktopId` is not a UUID. The message may instead carry the underlying database error. | --- # Download file Source: https://docs.orgo.ai/api-reference/files/download Endpoint: GET /files/download Summary: Get a signed download URL for a file. Returns a signed URL for a file stored in a workspace. The endpoint returns the URL, not the bytes. Fetch the URL to get the file. ## Query parameters - `id` (query, string, required) File ID, as returned by the list or upload endpoints. Omitting it returns 400. ## Response - `url` (string) Signed download URL, valid for one hour from the moment of the request. It carries the file's original name as an attachment filename. A browser saves it under that name rather than under the internal storage key. ## Example ```bash cURL curl "https://www.orgo.ai/api/files/download?id=$FILE_ID" \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os import requests response = requests.get( "https://www.orgo.ai/api/files/download", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, params={"id": os.environ["FILE_ID"]}, ) url = response.json()["url"] # Fetch the file itself file_response = requests.get(url) with open("downloaded.txt", "wb") as f: f.write(file_response.content) ``` ```javascript JavaScript const response = await fetch( `https://www.orgo.ai/api/files/download?id=${process.env.FILE_ID}`, { headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` } }, ); const { url } = await response.json(); // Fetch the file itself const fileResponse = await fetch(url); const blob = await fileResponse.blob(); ``` ### Response ```json { "url": "https://objects.us-east-2.storage.sh/…" } ``` **Info:** The URL is pre-signed. It works in a browser or any HTTP client with no authorization header of its own, and it expires one hour after it is issued. Request a new one rather than storing it. ## Errors Every error carries the shape `{"error": ""}`. | Status | Body | Cause | |--------|------|-------| | `400` | `{"error": "Missing file id"}` | The `id` query parameter was absent. | | `400` | `{"error": "id and fileId must identify the same file"}` | Both `id` and `fileId` were sent with different values. | | `401` | `{"error": "Invalid API key"}` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{"error": "Authentication required"}` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `401` | `{"error": "You do not have access to this workspace."}` | The file exists, but the caller neither owns its workspace nor belongs to it. | | `401` | `{"error": "This API key cannot access this workspace (workspace_scope_mismatch)."}` | A workspace-scoped key asked for a file in a different workspace. | | `401` | `{"error": "Service temporarily unavailable. …"}` | Orgo could not verify the credential because of a server-side fault. Retry. | | `404` | `{"error": "File not found"}` | No file has that ID. | | `500` | `{"error": "Download failed"}` | The URL could not be signed, or the `id` is not a UUID. The message may instead carry the underlying error. | ## Compatibility parameter Prefer `id`. The older `fileId` spelling is also accepted as an alias. If both are present with different values the request returns `400`. At least one is required. --- # Delete file Source: https://docs.orgo.ai/api-reference/files/delete Endpoint: DELETE /files/delete Summary: Permanently delete a file from a workspace. Permanently deletes a file from storage and removes its record from the workspace. **Warning:** This action cannot be undone. Signed download URLs issued for the file before deletion stop working. ## Query parameters - `id` (query, string, required) File ID, as returned by the list or upload endpoints. Omitting it returns 400. ## Response - `success` (boolean) Always `true`. Any failure is reported as an error status, not as `false`. ## Example ```bash cURL curl -X DELETE "https://www.orgo.ai/api/files/delete?id=$FILE_ID" \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os import requests response = requests.delete( "https://www.orgo.ai/api/files/delete", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, params={"id": os.environ["FILE_ID"]}, ) if response.ok: print("File deleted") ``` ```javascript JavaScript const response = await fetch( `https://www.orgo.ai/api/files/delete?id=${process.env.FILE_ID}`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` }, }, ); if (response.ok) console.log('File deleted'); ``` ### Response ```json { "success": true } ``` **Info:** The stored object is removed first, then the file record. If removing the object fails, the endpoint returns 500 and the record is left in place. The file still appears in the list endpoint. Retry the delete. Deleting a file does not remove any copy that was pushed onto a computer. A file synced to `/root/Desktop` stays on that computer. ## Errors Every error carries the shape `{"error": ""}`. | Status | Body | Cause | |--------|------|-------| | `400` | `{"error": "Missing file id"}` | The `id` query parameter was absent. | | `400` | `{"error": "id and fileId must identify the same file"}` | Both `id` and `fileId` were sent with different values. | | `401` | `{"error": "Invalid API key"}` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{"error": "Authentication required"}` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `401` | `{"error": "You do not have access to this workspace."}` | The file exists, but the caller neither owns its workspace nor belongs to it. | | `401` | `{"error": "This workspace is view-only. Ask the owner for write access (workspace_read_only)."}` | The caller has view-only access to the file's workspace. | | `401` | `{"error": "This API key cannot access this workspace (workspace_scope_mismatch)."}` | A workspace-scoped key targeted a file in a different workspace. | | `401` | `{"error": "Service temporarily unavailable. …"}` | Orgo could not verify the credential because of a server-side fault. Retry. | | `404` | `{"error": "File not found"}` | No file has that ID. A second delete of the same file returns 404. | | `500` | `{"error": "Delete failed"}` | The object or the record could not be removed, or the `id` is not a UUID. The message may instead carry the underlying error. | ## Compatibility parameter Prefer `id`. The older `fileId` spelling is also accepted as an alias. If both are present with different values the request returns `400`. At least one is required. --- # Get template schema Source: https://docs.orgo.ai/api-reference/templates/schema Endpoint: GET /template-schema Summary: Fetch the canonical orgo.ai/v1 template JSON Schema. Returns the complete `orgo.ai/v1` template schema as [JSON Schema](https://json-schema.org/) (Draft 2020-12). It covers every field, its type, accepted values, and a description. This is the canonical machine-readable contract for templates, consumed by editors, the CLI, and AI agents. **Info:** This endpoint is **public**. It requires no API key, sets permissive CORS headers, and is cacheable, so any tool can fetch it directly. It serves a static document, so the only status it returns is `200`. **Warning:** The schema models the **canonical** form only. It requires `api_version` and `template` and sets `additionalProperties: false`, so a document in the [sugar form](https://docs.orgo.ai/guides/templates/schema#sugar-form) validates as invalid against it: once for the missing `template` block, and once per sugar key. Sugar is desugared server-side, so check a sugar document with [Validate template](https://docs.orgo.ai/api-reference/templates/validate). ## Response A JSON Schema document. The top-level `properties` describe each template field; `$defs` holds the reusable sub-objects (`hardware`, `app`, `service`, `trigger`, and so on). For a guided walkthrough, see the [schema reference](https://docs.orgo.ai/guides/templates/schema). ## Example ```bash cURL curl https://www.orgo.ai/api/template-schema ``` ```python Python import requests schema = requests.get("https://www.orgo.ai/api/template-schema").json() print(schema["title"]) # "Orgo Template (orgo.ai/v1)" print(list(schema["properties"])) ``` ```javascript JavaScript const schema = await ( await fetch("https://www.orgo.ai/api/template-schema") ).json(); console.log(schema.title); ``` ### Response (excerpt) ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://orgo.ai/schemas/template/orgo.ai-v1.json", "title": "Orgo Template (orgo.ai/v1)", "type": "object", "required": ["api_version", "template"], "properties": { "api_version": { "type": "string", "enum": ["orgo.ai/v1"] }, "template": { "$ref": "#/$defs/metadata" }, "hardware": { "$ref": "#/$defs/hardware" } } } ``` **Tip:** Point your editor's YAML language server at this URL for inline autocomplete and validation of a **canonical** template: ```yaml # yaml-language-server: $schema=https://www.orgo.ai/api/template-schema api_version: orgo.ai/v1 template: name: my-workstation version: 1.0.0 ``` --- # List templates Source: https://docs.orgo.ai/api-reference/templates/list Endpoint: GET /templates Summary: List the templates you have published. Returns the templates published under your account. Each item carries the template's `ref`, its content-addressed `digest`, and the `published` timestamp. To read a version's full document, call [Get template](https://docs.orgo.ai/api-reference/templates/get). **Tip:** Looking for the curated templates Orgo ships (Coding, OpenClaw, Hermes Agent)? Use [List curated templates](https://docs.orgo.ai/api-reference/templates/list-curated) instead. ## Query parameters - `namespace` (query, string) Filter to a single namespace. Your own templates are published under `default` unless you choose another namespace. Omitted, every namespace you own is returned. ## Response - `templates` (array) Array of template list items. **item** - `ref` (string) Template ref, in `namespace/name@version` form. - `digest` (string) Content-addressed SHA-256 digest of the canonical template. Two semantically identical templates always share a digest. - `published` (string) ISO 8601 timestamp of when the version was published. ## Example ```bash cURL curl https://www.orgo.ai/api/templates \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os, requests r = requests.get( "https://www.orgo.ai/api/templates", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, ) for t in r.json()["templates"]: print(t["ref"], t["digest"][:12]) ``` ```javascript JavaScript const r = await fetch("https://www.orgo.ai/api/templates", { headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` }, }); const { templates } = await r.json(); templates.forEach((t) => console.log(t.ref)); ``` ### Response ```json { "templates": [ { "ref": "default/claude-code@1.0.0", "digest": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "published": "2026-06-08T17:00:00Z" } ] } ``` `templates` is an empty array when you have published nothing, or nothing in the requested namespace. Listing is not plan-gated. ## Errors | Status | Body | Meaning | |--------|------|---------| | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `401` | `{ "error": "This endpoint requires an account-wide credential (workspace_scope_mismatch)." }` | The key is workspace-scoped. Template endpoints need an account-wide key. This route answers with `401`, not `403`. | | `401` | `{ "error": "Service temporarily unavailable. …" }` | Orgo could not verify the key because of a server-side fault. The key is fine. Retry. | | `500` | `{ "error": string }` | Unexpected server error (`"internal error"`), or the template registry host failed or timed out. Retry. | | `503` | `{ "error": string }` | No fleet host is available to serve the template registry. Retry. | --- # Publish template Source: https://docs.orgo.ai/api-reference/templates/publish Endpoint: POST /templates Summary: Publish a template document to your registry. Publishes a template to your registry. The request body is a complete [`orgo.ai/v1`](https://docs.orgo.ai/guides/templates/schema) document, sent as YAML (`Content-Type: application/yaml`) or JSON. The body is parsed as YAML only when the content type contains `yaml`; anything else is parsed as JSON. A successful publish returns `201`. **Note:** Publishing templates requires a [Scale plan](https://orgo.ai/pricing) or higher. Launching a computer from a template, curated or your own, is separate: it needs a paid plan and counts against the workspace owner's computer quota. **Info:** **Refs are immutable.** Once `namespace/name@version` is published, re-publishing the same ref with different content returns `409`. Bump `template.version` to ship a change, or pass `?force=true` to overwrite the version in place while iterating. ## Query parameters - `auto_build` (query, boolean, default `false`) Set to `true` to build the [golden snapshot](https://docs.orgo.ai/guides/templates/introduction#golden-snapshots) immediately after publishing. Like calling [Build template](https://docs.orgo.ai/api-reference/templates/build) yourself, except that nothing is queued when this exact content is already built. Omitted, the version is published unbuilt and cannot launch until you build it. Only the exact string `true` enables it. - `force` (query, boolean, default `false`) Set to `true` to overwrite an existing version in place (delete + republish). Useful for fast iteration on a single version number. Omitted, re-publishing an existing ref with different content returns `409`. Only the exact string `true` enables it. ## Request body The raw template document. Validate it first with [Validate template](https://docs.orgo.ai/api-reference/templates/validate) to catch errors without writing anything. ## Response - `ref` (string) The published ref, `namespace/name@version`. - `digest` (string) Content-addressed SHA-256 digest of the canonical template. - `published` (string) ISO 8601 publish timestamp. - `auto_build` (string) Present only when `?auto_build=true`. `building` while the build is queued or running, `ready` when a golden snapshot for this exact content already exists. Absent when no build could be started. Read `build.error` for the reason, then call [Build template](https://docs.orgo.ai/api-reference/templates/build). - `build` (object) Present only when `?auto_build=true`. What happened to the requested build. The publish itself has succeeded either way. **build** - `queued` (boolean): `true` while a build job for this content is queued or running. - `status` (string): `queued`, `building`, `ready`, `failed`, or `not_built`. - `job_id` (string): Build job ID. Present when a job was queued or found in flight. - `state` (string): Queue state of the job this request queued: `queued` or `leased`. - `phase` (string): Current job phase. Present when a job was queued or found in flight. - `tier` (string): Build runner tier the job was queued on: `standard`, `fast`, or `turbo`. Present when this request queued the job. - `ahead` (integer): Queued jobs across all accounts that will run before this one. Present when this request queued the job. - `error` (string): Why no build was started. Present only then. - `code` (string): Machine-readable reason, when one exists, e.g. `HARDWARE_EXCEEDS_BUILD_RUNNER`. ## Example ```bash cURL curl -X POST "https://www.orgo.ai/api/templates?auto_build=true" \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/yaml" \ --data-binary @claude-code.yaml ``` ```python Python import os, requests with open("claude-code.yaml") as f: body = f.read() r = requests.post( "https://www.orgo.ai/api/templates", params={"auto_build": "true"}, headers={ "Authorization": f"Bearer {os.environ['ORGO_API_KEY']}", "Content-Type": "application/yaml", }, data=body, ) print(r.json()["ref"]) ``` ```javascript JavaScript import { readFileSync } from "node:fs"; const r = await fetch("https://www.orgo.ai/api/templates?auto_build=true", { method: "POST", headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}`, "Content-Type": "application/yaml", }, body: readFileSync("claude-code.yaml", "utf8"), }); const { ref, digest } = await r.json(); console.log(ref, digest); ``` ### Response ```json { "ref": "default/claude-code@1.0.0", "digest": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "published": "2026-06-08T17:00:00Z", "auto_build": "building", "build": { "queued": true, "status": "queued", "job_id": "9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f", "state": "queued", "phase": "queued", "tier": "standard", "ahead": 0 } } ``` ## Errors | Status | Body | Meaning | |--------|------|---------| | `400` | `{ "error": "request body required" }` | Empty body. | | `400` | `{ "error": string }` | The body could not be parsed as a template: not valid YAML or JSON, or not a canonical, sugar, or `{namespace, name, version, template}` envelope document. | | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `401` | `{ "error": "This endpoint requires an account-wide credential (workspace_scope_mismatch)." }` | The key is workspace-scoped. Template endpoints need an account-wide key. This route answers with `401`, not `403`. | | `401` | `{ "error": "Service temporarily unavailable. …" }` | Orgo could not verify the key because of a server-side fault. The key is fine. Retry. | | `403` | `{ "error": string, "code": "UPGRADE_REQUIRED", "upgradeTier": "scale_v2" }` | Below Scale, or your plan could not be verified. The gate fails closed, and nothing is written. | | `403` | `{ "error": string, "code": "PLAN_LIMIT", "upgradeTier": "enterprise" }` | Your account is on a custom plan that does not include templates. Nothing is written. | | `409` | `{ "error": string, "code": "version_exists" }` | A different template is already published at this `namespace/name@version`. Bump the version or use `?force=true`. | | `422` | `{ "error": "template validation failed", "errors": [{ "field", "code", "message", "hint"? }] }` | The template failed validation. Each entry pinpoints one problem. A `details` field, when present, repeats the same list. | | `429` | `{ "error": string, "code": string }` | Publish rate limit or registry quota reached, e.g. `publish_rate_limited` or `tenant_quota_exceeded`. Back off and retry. | | `500` | `{ "error": string }` | Unexpected server error (`"internal error"`), or the template registry host failed or timed out. Retry. | | `503` | `{ "error": string, "code"?: string }` | No fleet host is available to serve the template registry, or the template record could not be read or written (`publish_authority_unavailable`). Retry. | Branch on `code` rather than on the message text: `UPGRADE_REQUIRED` with `upgradeTier` is the upsell signal, and the rate-limit codes tell you what to back off from. --- # Validate template Source: https://docs.orgo.ai/api-reference/templates/validate Endpoint: POST /templates/validate Summary: Check a template document for errors without publishing it. Validates a template against the `orgo.ai/v1` schema and returns either the normalized document or a structured list of errors. This endpoint has **no side effects**. Nothing is written, so it is cheap enough to call on every keystroke in an editor. Validate is also the right way to check a [sugar-form](https://docs.orgo.ai/guides/templates/schema#sugar-form) template: it desugars server-side, which the published JSON Schema cannot do. ## Request body The raw template document. Send `Content-Type: application/yaml` to parse it as YAML. The body is parsed as YAML whenever the content type contains `yaml`; anything else is parsed as JSON. An empty body returns `400`. ## Response - `ok` (boolean) `true` if the template is valid. - `template` (object) The normalized template (sugar expanded into canonical form). Present when `ok` is `true`. - `errors` (array) Present when `ok` is `false`, alongside a `422` status. Each entry pinpoints one problem. **error** - `field` (string): Dotted path to the offending field, e.g. `hardware.cpu` or `files[2].to`. - `code` (string) Machine-readable code, one of: `required`, `invalid_enum`, `invalid_format`, `invalid_value`, `unsafe_path`, `mutex_conflict`, `unknown_reference`, `duplicate`, `unknown_api_version`. - `message` (string): Human-readable explanation. - `hint` (string): Suggestion for fixing it. Present on some errors only. ## Example ```bash cURL curl -X POST https://www.orgo.ai/api/templates/validate \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/yaml" \ --data-binary @claude-code.yaml ``` ```python Python import os, requests r = requests.post( "https://www.orgo.ai/api/templates/validate", headers={ "Authorization": f"Bearer {os.environ['ORGO_API_KEY']}", "Content-Type": "application/yaml", }, data=open("claude-code.yaml").read(), ) result = r.json() if not result["ok"]: for e in result["errors"]: print(f"{e['field']}: {e['message']}") ``` ```javascript JavaScript import { readFileSync } from "node:fs"; const r = await fetch("https://www.orgo.ai/api/templates/validate", { method: "POST", headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}`, "Content-Type": "application/yaml", }, body: readFileSync("claude-code.yaml", "utf8"), }); const result = await r.json(); if (!result.ok) console.error(result.errors); ``` ### Valid response ```json { "ok": true, "template": { "api_version": "orgo.ai/v1", "template": { "name": "claude-code", "version": "1.0.0" } } } ``` ### Invalid response (`422`) ```json { "ok": false, "errors": [ { "field": "hardware.cpu", "code": "invalid_enum", "message": "must be 1, 2, 4, 8, or 16" } ] } ``` ## Errors A validation failure is the `422` above: `ok` is `false` and the body carries `errors`, with no `error` field. Everything else is a transport-level failure. | Status | Body | Meaning | |--------|------|---------| | `400` | `{ "error": "request body required" }` | Empty body. | | `400` | `{ "error": string }` | The body could not be parsed as a template: not valid YAML or JSON, or not a canonical, sugar, or `{namespace, name, version, template}` envelope document. | | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `401` | `{ "error": "This endpoint requires an account-wide credential (workspace_scope_mismatch)." }` | The key is workspace-scoped. Template endpoints need an account-wide key. This route answers with `401`, not `403`. | | `401` | `{ "error": "Service temporarily unavailable. …" }` | Orgo could not verify the key because of a server-side fault. The key is fine. Retry. | | `422` | `{ "ok": false, "errors": [...] }` | The template is invalid. | | `429` | `{ "error": string, "code": "validate_rate_limited" }` | Validate rate limit reached. Back off and retry. | | `500` | `{ "error": string }` | Unexpected server error (`"internal error"`), or the validator host failed or timed out. Retry. | | `503` | `{ "error": string }` | No fleet host is available to run the validator. Retry. | --- # List template versions Source: https://docs.orgo.ai/api-reference/templates/list-versions Endpoint: GET /templates/{namespace}/{name} Summary: List every published version of one template. Lists all published versions of a single template, so you can show a version history or resolve the latest. The lookup is scoped to your own templates; another account's template with the same name is invisible. **Tip:** For the versions of a curated template, call `GET /templates/global/{namespace}/{name}` instead, e.g. `https://www.orgo.ai/api/templates/global/system/coding`. ## Path parameters - `namespace` (path, string, required) Template namespace. Your own templates default to `default`. - `name` (path, string, required) Template name. ## Response - `versions` (array) Array of template list items, one per published version. Each has `ref`, `version`, `digest`, and `published`. Empty when no template with this namespace and name exists in your registry. This endpoint does not return `404` for an unknown template. ## Example ```bash cURL curl https://www.orgo.ai/api/templates/default/claude-code \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os, requests r = requests.get( "https://www.orgo.ai/api/templates/default/claude-code", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, ) versions = [v["ref"] for v in r.json()["versions"]] print(versions) ``` ```javascript JavaScript const r = await fetch( "https://www.orgo.ai/api/templates/default/claude-code", { headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` } }, ); const { versions } = await r.json(); console.log(versions.map((v) => v.ref)); ``` ### Response ```json { "versions": [ { "ref": "default/claude-code@1.0.0", "version": "1.0.0", "digest": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "published": "2026-06-08T17:00:00Z" } ] } ``` ## Errors | Status | Body | Meaning | |--------|------|---------| | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `401` | `{ "error": "This endpoint requires an account-wide credential (workspace_scope_mismatch)." }` | The key is workspace-scoped. Template endpoints need an account-wide key. This route answers with `401`, not `403`. | | `401` | `{ "error": "Service temporarily unavailable. …" }` | Orgo could not verify the key because of a server-side fault. The key is fine. Retry. | | `500` | `{ "error": string }` | Unexpected server error (`"internal error"`), or the template registry host failed or timed out. Retry. | | `503` | `{ "error": string }` | No fleet host is available to serve the template registry. Retry. | --- # Get template Source: https://docs.orgo.ai/api-reference/templates/get Endpoint: GET /templates/{namespace}/{name}/{version} Summary: Fetch a single template version and its full document. Returns one template version, including the complete `orgo.ai/v1` document. Curated templates resolve here too: use their `system` namespace, e.g. `system/coding/1.0.4`. Reading a template is not plan-gated. ## Path parameters - `namespace` (path, string, required) Template namespace. Your own templates default to `default`. - `name` (path, string, required) Template name. - `version` (path, string, required) Version (semver), e.g. `1.0.0`. ## Response - `ref` (string): Template ref, `namespace/name@version`. - `digest` (string): Content-addressed SHA-256 digest. - `published` (string): ISO 8601 publish timestamp. - `template` (object): The full template document. See the [schema reference](https://docs.orgo.ai/guides/templates/schema). ## Example ```bash cURL curl https://www.orgo.ai/api/templates/default/claude-code/1.0.0 \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os, requests r = requests.get( "https://www.orgo.ai/api/templates/default/claude-code/1.0.0", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, ) print(r.json()["template"]) ``` ```javascript JavaScript const r = await fetch( "https://www.orgo.ai/api/templates/default/claude-code/1.0.0", { headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` } }, ); const item = await r.json(); console.log(item.template); ``` ### Response ```json { "ref": "default/claude-code@1.0.0", "digest": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "published": "2026-06-08T17:00:00Z", "template": { "api_version": "orgo.ai/v1", "template": { "name": "claude-code", "version": "1.0.0", "description": "Claude Code CLI, ready in the terminal." }, "hardware": { "cpu": 2, "ram_gb": 4, "resolution": "1280x720x24" } } } ``` ## Errors | Status | Body | Meaning | |--------|------|---------| | `400` | `{ "error": string }` | The path does not form a valid `namespace/name@version` ref. The message starts with `invalid`. | | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `401` | `{ "error": "This endpoint requires an account-wide credential (workspace_scope_mismatch)." }` | The key is workspace-scoped. Template endpoints need an account-wide key. This route answers with `401`, not `403`. | | `401` | `{ "error": "Service temporarily unavailable. …" }` | Orgo could not verify the key because of a server-side fault. The key is fine. Retry. | | `404` | `{ "error": "not found" }` | No such template version in this namespace, or it belongs to another account. | | `500` | `{ "error": string }` | Unexpected server error (`"internal error"`), or the template registry host failed or timed out. Retry. | | `503` | `{ "error": string }` | No fleet host is available to serve the template registry. Retry. | --- # Delete template version Source: https://docs.orgo.ai/api-reference/templates/delete Endpoint: DELETE /templates/{namespace}/{name}/{version} Summary: Delete one published version from your registry. Deletes a single published version you own: its registry entry, its golden snapshot, and any of its builds still waiting in the queue. To replace a version in place instead of deleting it, re-publish with [`?force=true`](https://docs.orgo.ai/api-reference/templates/publish). The call is idempotent. Deleting a version that is already gone, or never existed, returns `200` with `deleted: true`. **Warning:** Deleting a version that computers were launched from does not affect those running computers. They already started from the golden snapshot. The delete removes the version from your registry, so it can no longer be launched or built. It is scoped to your own templates, so a curated (`system/…`) ref removes nothing, even though the response still reads `deleted: true`. ## Path parameters - `namespace` (path, string, required): Template namespace. - `name` (path, string, required): Template name. - `version` (path, string, required): Version (semver) to delete. ## Response - `deleted` (boolean) Always `true` on success, including when the version was already gone. A failure comes back as a `4xx` or `5xx` instead. - `cancelledJobs` (integer) How many of the version's queued builds were cancelled. A build a runner had already claimed is not counted. ## Example ```bash cURL curl -X DELETE https://www.orgo.ai/api/templates/default/claude-code/1.0.0 \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os, requests r = requests.delete( "https://www.orgo.ai/api/templates/default/claude-code/1.0.0", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, ) print(r.json()) # {"deleted": true, "cancelledJobs": 0} ``` ```javascript JavaScript const r = await fetch( "https://www.orgo.ai/api/templates/default/claude-code/1.0.0", { method: "DELETE", headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` }, }, ); console.log(await r.json()); ``` ### Response ```json { "deleted": true, "cancelledJobs": 0 } ``` ## Errors | Status | Body | Meaning | |--------|------|---------| | `400` | `{ "error": string }` | A path segment is not a valid template namespace, name, or version, e.g. `"invalid template reference"`. | | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `401` | `{ "error": "This endpoint requires an account-wide credential (workspace_scope_mismatch)." }` | The key is workspace-scoped. Template endpoints need an account-wide key. This route answers with `401`, not `403`. | | `401` | `{ "error": "Service temporarily unavailable. …" }` | Orgo could not verify the key because of a server-side fault. The key is fine. Retry. | | `500` | `{ "error": string }` | Unexpected server error (`"internal error"`), or the template registry host failed or timed out. Retry; the delete is idempotent. | | `502` | `{ "error": "golden objects could not be scheduled for deletion", "code": "golden_delete_incomplete" }` | The version still exists. Retry. | | `503` | `{ "error": "template authority unavailable", "code": "delete_authority_unavailable" }` | The template record could not be read or written. The version still exists. Retry. | | `503` | `{ "error": string }` | No fleet host is available to serve the template registry. Retry. | --- # Build template Source: https://docs.orgo.ai/api-reference/templates/build Endpoint: POST /templates/{namespace}/{name}/{version}/build Summary: Bake a template version into a golden snapshot. Builds the [golden snapshot](https://docs.orgo.ai/guides/templates/introduction#golden-snapshots) for a version: Orgo boots a computer, runs the build steps and app installs, then captures a paused snapshot. A built template launches in seconds; an unbuilt one cannot launch at all. This call is **asynchronous** and returns immediately with `202 Accepted`. The build is queued for a dedicated build runner. Track progress by polling [Get build status](https://docs.orgo.ai/api-reference/templates/build-status). Requests are idempotent while a build is in flight: if a job for the same content is already queued or running, you get that job back instead of a second one. At most 10 of your builds can be queued or running at once. **Note:** Building templates requires a [Scale plan](https://orgo.ai/pricing) or higher. You can also build at publish time with [`POST /templates?auto_build=true`](https://docs.orgo.ai/api-reference/templates/publish). Reading the build status is not gated. **Tip:** The build is where `on_first_boot` runs, and it never runs again on a launch. `on_every_boot` runs during the build and again on any launch that cold-boots, such as one that injects secrets. See [hooks](https://docs.orgo.ai/guides/templates/schema#hooks). ## Path parameters - `namespace` (path, string, required): Template namespace. - `name` (path, string, required): Template name. - `version` (path, string, required): Version (semver) to build. ## Query parameters - `tier` (query, string) Build runner to use: `standard` (2 vCPU, 8 GB), `fast` (4 vCPU, 16 GB), or `turbo` (8 vCPU, 32 GB). Omitted, Orgo picks the smallest runner that holds the template's `hardware`. Any other value returns `400`. A runner smaller than the template's hardware returns `422`. - `launch` (query, boolean, default `false`) Set to `true` to create a computer from the template as soon as the build is ready. Only the exact string `true` enables it. The launch goes through the same plan and quota checks as [Create computer](https://docs.orgo.ai/api-reference/computers/create). - `project` (query, string) Workspace ID for the computer that `launch=true` creates. You must belong to the workspace, or the call returns `403`. Omitted, Orgo picks one of your workspaces. ## Response - `ref` (string): Template ref. - `digest` (string): Content-addressed digest being built. - `status` (string) `queued` while the job waits for a runner, `building` once a runner has claimed it. - `jobId` (string): Build job ID. - `phase` (string): Current job phase, e.g. `queued`. - `tier` (string): Build runner tier: `standard`, `fast`, or `turbo`. - `ahead` (integer): Queued jobs across all accounts that will run before this one. - `launchOnReady` (boolean): Whether a computer is created when the build is ready. For a job that was already in flight, this is that job's setting, not this request's. The shape above comes from the build queue. Production runs with the queue on, so this is the shape you get from `www.orgo.ai`. The queue is off by default in a deployment that does not enable it, and there the call still returns `202`, but with the older build-result shape: `ref`, `digest`, and `status` (`building`, or `ready` when a golden snapshot for this content already exists), without `jobId`, `phase`, `tier`, `ahead`, or `launchOnReady`. The `tier`, `launch`, and `project` query parameters are ignored in that mode. ## Example ```bash cURL curl -X POST https://www.orgo.ai/api/templates/default/claude-code/1.0.0/build \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os, requests, time ref = "default/claude-code/1.0.0" base = f"https://www.orgo.ai/api/templates/{ref}/build" hdr = {"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"} requests.post(base, headers=hdr) # kick off while True: # poll to ready status = requests.get(base, headers=hdr).json()["status"] print(status) if status in ("ready", "failed", "not_built"): break time.sleep(5) ``` ```javascript JavaScript const base = "https://www.orgo.ai/api/templates/default/claude-code/1.0.0/build"; const hdr = { Authorization: `Bearer ${process.env.ORGO_API_KEY}` }; await fetch(base, { method: "POST", headers: hdr }); let status = "building"; while (status === "building") { await new Promise((r) => setTimeout(r, 5000)); status = (await (await fetch(base, { headers: hdr })).json()).status; } console.log(status); ``` ### Response ```json { "ref": "default/claude-code@1.0.0", "digest": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "status": "queued", "jobId": "9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f", "phase": "queued", "tier": "standard", "ahead": 0, "launchOnReady": false } ``` ## Cancel a build Send a `DELETE` to the same build path to cancel a build that no runner has claimed yet. It returns `202` with `{ "status": "cancelled" }`, and the version's status reads `not_built` afterwards. A build a runner has already claimed runs to completion. When nothing is queued, Orgo falls back to cancelling on the template registry host: if that host had a build to cancel, the call returns `202` with `{ "status": "cancelling" }`. Otherwise, including for a build a runner has already claimed, it returns `404` with `{ "status": "idle" }`. ```bash curl -X DELETE https://www.orgo.ai/api/templates/default/claude-code/1.0.0/build \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ## Errors | Status | Body | Meaning | |--------|------|---------| | `400` | `{ "error": "unknown build tier \"\"" }` | `tier` is not `standard`, `fast`, or `turbo`. | | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `401` | `{ "error": "This endpoint requires an account-wide credential (workspace_scope_mismatch)." }` | The key is workspace-scoped. Template endpoints need an account-wide key. This route answers with `401`, not `403`. | | `401` | `{ "error": "Service temporarily unavailable. …" }` | Orgo could not verify the key because of a server-side fault. The key is fine. Retry. | | `403` | `{ "error": string, "code": "UPGRADE_REQUIRED", "upgradeTier": "scale_v2" }` | Below Scale, or your plan could not be verified. The gate fails closed, and no build starts. | | `403` | `{ "error": string, "code": "PLAN_LIMIT", "upgradeTier": "enterprise" }` | Your account is on a custom plan that does not include templates. No build starts. | | `403` | `{ "error": "you do not have access to that workspace" }` | `project` names a workspace you do not belong to. | | `409` | `{ "error": string }` | No such version is published in your namespaces, so there is nothing to build. Publish it first. | | `422` | `{ "error": string, "code": "HARDWARE_EXCEEDS_BUILD_RUNNER" }` | The template's `hardware` does not fit the requested `tier`, or any runner. The message names the smallest runner that fits. | | `429` | `{ "error": string }` | You already have 10 builds queued or running. Wait for one to finish. | | `500` | `{ "error": "internal error" }` | Unexpected server error. | `DELETE` (cancel) is not plan-gated and takes no query parameters. Its `404` body is `{ "status": "idle" }`, not an `error` object. --- # Get build status Source: https://docs.orgo.ai/api-reference/templates/build-status Endpoint: GET /templates/{namespace}/{name}/{version}/build Summary: Check whether a template version's golden snapshot is ready. Returns the current build state of a version. Poll this after [Build template](https://docs.orgo.ai/api-reference/templates/build) until `status` is `ready` or `failed`. Reading status is not plan-gated. A curated ref resolves too: pass the `system` namespace, e.g. `system/coding/1.0.4`, to read the build state of a curated template. ## Path parameters - `namespace` (path, string, required): Template namespace. - `name` (path, string, required): Template name. - `version` (path, string, required): Version (semver). ## Response - `status` (string) One of: - `not_built`: no golden snapshot yet, or the last queued build was cancelled. Call [Build template](https://docs.orgo.ai/api-reference/templates/build). - `building`: a build is queued or running. - `ready`: built and launchable. - `failed`: the last build failed. See `error`. - `ref` (string): Template ref. - `digest` (string): Content-addressed digest. - `build_time_ms` (integer): Build duration in milliseconds. Present once a queued build has finished. - `error` (string): Failure reason. Present when `status` is `failed`. The fields below are present when the version was built through the build queue, which is how [Build template](https://docs.orgo.ai/api-reference/templates/build) builds your own templates. They are absent for curated templates and for versions built before the queue. - `job_id` (string): ID of the newest build job for this version. - `phase` (string): Job phase. `queued` while waiting for a runner, `retrying` while waiting after a failed attempt. - `attempts` (integer): How many times a runner has attempted the job. - `queued_at` (string): ISO 8601 timestamp of when the job was queued. - `finished_at` (string): ISO 8601 timestamp of when the job finished, or `null` while it is in flight. ## Example ```bash cURL curl https://www.orgo.ai/api/templates/default/claude-code/1.0.0/build \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os, requests r = requests.get( "https://www.orgo.ai/api/templates/default/claude-code/1.0.0/build", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, ) print(r.json()["status"]) ``` ```javascript JavaScript const r = await fetch( "https://www.orgo.ai/api/templates/default/claude-code/1.0.0/build", { headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` } }, ); console.log((await r.json()).status); ``` ### Response ```json { "ref": "default/claude-code@1.0.0", "digest": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "status": "ready", "build_time_ms": 118420, "job_id": "9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f", "phase": "done", "attempts": 1, "queued_at": "2026-06-08T16:58:00Z", "finished_at": "2026-06-08T17:00:02Z" } ``` ## Errors | Status | Body | Meaning | |--------|------|---------| | `400` | `{ "error": string }` | The path does not form a valid `namespace/name@version` ref. The message starts with `invalid`. | | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `401` | `{ "error": "This endpoint requires an account-wide credential (workspace_scope_mismatch)." }` | The key is workspace-scoped. Template endpoints need an account-wide key. This route answers with `401`, not `403`. | | `401` | `{ "error": "Service temporarily unavailable. …" }` | Orgo could not verify the key because of a server-side fault. The key is fine. Retry. | | `404` | `{ "error": string }` | No such template version is published, in your namespaces or, for a `system` ref, among curated templates. | | `500` | `{ "error": string }` | Unexpected server error (`"internal error"`), or the template registry host failed or timed out. Retry. | | `503` | `{ "error": string }` | No fleet host is available to serve the template registry. Retry. | --- # Stream build logs Source: https://docs.orgo.ai/api-reference/templates/build-events Endpoint: GET /templates/{namespace}/{name}/{version}/build/events Summary: Follow a template build live over Server-Sent Events. Streams the build log as [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events). Use it to render a live, Docker-style build console. The stream stays open for the duration of the build. When the build ends (succeeds, fails, or is cancelled), it sends a final `event: end` frame whose data is `{}`, then closes. Closing the connection tears down the upstream subscription. **Warning:** The stream only carries a build that runs on the template registry host. Builds started with [Build template](https://docs.orgo.ai/api-reference/templates/build) or `POST /templates?auto_build=true` are queued for a dedicated build runner, which does not publish to this stream, so for those the call returns `404` with `{ "error": "no build in progress for this template" }`. Poll [Get build status](https://docs.orgo.ai/api-reference/templates/build-status) instead. **Note:** The stream is scoped to your own templates, so a curated (`system/…`) ref returns `404`. Each `data:` frame is a JSON object: - `ts` (string): Event timestamp (ISO 8601). - `level` (string): `info`, `success`, `warn`, or `error`. - `phase` (string) Build phase, e.g. `compile`, `boot`, `install`, `snapshot`, `record`, `archive`, or `ready`. - `source` (string): Origin of the line, e.g. `vm`, `orgo-init`, or `builder`. Optional. - `line` (string): A single line of build output. ## Path parameters - `namespace` (path, string, required): Template namespace. - `name` (path, string, required): Template name. - `version` (path, string, required): Version (semver). ## Example ```bash cURL curl -N https://www.orgo.ai/api/templates/default/claude-code/1.0.0/build/events \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os, json, requests with requests.get( "https://www.orgo.ai/api/templates/default/claude-code/1.0.0/build/events", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, stream=True, ) as r: for raw in r.iter_lines(): if raw and raw.startswith(b"data: "): event = json.loads(raw[6:]) if "line" in event: # the final `event: end` frame carries {} print(f"[{event['phase']}] {event['line']}") ``` ```javascript JavaScript // Browser: EventSource doesn't send Authorization headers, so stream with fetch. const r = await fetch( "https://www.orgo.ai/api/templates/default/claude-code/1.0.0/build/events", { headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` } }, ); const reader = r.body.getReader(); const decoder = new TextDecoder(); for (;;) { const { value, done } = await reader.read(); if (done) break; for (const line of decoder.decode(value).split("\n")) { if (line.startsWith("data: ")) { const e = JSON.parse(line.slice(6)); if ("line" in e) console.log(`[${e.phase}] ${e.line}`); // skip the final {} frame } } } ``` ### Stream ```text data: {"ts":"2026-06-08T17:00:01Z","level":"info","phase":"boot","source":"vm","line":"copying base rootfs"} data: {"ts":"2026-06-08T17:00:14Z","level":"info","phase":"install","source":"app-install","line":"npm install -g @anthropic-ai/claude-code"} data: {"ts":"2026-06-08T17:01:50Z","level":"success","phase":"snapshot","source":"vm","line":"snapshot captured"} data: {"ts":"2026-06-08T17:01:58Z","level":"success","phase":"ready","line":"build complete in 118420ms"} event: end data: {} ``` ## Errors Errors arrive as a JSON body with the status below, not as an event frame. A `200` always carries `Content-Type: text/event-stream`. | Status | Body | Meaning | |--------|------|---------| | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `401` | `{ "error": "This endpoint requires an account-wide credential (workspace_scope_mismatch)." }` | The key is workspace-scoped. Template endpoints need an account-wide key. This route answers with `401`, not `403`. | | `401` | `{ "error": "Service temporarily unavailable. …" }` | Orgo could not verify the key because of a server-side fault. The key is fine. Retry. | | `404` | `{ "error": string }` | No such template version in your namespaces, or no build of it is running on the registry host (`"no build in progress for this template"`). Forwarded from the build host. | | `502` | `{ "error": "upstream fetch failed: …" }` | The build host could not be reached. Retry. | | `503` | `{ "error": string }` | No fleet host is available to serve the stream. Retry. | --- # List curated templates Source: https://docs.orgo.ai/api-reference/templates/list-curated Endpoint: GET /templates/global Summary: Browse the templates Orgo publishes and maintains. Lists the curated templates Orgo builds and keeps up to date: ready-to-launch environments like Coding (Claude Code and Codex), OpenClaw, and Hermes Agent. Any authenticated account can list them, on any plan. To launch one, pass its `ref` as `template_ref` to [Create computer](https://docs.orgo.ai/api-reference/computers/create). Launching needs a **paid** plan. You don't need a Scale plan to launch a curated template, only to publish your own. The quota is the workspace owner's, and on the free plan it is zero computers, so the create returns `403` with code `UPGRADE_REQUIRED`. See [pricing](https://orgo.ai/pricing). ## Response - `templates` (array) Array of curated template list items, each with `ref`, `digest`, and `published`. Empty when the curated registry has no entries. To read one's full document, call [Get template](https://docs.orgo.ai/api-reference/templates/get) with its `system` ref. ## Example ```bash cURL curl https://www.orgo.ai/api/templates/global \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ```python Python import os, requests r = requests.get( "https://www.orgo.ai/api/templates/global", headers={"Authorization": f"Bearer {os.environ['ORGO_API_KEY']}"}, ) for t in r.json()["templates"]: print(t["ref"]) ``` ```javascript JavaScript const r = await fetch("https://www.orgo.ai/api/templates/global", { headers: { Authorization: `Bearer ${process.env.ORGO_API_KEY}` }, }); const { templates } = await r.json(); console.log(templates.map((t) => t.ref)); ``` ### Response ```json { "templates": [ { "ref": "system/coding@1.0.4", "digest": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "published": "2026-06-08T17:00:00Z" }, { "ref": "system/openclaw@1.1.8", "digest": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3", "published": "2026-06-08T17:00:00Z" }, { "ref": "system/hermes-agent@1.1.13", "digest": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4", "published": "2026-06-08T17:00:00Z" } ] } ``` **Tip:** To list every version of a single curated template, call `GET /templates/global/{namespace}/{name}`, e.g. `https://www.orgo.ai/api/templates/global/system/coding`. That route returns `{ "versions": [...] }`, and `404` `{ "error": "not found" }` when no curated template matches. ## Errors | Status | Body | Meaning | |--------|------|---------| | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `401` | `{ "error": "This endpoint requires an account-wide credential (workspace_scope_mismatch)." }` | The key is workspace-scoped. Template endpoints need an account-wide key. This route answers with `401`, not `403`. | | `401` | `{ "error": "Service temporarily unavailable. …" }` | Orgo could not verify the key because of a server-side fault. The key is fine. Retry. | | `500` | `{ "error": string }` | Unexpected server error (`"internal error"`), or the template registry host failed or timed out. Retry. | | `503` | `{ "error": string }` | No fleet host is available to serve the template registry. Retry. | **Launch a curated template** Go from a curated `ref` to a running computer in one API call. --- # Test-run a template Source: https://docs.orgo.ai/api-reference/templates/test-run Endpoint: POST /templates/run Summary: Boot a short-lived preview computer from a template ref. Boots a **short-lived, auto-reaped** computer from a template `ref`. This is the same in-editor “Run” that the dashboard uses to test a template before sharing it. The preview is reclaimed automatically, so a missed stop won't leak a computer. **Note:** Test-runs require a [Scale plan](https://orgo.ai/pricing) or higher: the preview is the authoring build-test, so it is gated the same way as publish and build. For a **persistent** computer, use [Create computer](https://docs.orgo.ai/api-reference/computers/create) with `template_ref` instead. That is the durable path, and it counts toward the workspace owner's computer quota. A preview does not. ## Request body - `ref` (body, string, required) Template ref to boot, in `namespace/name@version` form. Your own (`default/…`) or a curated (`system/…`) ref. The template's build must be `ready`. A missing or malformed `ref` returns `400`. ## Response Returns the preview computer you connect to exactly like any other. The body is the fleet's create-computer shape plus three added fields. - `desktop_id` (string) The preview's identifier in Orgo. Use it as the `instance_id` in connection URLs (`wss://www.orgo.ai/desktops/{instance_id}/ws/websockify`, `…/ws/terminal`, `…/ws/audio`), and pass it back to stop the run. - `desktop_api_token` (string) Bearer token for the computer's Desktop API: `/bash`, `/terminal`, `/events`. Send it verbatim. Equal to `vnc_password`. - `public_host` (string) Public hostname of the fleet host this preview was placed on. - `id` (string) The preview's id on the fleet host. `DELETE` also accepts this value. - `status` (string) Current status. - `vnc_password` (string) VNC / WebSocket token for the preview. - `ip` (string) The computer's address on the host's private network. - `novnc_port` (integer) Host port serving noVNC / websockify. - `api_port` (integer) Host port serving the Desktop API. - `resolution` (string) Display resolution, `WIDTHxHEIGHTxDEPTH`. - `vcpus` (integer) vCPU count the preview booted with. - `mem_mb` (integer) RAM in MB. - `disk_size_gb` (integer) Disk in GB. - `bandwidth_limit_mbps` (integer) Network bandwidth cap in Mbps. - `audio_enabled` (boolean) Whether the audio device is enabled. - `template_ports` (object) Map of the template's public `internal` ports to the host ports they were published on. Absent when the template exposes no public ports. See [Create computer](https://docs.orgo.ai/api-reference/computers/create) for the full connection model. ## Example ```bash Start curl -X POST https://www.orgo.ai/api/templates/run \ -H "Authorization: Bearer $ORGO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"ref": "default/my-template@1.0.0"}' ``` ```bash Stop # Stop the preview when you're done (id = desktop_id from the run response) curl -X DELETE "https://www.orgo.ai/api/templates/run?id=$COMPUTER_ID" \ -H "Authorization: Bearer $ORGO_API_KEY" ``` ## Stop a test-run `DELETE /templates/run?id={instance_id}` stops the preview and reclaims it. Pass the `desktop_id` from the run response, or the `id`. Returns `200` `{ "deleted": true }`, including when the preview was already gone. Omitting `id` returns `400` `{ "error": "id required" }`, and a request with no `Authorization` header returns `401` `{ "error": "Authentication required" }`. Any other upstream failure is forwarded with its own status and `{ "error": string }`. Previews are also reaped automatically. ## Errors | Status | Body | Meaning | |--------|------|---------| | `400` | `{ "error": "invalid JSON body" }` | The body is not valid JSON. | | `400` | `{ "error": "ref required" }` | No `ref` in the body. | | `400` | `{ "error": string }` | `ref` is not in `namespace/name@version` form. The message starts with `invalid ref`. | | `401` | `{ "error": "Invalid API key" }` | The Bearer token starts with `sk_` but is not a known Orgo key. | | `401` | `{ "error": "Authentication required" }` | No `Authorization` header, or a Bearer token that is not an `sk_` key. | | `401` | `{ "error": "This endpoint requires an account-wide credential (workspace_scope_mismatch)." }` | The key is workspace-scoped. Template endpoints need an account-wide key. This route answers with `401`, not `403`. | | `401` | `{ "error": "Service temporarily unavailable. …" }` | Orgo could not verify the key because of a server-side fault. The key is fine. Retry. | | `403` | `{ "error": string, "code": "UPGRADE_REQUIRED", "upgradeTier": "scale_v2" }` | Below Scale, or your plan could not be verified. The gate fails closed. | | `403` | `{ "error": string, "code": "PLAN_LIMIT", "upgradeTier": "enterprise" }` | Your account is on a custom plan that does not include templates, or the template's hardware exceeds that plan's per-computer limit. | | `409` | `{ "error": string, "code": "TEMPLATE_NOT_READY" }` | The template's build isn't `ready`, or the version does not exist. [Build it](https://docs.orgo.ai/api-reference/templates/build) first. | | `429` | `{ "error": string, "code": string }` | Run rate limit or concurrency cap reached, e.g. `vm_rate_limited` or `tenant_vm_limit`. Test-runs of curated (`system/…`) refs share these limits across all accounts. Back off and retry. | | other `4xx` or `5xx` | `{ "error": string, "code"?: string, "upgradeTier"?: string }` | Any other failure from the fleet host is forwarded with the host's status. | | `500` | `{ "error": "internal error" }` | Unexpected server error. | | `500` | `{ "error": "could not prepare a preview connection for this run" }` | The preview booted but its connection record could not be stored, so it was torn back down. Retry. | | `503` | `{ "error": string, "code"?: string, "retryable"?: true }` | The fleet couldn't serve this run: `golden_unavailable` (the built snapshot is missing), or `TEMPLATE_READINESS_UNAVAILABLE` (Orgo could not determine whether the build is ready), which carries `retryable: true`. A `503` with `error` alone means no fleet host was available, or a custom plan's limits could not be read. Retry. |