# MIOSA Documentation - Full Text Generated: 2026-06-09T07:08:00.015Z Canonical origin: https://miosa.ai Worker fallback origin: https://miosa.roberto-c49.workers.dev This file is generated from src/routes/docs. It is intended for AI agents, crawlers, search indexers, and customers who need a single plain-text copy of the public documentation. --- # MIOSA Documentation URL: https://miosa.ai/docs Fallback URL: https://miosa.roberto-c49.workers.dev/docs Source: src/routes/docs/+page.md Description: Build, preview, deploy, and operate AI-generated applications on managed compute. # MIOSA Documentation Build, preview, deploy, and operate AI-generated applications from one API. MIOSA gives your product isolated sandboxes for code work, live previews for iteration, durable deployments for production, desktop computers for browser agents, and white-label controls for customer-owned experiences.
Start building → API reference

Last updated:

--- ## Start here Create a sandbox, run a command, expose a live preview, and understand the resource model before wiring MIOSA into your product. Use isolated Linux environments for generated code, dependency installs, file edits, build commands, tests, and preview servers. Publish generated applications to stable URLs with versions, releases, environment variables, dynamic runtimes, and rollback paths. Put previews, deployments, browser tokens, and customer attribution behind your own product and domain model. Give agents a full Linux desktop when they need browsers, screenshots, clicks, uploads, form fills, or human handoff. Use the REST API directly, ship with SDKs, or automate workflows from the CLI. Always trust URLs returned by the API. --- ## What you can build An agent writes files into a sandbox, runs builds, opens previews, applies feedback, snapshots progress, and publishes a production app. Give each customer isolated workspaces, branded preview domains, deployment domains, custom domains, browser tokens, and usage attribution. Turn prompts into published landing pages, intake forms, calculators, content hubs, and campaign sites without exposing MIOSA. Run apps that need a server process, API routes, background work, environment variables, and persistent release management. Execute user code in isolated environments, stream stdout and stderr, capture artifacts, and shut work down when finished. Use Computers for real browser sessions: navigate, click, type, upload files, capture screenshots, and hand control to users. Bring third-party computer capacity into MIOSA while keeping one API, one product surface, and one user relationship. Measure sandbox lifecycles, desktop readiness, screenshot/action latency, and task success across agent workflows. --- ## Platform guides --- ## Developer surfaces | Surface | Use it when | |---|---| | [REST API](/docs/api-reference/) | You want complete control over sandboxes, files, exec, previews, deployments, computers, domains, tokens, and events. | | [CLI](/docs/cli/) | You want to create, inspect, publish, and debug resources from a terminal. | | [Python SDK](/docs/sdks/python/) | You are building agent loops, backend orchestration, or data workflows in Python. | | [TypeScript SDK](/docs/sdks/typescript/) | You are building Node services, web backends, or product integrations in TypeScript. | | [MCP server](/docs/mcp/) | You want an AI agent to use MIOSA tools through the Model Context Protocol. | | [Agent Development Kit](/docs/adk/) | You want higher-level agent building blocks on top of the raw sandbox and deployment APIs. | --- ## For LLMs MIOSA publishes machine-readable docs indexes so agents can retrieve current public documentation without scraping the site. --- ## Current production contract Use URLs returned by the API instead of constructing hostnames yourself: - `preview_url` for sandbox previews. - `desktop_url` for computer desktops. - `public_url` for deployments. - `custom_domain` or deployment-domain URLs when a workspace has branded domains configured. See the [changelog](/docs/changelog/) for the latest public platform behavior. --- # Agent Development Kit (ADK) URL: https://miosa.ai/docs/adk Fallback URL: https://miosa.roberto-c49.workers.dev/docs/adk Source: src/routes/docs/adk/+page.md Description: Build LLM agents that use MIOSA sandboxes, desktops, and deploys with the tool-call loop already wired. TypeScript ADK first; Python next. # Agent Development Kit (ADK) The MIOSA ADK lets you build production-ready LLM agents that natively use MIOSA primitives - sandboxes, full desktop environments, and one-click deploys - with the tool-call loop already wired. If `@miosa/sdk` is "call MIOSA APIs from code", `@miosa/adk` is **"build an AGENT that uses MIOSA"**. ```bash npm install @miosa/adk @anthropic-ai/sdk ``` --- ## 30-second example ```typescript const agent = new Agent({ provider: anthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY!, model: "claude-opus-4-7", }), miosaApiKey: process.env.MIOSA_API_KEY!, }); const result = await agent.run({ prompt: "Create a MIOSA sandbox, write a Python hello-world, run it, then destroy the sandbox.", maxIterations: 10, }); console.log(result.finalText); ``` That's it. The agent boots a sandbox, writes the file, runs it, prints output, destroys the sandbox, returns the final response. No tool-loop boilerplate. --- ## ADK vs SDK The raw TypeScript SDK around MIOSA's REST API. You call methods: ```typescript const computer = await client.computers.create({ name: "x" }); await computer.exec.bash("echo hello"); ``` Use this when you're **writing application code** that talks to MIOSA. Adds the agent loop on top. You give it a provider + prompt: ```typescript const agent = new Agent({ provider, miosaApiKey }); await agent.run({ prompt: "build a flask app and run it" }); ``` Use this when you're **building an agent** that should use MIOSA. --- ## Built-in MIOSA tools When you pass `miosaApiKey`, the agent gets these tools out of the box: | Tool | Does | |------|------| | `create_sandbox` | Boot a Debian 12 microVM (Python + Node). | | `create_desktop` | Start a full desktop environment. | | `list_sandboxes` | List your active computers. | | `get_sandbox` | Fetch one sandbox by id (status / IP). | | `destroy_sandbox` | Delete a computer. | | `exec` | Run a bash command. | | `exec_python` | Run a Python snippet inline. | | `read_file` | Read a file from the VM. | | `write_file` | Write a file inside the VM. | | `list_files` | List a directory inside the VM. | Get an API key at , or run `npx -y @miosa/cli mcp install` to wire one via browser-based auth. --- ## Custom tools Compose your own tool catalogue alongside (or instead of) the MIOSA tools: ```typescript Agent, anthropicProvider, miosaTools, type Tool, } from "@miosa/adk"; const miosa = new Miosa({ apiKey: process.env.MIOSA_API_KEY! }); const sendSlack: Tool = { name: "send_slack", description: "Post a message to the #builds Slack channel.", inputSchema: { type: "object", properties: { text: { type: "string" } }, required: ["text"], }, async execute(args) { const text = String(args["text"] ?? ""); // your Slack call here return `Sent: ${text}`; }, }; const agent = new Agent({ provider: anthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY! }), tools: [...miosaTools({ client: miosa }), sendSlack], }); ``` --- ## Streaming step events Drive a live UI as the agent runs: ```typescript const result = await agent.run({ prompt: "...", onStep: (step) => { console.log(`[turn ${step.index}] ${step.text}`); for (const call of step.toolCalls) { console.log( ` → ${call.name}(${JSON.stringify(call.input)}) → ${call.output.slice(0, 80)}`, ); } }, }); ``` The `AgentStep` shape: ```typescript interface AgentStep { index: number; text: string; toolCalls: Array<{ id: string; name: string; input: Record; output: string; isError: boolean; }>; usage?: { inputTokens: number; outputTokens: number }; } ``` --- ## Providers `@miosa/adk` ships with one provider today; the `Provider` interface is small enough that adding more is straightforward. ```typescript const provider = anthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY!, model: "claude-opus-4-7", // default maxTokens: 4096, // default }); ``` Uses `@anthropic-ai/sdk` Messages API with native tool-use. Supports any Claude model that supports tool calling. Implement the `Provider` interface for OpenAI, Gemini, Ollama, or anything that supports tool use: ```typescript const openaiProvider: Provider = { name: "openai", model: "gpt-4o", async step(args: ProviderStepArgs): Promise { // 1. translate args.messages + args.tools into OpenAI's format // 2. call openai.chat.completions.create(...) // 3. extract text + tool_calls + stop_reason return { text, toolCalls, stopReason: "end_turn" }; }, }; const agent = new Agent({ provider: openaiProvider, miosaApiKey: "..." }); ``` PRs adding first-party OpenAI / Gemini / Ollama providers are welcome at [the MIOSA monorepo](https://github.com/Miosa-osa/miosa/tree/main/sdks/adk-typescript). --- ## Termination `agent.run()` returns when: - The model emits an `end_turn` with no further tool calls (normal completion) - A `stop_sequence` is hit - `maxIterations` is reached (default 10) - safety guard against runaway loops - The provider throws (returned as `stopReason: "error"`) ```typescript const result = await agent.run({ prompt: "...", maxIterations: 20 }); if (result.stopReason === "max_iterations") { console.warn("Agent didn't finish in 20 turns"); } ``` --- ## Quick comparison | | `@miosa/adk` | Anthropic Claude Agent SDK | OpenAI Agents SDK | Google ADK | |--|--|--|--|--| | Tool-call loop | ✓ | ✓ | ✓ | ✓ | | MIOSA tools built-in | **✓** | - | - | - | | Anthropic provider | ✓ | ✓ | - | - | | OpenAI provider | (BYO) | - | ✓ | - | | Sub-agents | (roadmap) | ✓ | ✓ | ✓ | | Memory | (roadmap) | ✓ | (basic) | ✓ | | Cost | free | free | free | free | The ADK is intentionally small. If you want MIOSA-as-execution-substrate with a clean agent loop, you're in the right place. --- ## What's coming next - **Python ADK** (`pip install miosa-adk`) - same surface, Python ergonomics. - **More providers** - first-party OpenAI, Gemini, Ollama. - **Sub-agents** - spawn child agents with scoped tool subsets. - **Memory** - built-in conversation memory with TTL + summarization. - **Streaming output** - token-by-token via provider.streamStep. Open an issue at if there's something you need sooner. --- # Interactive API Reference - MIOSA Docs URL: https://miosa.ai/docs/api Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api Source: src/routes/docs/api/+page.svelte Description: Browse and try every MIOSA API endpoint interactively. Covers sandboxes, computers, deployments, API keys, events, and more. {#if !mounted} {/if} --- # API Reference URL: https://miosa.ai/docs/api-reference Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference Source: src/routes/docs/api-reference/+page.md Description: REST API reference for the MIOSA platform - base URL, auth, request/response envelopes, and the full endpoint index. Want to browse and try endpoints without writing code? The Interactive API Reference renders the full OpenAPI spec with an in-browser request runner - no setup required. Every MIOSA API endpoint is REST over HTTPS, returns JSON, and follows a consistent envelope format. The SDKs wrap these endpoints - you can use them directly when you need low-level control or want to integrate from a language without an official SDK. ## Base URL ``` https://api.miosa.ai/api/v1 ``` ## Authentication All requests require a Bearer token: ```http Authorization: Bearer msk_live_... ``` MIOSA API keys (`msk_*`) and short-lived JWT tokens are both accepted. Keys are scoped to an organization and its workspaces/projects. See [API Keys](/docs/platform/api-keys/) for scope details. ## Request format - `Content-Type: application/json` for all request bodies. - File uploads use `multipart/form-data`. - All field names are **snake_case**. - Mutation requests (`POST`, `PUT`, `PATCH`, `DELETE`) should include an `Idempotency-Key` header to enable safe retries: ```http POST /api/v1/sandboxes Authorization: Bearer msk_live_... Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 Content-Type: application/json { "template": "miosa-sandbox", "workspace_slug": "dr-smith-clinic", "project_slug": "lead-magnet", "external_workspace_id": "clinic_123", "external_user_id": "dr-smith-456", "external_project_id": "project_789" } ``` Use a UUID v4 as the idempotency key. Repeating the same key within 24 hours returns the original response without re-executing the mutation. ## Response envelope Single-resource responses: ```json { "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "my-sandbox", "status": "running", "created_at": "2026-05-17T10:00:00Z" } } ``` List responses include a pagination object: ```json { "data": [ { "id": "...", "name": "..." } ], "page": { "page": 1, "page_size": 20, "total": 150 } } ``` Pagination parameters accepted by all list endpoints: | Parameter | Type | Default | Max | |---|---|---|---| | `page` | integer | 1 | - | | `page_size` | integer | 20 | 100 | ## Error envelope All errors follow a single shape: ```json { "error": { "code": "sandbox_not_found", "message": "No sandbox with id sbx_abc exists in this workspace.", "request_id": "req_01jv..." } } ``` Include the `request_id` when contacting support. ### HTTP status codes | Status | Meaning | |---|---| | 200 | Success | | 201 | Created | | 202 | Accepted - async operation started | | 400 | Bad request - missing or invalid parameters | | 401 | Unauthorized - invalid or missing token | | 403 | Forbidden - valid token, insufficient scope | | 404 | Not found | | 409 | Conflict - invalid state transition (e.g. starting a running computer) | | 413 | Payload too large - file upload exceeds 10 MB | | 422 | Unprocessable entity - validation error | | 429 | Rate limited | | 500 | Internal server error | | 502 | Bad gateway - command service unreachable | ## Rate limits | Scope | Limit | |---|---| | General API | 300 req/min per workspace | | Auth endpoints | 20 req/min | | Public/unauthenticated | 60 req/min | ## IDs and timestamps All resource IDs are UUID v4. All timestamps are ISO 8601 in UTC: `2026-05-17T10:00:00Z`. ## Ownership fields Create endpoints for resources accept the same ownership selectors: | Field | Use | |---|---| | `workspace_id` | Existing MIOSA workspace UUID | | `workspace_slug` | Existing or auto-created workspace slug | | `workspace_name` | Display name used when auto-creating a workspace | | `project_id` | Existing MIOSA project UUID | | `project_slug` | Existing or auto-created project slug | | `project_name` | Display name used when auto-creating a project | | `external_workspace_id` | Your customer/account/workspace ID | | `external_user_id` | Your end-user ID | | `external_project_id` | Your project/app/document ID | Responses include `workspace_id` and `project_id` when the resource is owned by a workspace/project. See [Ownership and Attribution](/docs/platform/attribution/). --- ## Compute Create, list, get, stop, and delete sandbox environments. The core compute primitive. [Open →](/docs/api-reference/sandboxes/) Run bash commands and Python code inside a running computer or sandbox. [Open →](/docs/api-reference/exec/) Server-sent events stream for long-running commands - line-by-line stdout/stderr. [Open →](/docs/api-reference/streaming-exec/) Read, write, stat, mkdir, rename, copy, and chmod files inside a computer. [Open →](/docs/api-reference/files/) Directory listings, multi-file operations, and archive upload/download. [Open →](/docs/api-reference/filesystem/) Create, restore, and delete point-in-time snapshots of a computer's disk state. [Open →](/docs/api-reference/snapshots/) ## Deploy Manage static and server deployments - create, list, get, and delete. [Open →](/docs/api-reference/deployments/) Immutable deployment versions. Each publish creates a new version. [Open →](/docs/api-reference/versions/) Promote a version to a named release (production, staging, etc.). [Open →](/docs/api-reference/releases/) Register, verify, and manage custom domains on deployments. [Open →](/docs/api-reference/custom-domains/) ## Computers Full lifecycle for desktop VM computers - create, start, stop, restart, clone, resize. [Open →](/docs/api-reference/computers/) Desktop control actions: screenshot, click, type, key, scroll, drag, hotkey, windows, accessibility tree. [Open →](/docs/api-reference/desktop/) Register your own hardware as a MIOSA computer (BYOC). One command, any machine. [Open →](/docs/api-reference/open-computers/) ## Platform Customer/client workspaces inside an organization. [Open →](/docs/api-reference/workspaces/) Apps, websites, documents, and workflows inside a workspace. [Open →](/docs/api-reference/projects/) Managed background services attached to a computer (databases, queues, etc.). [Open →](/docs/api-reference/services/) Inbound and outbound network rules for a computer. [Open →](/docs/api-reference/network-policy/) Lifecycle events emitted by computers and sandboxes - for webhooks and audit logs. [Open →](/docs/api-reference/events/) Read credit balance, list usage transactions, and top up. [Open →](/docs/api-reference/credits/) List available regions and their current capacity. [Open →](/docs/api-reference/regions/) The SDKs expose every endpoint listed above. If you prefer not to make raw HTTP calls, start with the [Python SDK](https://pypi.org/project/miosa/) or [TypeScript SDK](https://www.npmjs.com/package/@miosa/sdk). --- # Audit Log API URL: https://miosa.ai/docs/api-reference/audit-log Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/audit-log Source: src/routes/docs/api-reference/audit-log/+page.md Description: Retrieve a cursor-paginated audit log of all actions taken within your tenant. The audit log records all significant actions performed within your tenant - resource creation and deletion, API key usage, settings changes, and more. Results are cursor-paginated and ordered newest-first. Base path: `/api/v1/audit-log` Audit events are immutable. There is no write API. The log is append-only and retained for 90 days on Pro plans, 365 days on Enterprise. --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/audit-log` | Fetch recent audit events | --- ## List Audit Events **`GET /api/v1/audit-log`** ### Auth ``` Authorization: Bearer msk_... ``` ### Query Parameters | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `limit` | integer | No | `50` | Events per page (max `100`) | | `after` | string | No | - | Cursor from previous response's `next_cursor` | | `type` | string | No | - | Filter by event type prefix (e.g. `sandbox.`) | | `actor_id` | string | No | - | Filter by actor user ID | | `resource_type` | string | No | - | Filter by resource type (e.g. `sandbox`) | | `resource_id` | string | No | - | Filter by resource ID | ### Response - `200 OK` ```json { "data": [ { "id": "evt_01hwxyz...", "type": "sandbox.created", "actor": { "type": "api_key", "id": "user_clinic_42" }, "resource": { "type": "sandbox", "id": "sbx_abc123" }, "ts": "2026-05-26T10:14:23Z", "metadata": { "image": "debian-12-sandbox-v8", "workspace_id": "ws_01hwxyz..." } } ], "next_cursor": "MjAyNi0wNS0yNlQxMDoxNDoyM1o" } ``` | Field | Type | Description | |-------|------|-------------| | `data[].id` | string | Unique event ID | | `data[].type` | string | Dot-namespaced event type | | `data[].actor.type` | string | `"user"` or `"api_key"` | | `data[].actor.id` | string | User ID or `external_user_id` for API key actors | | `data[].resource.type` | string | Resource type (e.g. `sandbox`, `computer`, `webhook`) | | `data[].resource.id` | string | Resource ID | | `data[].ts` | string | ISO 8601 timestamp | | `data[].metadata` | object | Event-specific detail fields | | `next_cursor` | string | Opaque cursor; omitted when no more pages | ### Common Event Types | Type | Trigger | |------|---------| | `sandbox.created` | Sandbox provisioned | | `sandbox.destroyed` | Sandbox deleted | | `sandbox.exec` | `POST /exec` called | | `computer.created` | Computer provisioned | | `computer.stopped` | Computer stopped | | `computer.deleted` | Computer deleted | | `snapshot.created` | Snapshot initiated | | `api_key.created` | API key minted | | `api_key.deleted` | API key revoked | | `tenant.preview_domain.set` | Preview domain configured | | `webhook.created` | Outgoing webhook created | | `quota.upserted` | Per-user quota set | | `settings.updated` | Tenant settings changed | Use the `type` filter with a prefix to narrow events (e.g. `type=sandbox.` returns all sandbox events). --- ## Pagination Audit events are returned newest-first. To page forward: 1. Issue the initial request without `after`. 2. If `next_cursor` is present, re-issue with `after={next_cursor}`. 3. Stop when `next_cursor` is absent. Cursors are opaque base64url strings encoding a timestamp. Do not parse or construct them. --- ## Errors | Status | Code | Cause | |--------|------|-------| | 400 | - | Invalid cursor format | --- ## Examples ```bash # First page curl "https://api.miosa.ai/api/v1/audit-log?limit=50&type=sandbox." \ -H "Authorization: Bearer msk_live_..." # Next page curl "https://api.miosa.ai/api/v1/audit-log?limit=50&after=MjAyNi0wNS0yNlQxMDoxNDoyM1o" \ -H "Authorization: Bearer msk_live_..." # Filter by resource curl "https://api.miosa.ai/api/v1/audit-log?resource_type=computer&resource_id=cmp_xyz789" \ -H "Authorization: Bearer msk_live_..." ``` ```python client = miosa.Miosa() # Page through all sandbox events in the last page cursor = None while True: page = client.audit_log.list( type="sandbox.", limit=100, after=cursor, ) for event in page.data: print(event.type, event.resource.id, event.ts) if not page.next_cursor: break cursor = page.next_cursor ``` ```typescript const client = new Miosa(); let cursor: string | undefined; do { const page = await client.auditLog.list({ type: 'sandbox.', limit: 100, after: cursor, }); for (const event of page.data) { console.log(event.type, event.resource.id, event.ts); } cursor = page.nextCursor; } while (cursor); ``` --- # Computers API URL: https://miosa.ai/docs/api-reference/computers Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/computers Source: src/routes/docs/api-reference/computers/+page.md Description: API reference for creating, managing, and controlling MIOSA computers - full lifecycle, desktop control, files, metrics, and env vars. Computers are persistent desktop VMs with a Linux desktop, streamed desktop access, and APIs for exec, files, apps, metrics, and desktop control. Base path: `/api/v1/computers`. Verbs supported: **GET** (list/get/config), **POST** (create/start/stop/restart/clone/resize), **PATCH** (update), **PUT** (update), **DELETE** (delete). All mutation endpoints (`POST`, `PATCH`, `PUT`, `DELETE`) accept an `Idempotency-Key` header. Use a UUID v4. Repeating the same key within 24 hours returns the original response without re-executing the mutation. Rate limit: 300 requests/min per workspace across all API endpoints. Exceeding the limit returns HTTP 429 with a `Retry-After` header. --- ## Create a Computer **`POST /api/v1/computers`** Creates a new computer and begins provisioning. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | Display name | | `template_type` | string | Yes | Template (`"miosa-desktop"`) | | `size` | string | No | `"small"` (4GB/1CPU), `"medium"` (8GB/2CPU), `"large"` (16GB/4CPU). Default: `"small"` | | `selected_apps` | string[] | No | Applications to install after boot | | `workspace_id` | UUID | No | Workspace to assign this computer to. Defaults to the tenant's default workspace. | | `workspace_slug` | string | No | Existing or auto-created workspace slug. | | `workspace_name` | string | No | Workspace display name if auto-created. | | `project_id` | UUID | No | Project to assign this computer to. Defaults to the workspace default project. | | `project_slug` | string | No | Existing or auto-created project slug. | | `project_name` | string | No | Project display name if auto-created. | | `external_workspace_id` | string | No | Your own customer/account/workspace ID. | | `external_user_id` | string | No | Your own end-user ID. | | `external_project_id` | string | No | Your own project/app/document ID. | ### Response - `201 Created` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "tenant_id": "...", "owner_user_id": "...", "name": "my-computer", "slug": "my-computer", "template_type": "miosa-desktop", "size": "small", "vcpus": 1, "memory_mb": 4096, "status": "provisioning", "vm_id": null, "workspace_id": "660e8400-e29b-41d4-a716-446655440001", "project_id": "770e8400-e29b-41d4-a716-446655440002", "external_workspace_id": null, "external_user_id": null, "external_project_id": null, "sandbox_url": "https://my-computer.sandbox.miosa.ai", "desktop_url": "https://my-computer.computer.miosa.ai/desktop/index.html", "selected_apps": [], "settings": {}, "ai_config": {}, "agent_session_id": null, "agent_status": null, "resolution": "1280x720", "auto_stop": 0, "created_at": "2026-04-11T00:00:00Z", "updated_at": "2026-04-11T00:00:00Z" } ``` `sandbox_url` is a legacy preview field on the Computer response. Use `desktop_url` for the desktop stream and the `/computers/{id}/urls` endpoint for short-lived desktop and terminal URLs. ### Errors | Status | Error | Cause | |--------|-------|-------| | 400 | `name is required` | Missing name or template_type | | 402 | `INSUFFICIENT_CREDITS` | Not enough credits to create | | 422 | `TENANT_RESOLUTION_FAILED` | Cannot determine tenant for user | ```bash curl -X POST https://api.miosa.ai/api/v1/computers \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "my-computer", "template_type": "miosa-desktop", "size": "small", "workspace_slug": "dr-smith-clinic", "workspace_name": "Dr. Smith Clinic", "project_slug": "records-portal", "project_name": "Records Portal", "external_workspace_id": "clinic_123", "external_user_id": "dr-smith-456", "external_project_id": "project_789" }' ``` --- ## List Computers **`GET /api/v1/computers`** Returns all computers belonging to the authenticated user's tenant. ### Query Parameters | Parameter | Type | Description | |-----------|------|-------------| | `workspace_id` | UUID | Filter computers to a specific workspace. Omit to return computers across all workspaces. | | `project_id` | UUID | Filter computers to a specific project. | | `external_workspace_id` | string | Filter by your customer/account/workspace ID. | | `external_user_id` | string | Filter by your end-user ID. | | `external_project_id` | string | Filter by your project/app/document ID. | ### Response - `200 OK` ```json { "computers": [ { "id": "...", "name": "my-computer", "status": "running", "size": "small", "created_at": "2026-04-11T00:00:00Z" } ], "total": 1 } ``` ```bash # All computers curl https://api.miosa.ai/api/v1/computers \ -H "Authorization: Bearer $MIOSA_API_KEY" # Filtered to a workspace curl "https://api.miosa.ai/api/v1/computers?workspace_id=660e8400-e29b-41d4-a716-446655440001" \ -H "Authorization: Bearer $MIOSA_API_KEY" # Filtered to a project curl "https://api.miosa.ai/api/v1/computers?project_id=770e8400-e29b-41d4-a716-446655440002" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Get a Computer **`GET /api/v1/computers/{id}`** ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | `id` | UUID | Computer ID | ### Response - `200 OK` Full computer object (same as create response, with current status). ### Errors | Status | Error | Cause | |--------|-------|-------| | 400 | `invalid computer id` | Not a valid UUID | | 404 | `computer not found` | Does not exist or belongs to different tenant | ```bash curl https://api.miosa.ai/api/v1/computers/{id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Update a Computer **`PATCH /api/v1/computers/{id}`** Currently supports updating `agent_session_id` only. ### Request Body | Field | Type | Description | |-------|------|-------------| | `agent_session_id` | string | Link an AI agent session | ### Response - `200 OK` Updated computer object. ```bash curl -X PATCH https://api.miosa.ai/api/v1/computers/{id} \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"agent_session_id": "session-uuid"}' ``` --- ## Delete a Computer **`DELETE /api/v1/computers/{id}`** Permanently destroys the VM and removes the computer record. ### Response - `200 OK` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "deleted": true } ``` ### Errors | Status | Error | Cause | |--------|-------|-------| | 403 | `not a member of this computer` | No access to this computer | | 404 | `computer not found` | Does not exist | ```bash curl -X DELETE https://api.miosa.ai/api/v1/computers/{id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Start a Computer **`POST /api/v1/computers/{id}/start`** Resumes a stopped or paused computer. ### Response - `200 OK` Updated computer object with `status: "running"`. ### Errors | Status | Error | Cause | |--------|-------|-------| | 402 | `INSUFFICIENT_CREDITS` | Not enough credits | | 409 | `computer cannot be started from its current status` | Not in a startable state | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/start \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Stop a Computer **`POST /api/v1/computers/{id}/stop`** Pauses a running computer. Can be restarted later. ### Response - `200 OK` Updated computer object with `status: "stopped"`. ### Errors | Status | Error | Cause | |--------|-------|-------| | 409 | `computer cannot be stopped from its current status` | Not running | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/stop \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Restart a Computer **`POST /api/v1/computers/{id}/restart`** Stops and immediately restarts a running computer. ### Response - `200 OK` Updated computer object. ### Errors | Status | Error | Cause | |--------|-------|-------| | 409 | `computer must be running to restart` | Not running | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/restart \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Get Auto-Stop Configuration **`GET /api/v1/computers/{id}/auto-stop`** ### Response - `200 OK` ```json { "auto_stop_seconds": 3600, "enabled": true } ``` ```bash curl https://api.miosa.ai/api/v1/computers/{id}/auto-stop \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Update Auto-Stop Configuration **`PATCH /api/v1/computers/{id}/auto-stop`** ### Request Body | Field | Type | Description | |-------|------|-------------| | `auto_stop_seconds` | integer | Seconds of inactivity before auto-stop. `0` or `null` to disable. | ### Response - `200 OK` Updated auto-stop configuration. ```bash curl -X PATCH https://api.miosa.ai/api/v1/computers/{id}/auto-stop \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"auto_stop_seconds": 3600}' ``` --- ## Get Desktop Stream Credentials **`GET /api/v1/computers/{id}/vnc-credentials`** Returns credentials for direct desktop streaming access. ### Response - `200 OK` ```json { "desktop_url": "https://my-computer.computer.miosa.ai/desktop/index.html", "ws_url": "wss://my-computer.computer.miosa.ai/ws/vnc/550e8400-e29b-41d4-a716-446655440000?auth=stream_token", "token": "stream_auth_token", "expires_at": 1712700060, "computer_id": "550e8400-e29b-41d4-a716-446655440000", "slug": "my-computer" } ``` ```bash curl https://api.miosa.ai/api/v1/computers/{id}/vnc-credentials \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Get Stream Token **`POST /api/v1/computers/{id}/stream-token`** Returns a time-limited auth token for desktop and terminal WebSocket connections. ### Response - `200 OK` ```json { "token": "stream_auth_token", "expires_at": 1712700060 } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/stream-token \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Get Computer URLs **`GET /api/v1/computers/{id}/urls`** Returns all connection URLs for the computer. ### Response - `200 OK` ```json { "desktop_url": "https://my-computer.computer.miosa.ai/desktop/index.html", "stream_url": "https://my-computer.computer.miosa.ai/desktop/index.html", "terminal_url": "wss://my-computer.computer.miosa.ai/ws/terminal/550e8400-e29b-41d4-a716-446655440000", "computer_id": "550e8400-e29b-41d4-a716-446655440000" } ``` ```bash curl https://api.miosa.ai/api/v1/computers/{id}/urls \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## List Installed Apps **`GET /api/v1/computers/{id}/apps`** Returns the list of applications installed on the computer. ### Response - `200 OK` Response containing the list of installed applications. ```bash curl https://api.miosa.ai/api/v1/computers/{id}/apps \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Clone a Computer **`POST /api/v1/computers/{id}/clone`** Creates a new computer from the current state of an existing one. The source computer must be running; the clone starts in `provisioning` and progresses to `running`. ### Response - `201 Created` Full computer object for the new clone with its own ID. ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/clone \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Resize a Computer **`POST /api/v1/computers/{id}/resize`** Changes the vCPU and memory allocation. The computer must be stopped first. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `size` | string | Yes | `"small"` (4 GB/1 CPU), `"medium"` (8 GB/2 CPU), `"large"` (16 GB/4 CPU) | ### Response - `200 OK` Updated computer object with new `size`, `vcpus`, and `memory_mb`. ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/resize \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"size": "medium"}' ``` --- ## Move a Computer **`POST /api/v1/computers/{id}/move`** Migrates a computer to a different region. The computer must be stopped. Live migration is not supported; the computer will be moved offline. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `region` | string | Yes | Target region slug (e.g. `"us-east-ny"`) | ### Response - `200 OK` Updated computer object with new `region`. ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/move \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"region": "us-east-ny"}' ``` --- ## Get Metrics **`GET /api/v1/computers/{id}/metrics`** Returns time-series CPU, RAM, and credit-burn metrics. Useful for dashboards and auto-stop logic. ### Query Parameters | Parameter | Type | Description | |-----------|------|-------------| | `window` | string | Time window: `"1h"` (default), `"24h"`, `"7d"` | ### Response - `200 OK` ```json { "cpu_percent": [ {"ts": "2026-05-17T10:00:00Z", "value": 12.4}, {"ts": "2026-05-17T10:01:00Z", "value": 8.1} ], "memory_percent": [ {"ts": "2026-05-17T10:00:00Z", "value": 45.2} ], "credits_per_hour": 3 } ``` ```bash curl "https://api.miosa.ai/api/v1/computers/{id}/metrics?window=1h" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Env Vars Encrypted per-computer environment variables. Values are decrypted at boot and injected into the VM environment. ### List **`GET /api/v1/computers/{id}/env`** ```bash curl https://api.miosa.ai/api/v1/computers/{id}/env \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` ### Create **`POST /api/v1/computers/{id}/env`** | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | Variable name (e.g. `DATABASE_URL`) | | `value` | string | Yes | Plaintext value - encrypted at rest | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/env \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "DATABASE_URL", "value": "postgres://user:pass@host/db"}' ``` ### Update **`PATCH /api/v1/computers/{id}/env/{name}`** | Field | Type | Required | Description | |-------|------|----------|-------------| | `value` | string | Yes | New plaintext value | ### Delete **`DELETE /api/v1/computers/{id}/env/{name}`** Returns `200 OK`. --- ## Port Exposure Control per-port visibility for services running inside the computer. ### List Ports **`GET /api/v1/computers/{id}/ports`** ### Expose a Port **`POST /api/v1/computers/{id}/ports`** | Field | Type | Required | Description | |-------|------|----------|-------------| | `port` | integer | Yes | Port number (1-65535) | | `visibility` | string | Yes | `"public"`, `"private"`, or `"protected"` | ### Update Port Visibility **`PATCH /api/v1/computers/{id}/ports/{port}`** ### Remove Port **`DELETE /api/v1/computers/{id}/ports/{port}`** ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/ports \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"port": 3000, "visibility": "public"}' ``` --- ## Common Errors | Status | Code | Cause | |--------|------|-------| | 400 | `invalid computer id` | Path parameter is not a valid UUID | | 402 | `INSUFFICIENT_CREDITS` | Not enough credits to start or create | | 403 | `not a member of this computer` | No access to this computer | | 404 | `computer not found` | Does not exist or belongs to a different tenant | | 409 | `COMPUTER_NOT_RUNNING` | Operation requires a running computer | | 502 | `AGENT_UNAVAILABLE` | Computer command service unavailable | --- ## See also - [Desktop API](/docs/api-reference/desktop/) - screenshot, click, type, scroll, drag, windows - [Files API](/docs/api-reference/files/) - read, write, stat, mkdir, rename, copy, chmod, download - [Exec API](/docs/api-reference/exec/) - bash and Python execution - [Snapshots API](/docs/api-reference/snapshots/) - create, restore, and delete checkpoints - [Events (SSE)](/docs/api-reference/events/) - computer lifecycle event stream - [Regions](/docs/api-reference/regions/) - available regions for computer placement --- # Credits API URL: https://miosa.ai/docs/api-reference/credits Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/credits Source: src/routes/docs/api-reference/credits/+page.md Description: API reference for checking credit balance, transaction history, and usage data. MIOSA uses a credit-based billing system. Credits are consumed for compute time and AI API calls. Base path: `/api/v1/credits` --- ## Get Balance **`GET /api/v1/credits/balance`** Returns the current credit balance for your tenant. ### Response - `200 OK` ```json { "tenant_id": "550e8400-e29b-41d4-a716-446655440000", "balance_credits": 850, "lifetime_earned": 1000, "lifetime_spent": 150, "credit_expiry_at": "2026-10-08T00:00:00Z", "updated_at": "2026-04-11T15:00:00Z" } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | `tenant_id` | UUID | Your tenant ID | | `balance_credits` | integer | Current available credits | | `lifetime_earned` | integer | Total credits ever earned (purchases + promos) | | `lifetime_spent` | integer | Total credits ever spent | | `credit_expiry_at` | ISO 8601 | When current credits expire (180 days from purchase) | | `updated_at` | ISO 8601 | Last balance change | If no credit balance record exists yet, returns zeros: ```json { "tenant_id": "...", "balance_credits": 0, "lifetime_earned": 0, "lifetime_spent": 0, "credit_expiry_at": null, "updated_at": null } ``` ```bash curl https://api.miosa.ai/api/v1/credits/balance \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Get Transactions **`GET /api/v1/credits/transactions`** Returns paginated credit transaction history. ### Query Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `page` | integer | 1 | Page number (1-based) | | `page_size` | integer | 20 | Items per page (max: 100) | ### Response - `200 OK` ```json { "transactions": [ { "id": "uuid", "tenant_id": "uuid", "user_id": "uuid", "computer_id": "uuid", "type": "spend", "amount_credits": -5, "description": "LLM API call", "provider": "ollama", "model": "nemotron-3-super", "input_tokens": 1500, "output_tokens": 500, "cost_usd_micros": 250, "inserted_at": "2026-04-11T15:00:00Z" }, { "id": "uuid", "tenant_id": "uuid", "user_id": "uuid", "computer_id": null, "type": "earn", "amount_credits": 1000, "description": "Starter plan monthly credits", "provider": null, "model": null, "input_tokens": null, "output_tokens": null, "cost_usd_micros": null, "inserted_at": "2026-04-01T00:00:00Z" } ], "total": 47, "page": 1, "page_size": 20 } ``` ### Transaction Fields | Field | Type | Description | |-------|------|-------------| | `id` | UUID | Transaction ID | | `tenant_id` | UUID | Tenant ID | | `user_id` | UUID | User who triggered the transaction | | `computer_id` | UUID | Associated computer (if applicable) | | `type` | string | `"earn"` or `"spend"` | | `amount_credits` | integer | Credits (positive = earned, negative = spent) | | `description` | string | Human-readable description | | `provider` | string | LLM provider (for AI transactions) | | `model` | string | Model name (for AI transactions) | | `input_tokens` | integer | Input tokens used (for AI transactions) | | `output_tokens` | integer | Output tokens generated (for AI transactions) | | `cost_usd_micros` | integer | Cost in microdollars (1/1,000,000 USD) | | `inserted_at` | ISO 8601 | Transaction timestamp | ```bash curl "https://api.miosa.ai/api/v1/credits/transactions?page=1&page_size=50" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Get Usage Summary **`GET /api/v1/credits/usage`** Returns daily usage rollups grouped by provider and model. ### Query Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `from` | ISO 8601 | 30 days ago | Start of date range | | `to` | ISO 8601 | Now | End of date range | ### Response - `200 OK` ```json { "usage": [ { "day": "2026-04-11T00:00:00Z", "provider": "ollama", "model": "nemotron-3-super", "credits_spent": 45, "input_tokens": 15000, "output_tokens": 5000, "cost_usd_micros": 2250, "call_count": 12 }, { "day": "2026-04-10T00:00:00Z", "provider": "ollama", "model": "nemotron-3-super", "credits_spent": 30, "input_tokens": 10000, "output_tokens": 3000, "cost_usd_micros": 1500, "call_count": 8 } ], "from": "2026-03-12T00:00:00Z", "to": "2026-04-11T15:00:00Z" } ``` ### Usage Entry Fields | Field | Type | Description | |-------|------|-------------| | `day` | ISO 8601 | Date (truncated to day) | | `provider` | string | LLM provider | | `model` | string | Model name | | `credits_spent` | integer | Total credits spent that day | | `input_tokens` | integer | Total input tokens | | `output_tokens` | integer | Total output tokens | | `cost_usd_micros` | integer | Total cost in microdollars | | `call_count` | integer | Number of API calls | ### Errors | Status | Error | Cause | |--------|-------|-------| | 400 | `invalid 'from' timestamp` | Not valid RFC 3339 | | 400 | `invalid 'to' timestamp` | Not valid RFC 3339 | | 400 | `'from' must be before 'to'` | Invalid date range | ```bash curl "https://api.miosa.ai/api/v1/credits/usage?from=2026-04-01T00:00:00Z&to=2026-04-11T23:59:59Z" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Credit Pricing ### Compute Credits | Size | Credits/hour | |------|-------------| | Small (4GB/1CPU) | 3 | | Medium (8GB/2CPU) | 6 | | Large (16GB/4CPU) | 12 | ### AI Credits (per 1M tokens) | Tier | Input | Output | |------|-------|--------| | Standard models | 10 | 20 | | Advanced models | 60 | 100 | ### Plans | Plan | Price/month | Credits | |------|------------|---------| | Free | $0 | 100 | | Starter | $29 | 1,000 | | Pro | $79 | 3,000 | | Scale | $199 | 10,000 | Credits expire **180 days** from purchase. Unused plan credits do not roll over. --- ## See also - [API Reference overview](/docs/api-reference/) - base URL and authentication - [Error Codes](/docs/api-reference/errors/) - `INSUFFICIENT_CREDITS` error handling - [Workspaces](/docs/api-reference/workspaces/) - workspace and tenant management --- # Custom Domains API URL: https://miosa.ai/docs/api-reference/custom-domains Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/custom-domains Source: src/routes/docs/api-reference/custom-domains/+page.md Description: API reference for mapping tenant-owned FQDNs to MIOSA previews with automatic TLS. Custom domains let you expose MIOSA previews, computers, and deployments under tenant-owned FQDNs. There are four related surfaces: - **Computer custom domains** map one fully qualified domain, such as `app.yourdomain.com`, to one Computer service. - **Deployment custom domains** map one fully qualified domain, such as `program.drsmithclinic.com`, to one Deployment. - **Tenant preview domains** white-label generated preview links for the whole organization. After configuration, sandbox `/expose` responses use URLs such as `https://5173-sbx01j9x.sandbox.cliniciq.dev`. - **Workspace and project preview domains** override the organization preview domain for one client workspace or one project. MIOSA handles verification and automatic TLS certificate issuance via Caddy on-demand TLS. Nested base paths: `/api/v1/computers/{id}/domains` and `/api/v1/deployments/{id}/domains`. Flat frontend-compatible base path: `/api/v1/custom-domains`. Custom domains must not end in `miosa.ai` - that namespace is reserved for platform subdomains. Use a domain you control. --- ## Quick Start ```typescript const client = new Miosa(); // 1. Register the domain const domain = await client.domains.register(computerId, { domain: 'app.yourcompany.com', }); console.log(domain.verificationTarget); // e.g. "verify.miosa.ai" // 2. Add a CNAME record at your DNS provider: // app.yourcompany.com → verify.miosa.ai // 3. Trigger verification const verified = await client.domains.verify(computerId, domain.id); console.log(verified.status); // "verified" or "failed" ``` ```bash # Register curl -X POST https://api.miosa.ai/api/v1/computers/{id}/domains \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"domain": "app.yourcompany.com"}' ``` --- ## Endpoints ### Computer custom domains | Method | Path | Description | |--------|------|-------------| | `POST` | `/computers/{id}/domains` | Register a custom domain | | `GET` | `/computers/{id}/domains` | List domains for a computer | | `POST` | `/computers/{id}/domains/{domain_id}/verify` | Trigger DNS verification | | `DELETE` | `/computers/{id}/domains/{domain_id}` | Remove a domain | ### Deployment custom domains | Method | Path | Description | |--------|------|-------------| | `POST` | `/deployments/{id}/domains` | Register a custom domain | | `GET` | `/deployments/{id}/domains` | List domains for a deployment | ### Flat custom domains | Method | Path | Description | |--------|------|-------------| | `GET` | `/custom-domains?computer_id={id}` | List domains for a computer | | `GET` | `/custom-domains?deployment_id={id}` | List domains for a deployment | | `POST` | `/custom-domains` | Register a domain with `computer_id` or `deployment_id` | | `DELETE` | `/custom-domains/{id}` | Remove a domain | ### Tenant preview domains | Method | Path | Description | |--------|------|-------------| | `GET` | `/tenant/preview-domain` | Read the tenant preview domain | | `PUT` | `/tenant/preview-domain` | Set the tenant preview domain | | `DELETE` | `/tenant/preview-domain` | Clear the tenant preview domain | | `GET` | `/tenant/preview-domain/verify` | Check DNS/TLS readiness | ### Workspace and project preview domains | Method | Path | Description | |--------|------|-------------| | `GET` | `/workspaces/{id}/preview-domain` | Read the workspace preview domain | | `PUT` | `/workspaces/{id}/preview-domain` | Set the workspace preview domain | | `DELETE` | `/workspaces/{id}/preview-domain` | Clear the workspace preview domain | | `GET` | `/workspaces/{id}/preview-domain/verify` | Check workspace DNS readiness | | `GET` | `/projects/{id}/preview-domain` | Read the project preview domain | | `PUT` | `/projects/{id}/preview-domain` | Set the project preview domain | | `DELETE` | `/projects/{id}/preview-domain` | Clear the project preview domain | | `GET` | `/projects/{id}/preview-domain/verify` | Check project DNS readiness | --- ## White-label Managed Links Use a tenant preview domain when generated app/artifact preview URLs should use your organization's domain instead of the platform default `miosa.app`. ```bash curl -X PUT https://api.miosa.ai/api/v1/tenant/preview-domain \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"preview_domain":"preview.yourcompany.com"}' ``` Then expose a sandbox port: ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/{id}/expose \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"port":5173}' ``` Response: ```json { "url": "https://5173-sbx01j9x.sandbox.preview.yourcompany.com" } ``` For a client workspace domain: ```bash curl -X PUT https://api.miosa.ai/api/v1/workspaces/$WORKSPACE_ID/preview-domain \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"preview_domain":"drsmithclinic.com"}' ``` For a single project domain: ```bash curl -X PUT https://api.miosa.ai/api/v1/projects/$PROJECT_ID/preview-domain \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"preview_domain":"program.drsmithclinic.com"}' ``` DNS must delegate wildcard traffic to the MIOSA preview router: | Record type | Name | Value | Used for | |---|---|---|---| | `CNAME` | `*` | `proxy.miosa.ai` | `https://{slug}.{domain}` managed deployment/computer/default preview URLs | | `CNAME` | `*.sandbox` | `proxy.miosa.ai` | `https://{port}-{slug}.sandbox.{domain}` sandbox port previews | If no preview domain is configured, MIOSA returns the managed `miosa.app` fallback. This fallback remains available even after custom domains are attached. Preview-domain precedence is project → workspace → tenant → MIOSA fallback. Exact deployment/computer custom domains still take priority over preview-domain inheritance. --- ## Register a Domain **`POST /api/v1/computers/{id}/domains`** ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `domain` | string | Yes | Fully qualified domain name. RFC 1123 hostname, max 253 chars | ### Response - `201 Created` ```json { "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "computer_id": "...", "deployment_id": null, "tenant_id": "...", "workspace_id": "660e8400-e29b-41d4-a716-446655440001", "project_id": "770e8400-e29b-41d4-a716-446655440002", "fqdn": "app.yourcompany.com", "status": "pending", "verification_target": "verify.miosa.ai", "verified_at": null, "tls_issued_at": null, "external_workspace_id": "clinic_123", "external_user_id": "dr-smith-456", "external_project_id": "project_789", "created_at": "2026-04-11T00:00:00Z", "updated_at": "2026-04-11T00:00:00Z" } } ``` ### Status Values | Status | Description | |--------|-------------| | `pending` | Registered; awaiting DNS verification | | `verified` | CNAME confirmed; Caddy may issue a cert | | `active` | TLS certificate issued and in use | | `failed` | Verification or TLS issuance failed | | `removed` | Domain detached (soft marker before deletion) | ### Errors | Status | Error | Cause | |--------|-------|-------| | 409 | `has already been taken` | FQDN already registered (globally unique) | | 422 | Validation error | Invalid FQDN format or ends with `miosa.ai` | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/domains \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"domain": "preview.yourapp.io"}' ``` ### Register a deployment domain ```bash curl -X POST https://api.miosa.ai/api/v1/deployments/{id}/domains \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"domain": "program.drsmithclinic.com"}' ``` Deployment domains inherit `workspace_id`, `project_id`, and external attribution from the deployment. --- ## List Domains **`GET /api/v1/computers/{id}/domains`** ### Response - `200 OK` ```json { "data": [ { "id": "...", "fqdn": "app.yourcompany.com", "status": "active", "verified_at": "2026-04-11T01:00:00Z", "tls_issued_at": "2026-04-11T01:05:00Z", "created_at": "2026-04-11T00:00:00Z" } ], "total": 1 } ``` ```bash curl https://api.miosa.ai/api/v1/computers/{id}/domains \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Verify a Domain **`POST /api/v1/computers/{id}/domains/{domain_id}/verify`** Checks that the CNAME record at `fqdn` resolves to `verification_target`. On success the domain transitions to `"verified"` and Caddy will issue a TLS certificate on the next HTTPS request to the FQDN. ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | `id` | UUID | Computer ID | | `domain_id` | UUID | Domain ID | ### Response - `200 OK` ```json { "data": { "id": "...", "fqdn": "app.yourcompany.com", "status": "verified", "verified_at": "2026-04-11T01:00:00Z" } } ``` ### Errors | Status | Error | Cause | |--------|-------|-------| | 409 | `domain is already verified` | Already completed | | 422 | `CNAME not found` | DNS record missing or not yet propagated | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/domains/{domain_id}/verify \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Remove a Domain **`DELETE /api/v1/computers/{id}/domains/{domain_id}`** Detaches the domain from the computer. The certificate is not revoked immediately (Caddy lets it expire naturally), but the domain will no longer route to the computer. ### Response - `200 OK` ```json { "data": { "id": "...", "fqdn": "app.yourcompany.com", "status": "removed" } } ``` ```bash curl -X DELETE https://api.miosa.ai/api/v1/computers/{id}/domains/{domain_id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## DNS Setup Reference After registering a domain, add the following DNS record at your registrar or DNS provider: | Record | Host | Value | |--------|------|-------| | CNAME | `app` (or full subdomain) | `verify.miosa.ai` | DNS propagation typically takes 1-60 minutes. Call `verify` once propagation is complete. If verification fails, wait a few minutes and retry - the endpoint is idempotent. --- ## Common Recipes ### Automate full domain onboarding ```python from miosa import Miosa client = Miosa() domain = client.domains.register(computer_id, fqdn="api.yourcompany.com") print(f"Add CNAME: {domain.fqdn} → {domain.verification_target}") print("Waiting for DNS propagation...") while True: result = client.domains.verify(computer_id, domain.id) if result.status == "verified": print("Domain verified! TLS will be issued on first HTTPS request.") break elif result.status == "failed": raise RuntimeError("Verification failed - check your DNS records.") time.sleep(30) ``` ### List all pending domains (potential stuck ACME issuances) ```typescript const computers = await client.computers.list(); for (const computer of computers.data) { const { data: domains } = await client.domains.list(computer.id); const pending = domains.filter(d => d.status === 'pending'); if (pending.length > 0) { console.log(`${computer.name}: ${pending.length} domain(s) pending verification`); } } ``` --- # API Reference / Deployments URL: https://miosa.ai/docs/api-reference/deployments Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/deployments Source: src/routes/docs/api-reference/deployments/+page.md Description: REST API for the Deployment resource - create, list, get, update, delete, publish, rollback. A **Deployment** is the stable production object for a published app/site/API. See [Deploy / Overview](/docs/deploy/overview/) for the conceptual model. Sandbox-sourced create + publish + rollback are part of Phase 2B / 3 of the deployment refactor. Today, the backward-compatible `POST /api/v1/sandboxes/:id/deploy` route exists and uses the sandbox-backed bridge. Endpoints below show the steady-state shape; in-progress endpoints are marked. ## Endpoints ```http POST /api/v1/projects/:project_id/deployments # Phase 2B GET /api/v1/deployments GET /api/v1/deployments/:id PATCH /api/v1/deployments/:id DELETE /api/v1/deployments/:id POST /api/v1/deployments/:id/publish # Phase 2B POST /api/v1/deployments/:id/rollback # Phase 2B POST /api/v1/sandboxes/:id/deploy # Backward-compat ``` All endpoints require `Authorization: Bearer <msk_*>`. Mutations should include an `Idempotency-Key` header. ## Create ```http POST /api/v1/projects/:project_id/deployments ``` Body: ```json { "name": "Smile Dental Landing", "source_type": "sandbox", "workspace_slug": "dr-smith-clinic", "workspace_name": "Dr. Smith Clinic", "project_slug": "smile-dental-landing", "project_name": "Smile Dental Landing", "external_workspace_id": "clinic_123", "external_user_id": "dr-smith-456", "external_project_id": "project_789", "metadata": { } } ``` Ownership fields accepted on create: `workspace_id`, `workspace_slug`, `workspace_name`, `project_id`, `project_slug`, `project_name`, `external_workspace_id`, `external_user_id`, and `external_project_id`. Scopes: `deployments:write`. ## Publish ```http POST /api/v1/deployments/:id/publish ``` Body: ```json { "source_sandbox_id": "sbx_...", "kind": "auto", "environment": "production", "output_path": "/workspace", "build_command": "npm run build", "run_command": "npm start", "port": 3000, "health_check_path": "/_health", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "project_id": "660e8400-e29b-41d4-a716-446655440001", "external_workspace_id": "clinic_123" } ``` Server-side behavior is documented in [Publishing](/docs/deploy/publishing/). Response: ```json { "data": { "deployment": { "id": "dep_...", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "project_id": "660e8400-e29b-41d4-a716-446655440001", "active_version_id": "ver_...", "state": "running", "public_url": "https://smile-dental.cliniciq.miosa.app" }, "version": { "id": "ver_...", "kind": "static", "state": "ready", "artifact_uri": "s3://...", "artifact_sha256": "...", "source_sha256": "..." }, "services": [ { "id": "svc_...", "type": "static_web", "state": "healthy" } ], "promoted": true } } ``` ## List ```http GET /api/v1/deployments GET /api/v1/deployments?workspace_id=550e8400-e29b-41d4-a716-446655440000 GET /api/v1/deployments?project_id=660e8400-e29b-41d4-a716-446655440001 GET /api/v1/deployments?external_workspace_id=clinic_123 GET /api/v1/deployments?external_user_id=dr-smith-456 GET /api/v1/deployments?state=running ``` All filters are organization-scoped server-side. See [Ownership and Attribution](/docs/platform/attribution/). ## Get ```http GET /api/v1/deployments/:id ``` Returns the full deployment row including `workspace_id`, `project_id`, `active_version_id`, attribution, and metadata. ## Update ```http PATCH /api/v1/deployments/:id ``` Body: any subset of mutable fields (`name`, `auto_deploy`, `metadata`, `external_*`). ## Rollback ```http POST /api/v1/deployments/:id/rollback ``` Body: ```json { "version_id": "ver_..." } ``` See [Rollback](/docs/deploy/rollback/). ## Delete ```http DELETE /api/v1/deployments/:id ``` Tears down runtime instances, removes routes, marks the deployment deleted. Versions and releases are retained per the retention policy. ## See also - [Versions](/docs/api-reference/versions/) - version sub-resource - [Releases](/docs/api-reference/releases/) - build artifact reference (Phase 2B) - [Custom Domains](/docs/api-reference/custom-domains/) - domain sub-resource - [Deploy / Overview](/docs/deploy/overview/) - conceptual model --- # Desktop API URL: https://miosa.ai/docs/api-reference/desktop Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/desktop Source: src/routes/docs/api-reference/desktop/+page.md Description: API reference for all desktop control endpoints - screenshot, mouse, keyboard, clipboard, windows, accessibility tree. Desktop control endpoints proxy commands to the computer command service inside the computer VM via the MIOSA control plane. All coordinates are in screen pixels (0,0 = top-left). Base path: `/api/v1/computers/{id}/desktop` All desktop endpoints require the computer to be in `"running"` status. Returns `409 COMPUTER_NOT_RUNNING` if the computer is stopped or provisioning. Rate limit: 300 req/min per workspace. Individual desktop actions are fast but high-frequency AI agents should batch operations where possible. --- ## Screenshot **`GET /api/v1/computers/{id}/desktop/screenshot`** Captures the full desktop as a PNG image. ### Response - `200 OK` Binary PNG data with `Content-Type: image/png`. ```bash curl https://api.miosa.ai/api/v1/computers/{id}/desktop/screenshot \ -H "Authorization: Bearer $MIOSA_API_KEY" \ --output screenshot.png ``` --- ## Screenshot Region **`POST /api/v1/computers/{id}/desktop/screenshot/region`** Captures a rectangular region of the screen. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `x` | integer | Yes | Left edge X coordinate | | `y` | integer | Yes | Top edge Y coordinate | | `width` | integer | Yes | Width in pixels | | `height` | integer | Yes | Height in pixels | ### Response - `200 OK` Binary PNG data. ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/screenshot/region \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"x": 0, "y": 0, "width": 800, "height": 600}' \ --output region.png ``` --- ## Click **`POST /api/v1/computers/{id}/desktop/click`** Performs a mouse click at the specified coordinates. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `x` | integer | Yes | X coordinate | | `y` | integer | Yes | Y coordinate | | `button` | string | No | `"left"` (default), `"right"`, `"middle"` | ### Response - `200 OK` ```json { "success": true } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/click \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"x": 500, "y": 300}' ``` --- ## Double-Click **`POST /api/v1/computers/{id}/desktop/double-click`** Performs a double-click at the specified coordinates. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `x` | integer | Yes | X coordinate | | `y` | integer | Yes | Y coordinate | ### Response - `200 OK` ```json { "success": true } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/double-click \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"x": 500, "y": 300}' ``` --- ## Type Text **`POST /api/v1/computers/{id}/desktop/type`** Types a string of text as keyboard input. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `text` | string | Yes | Text to type | ### Response - `200 OK` ```json { "success": true } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/type \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "Hello, World!"}' ``` --- ## Key Press **`POST /api/v1/computers/{id}/desktop/key`** Presses a key or key combination. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `key` | string | Yes | Key name or combination | Key combinations use `+` as separator: `"ctrl+c"`, `"alt+Tab"`, `"ctrl+shift+t"`. ### Common Keys | Key | Value | |-----|-------| | Enter | `Return` | | Tab | `Tab` | | Escape | `Escape` | | Backspace | `BackSpace` | | Delete | `Delete` | | Space | `space` | | Arrows | `Up`, `Down`, `Left`, `Right` | | Function | `F1`...`F12` | | Home/End | `Home`, `End` | | Page Up/Down | `Page_Up`, `Page_Down` | ### Response - `200 OK` ```json { "success": true } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/key \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"key": "ctrl+c"}' ``` --- ## Scroll **`POST /api/v1/computers/{id}/desktop/scroll`** Scrolls the mouse wheel at the specified position. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `x` | integer | Yes | X coordinate | | `y` | integer | Yes | Y coordinate | | `direction` | string | Yes | `"up"` or `"down"` | | `amount` | integer | No | Number of scroll steps (default: 3) | ### Response - `200 OK` ```json { "success": true } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/scroll \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"x": 500, "y": 300, "direction": "down", "amount": 5}' ``` --- ## Drag **`POST /api/v1/computers/{id}/desktop/drag`** Performs a click-and-drag from one point to another. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `start_x` | integer | Yes | Starting X coordinate | | `start_y` | integer | Yes | Starting Y coordinate | | `end_x` | integer | Yes | Ending X coordinate | | `end_y` | integer | Yes | Ending Y coordinate | ### Response - `200 OK` ```json { "success": true } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/drag \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"start_x": 100, "start_y": 200, "end_x": 400, "end_y": 500}' ``` --- ## Wait **`POST /api/v1/computers/{id}/desktop/wait`** Server-side pause. Holds the connection for the specified duration before responding. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `seconds` | number | No | Seconds to wait (default: 1, max: 30) | ### Response - `200 OK` ```json { "success": true, "waited_seconds": 2 } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/wait \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"seconds": 2}' ``` --- ## List Windows **`GET /api/v1/computers/{id}/desktop/windows`** Returns a list of open windows on the desktop. ### Response - `200 OK` ```json { "windows": [ { "id": "0x2200003", "title": "Terminal - user@computer", "class": "Xfce4-terminal", "x": 100, "y": 50, "width": 800, "height": 600 } ] } ``` ```bash curl https://api.miosa.ai/api/v1/computers/{id}/desktop/windows \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Focus Window **`POST /api/v1/computers/{id}/desktop/window/focus`** Brings a window to the foreground. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `window_id` | string | Yes | Window ID from the list windows response | ### Response - `200 OK` ```json { "success": true } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/window/focus \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"window_id": "0x2200003"}' ``` --- ## Launch Application **`POST /api/v1/computers/{id}/desktop/launch`** Launches an application by name or command. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `app` | string | Yes | Application name or command (e.g., `"firefox"`, `"xfce4-terminal"`) | ### Response - `200 OK` ```json { "success": true } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/launch \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"app": "firefox"}' ``` --- ## Cursor Position **`GET /api/v1/computers/{id}/desktop/cursor`** Returns the current mouse cursor coordinates. ### Response - `200 OK` ```json { "x": 500, "y": 300 } ``` ```bash curl https://api.miosa.ai/api/v1/computers/{id}/desktop/cursor \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Hotkey **`POST /api/v1/computers/{id}/desktop/hotkey`** Presses a hotkey combination. Functionally equivalent to `key` but uses a dedicated endpoint for named combinations. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `keys` | string[] | Yes | Ordered list of keys to press simultaneously (e.g. `["ctrl", "shift", "t"]`) | ### Response - `200 OK` ```json {"success": true} ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/hotkey \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"keys": ["ctrl", "shift", "t"]}' ``` --- ## Key Down / Key Up **`POST /api/v1/computers/{id}/desktop/key-down`** **`POST /api/v1/computers/{id}/desktop/key-up`** Send individual key-down or key-up events. Use these for precise control when holding a modifier key while performing another action. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `key` | string | Yes | Key name (same format as `key` endpoint) | ### Response - `200 OK` ```json {"success": true} ``` --- ## Mouse Down / Mouse Up **`POST /api/v1/computers/{id}/desktop/mouse-down`** **`POST /api/v1/computers/{id}/desktop/mouse-up`** Send individual mouse-button-down or mouse-button-up events. Use for custom drag sequences or long-press interactions. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `x` | integer | Yes | X coordinate | | `y` | integer | Yes | Y coordinate | | `button` | string | No | `"left"` (default), `"right"`, `"middle"` | ### Response - `200 OK` ```json {"success": true} ``` --- ## Move Mouse **`POST /api/v1/computers/{id}/desktop/move`** Moves the mouse cursor to the specified position without clicking. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `x` | integer | Yes | Target X coordinate | | `y` | integer | Yes | Target Y coordinate | ### Response - `200 OK` ```json {"success": true} ``` --- ## Clipboard ### Get Clipboard **`GET /api/v1/computers/{id}/desktop/clipboard`** Returns the current clipboard contents. ### Response - `200 OK` ```json {"text": "Hello, World!"} ``` ### Set Clipboard **`POST /api/v1/computers/{id}/desktop/clipboard`** Sets the clipboard contents. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `text` | string | Yes | Text to place in the clipboard | ### Response - `200 OK` ```json {"success": true} ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/desktop/clipboard \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "Hello, World!"}' ``` --- ## Screen Size **`GET /api/v1/computers/{id}/desktop/screen-size`** Returns the current desktop resolution. ### Response - `200 OK` ```json {"width": 1280, "height": 720} ``` --- ## Environment **`GET /api/v1/computers/{id}/desktop/environment`** Returns desktop session metadata - display server, session type, active user, and desktop environment name. --- ## Set Wallpaper **`POST /api/v1/computers/{id}/desktop/wallpaper`** Sets the desktop wallpaper. Supports per-tenant white-label branding. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `url` | string | Yes | Publicly accessible URL of the image to set as wallpaper | --- ## Accessibility Tree **`GET /api/v1/computers/{id}/desktop/accessibility-tree`** Returns the AT-SPI accessibility tree for the current desktop state. The tree describes all visible UI elements with their roles, labels, states, and bounding boxes. Use this for element discovery when exact coordinates are unknown. ### Response - `200 OK` ```json { "tree": { "role": "application", "name": "Firefox", "children": [ { "role": "button", "name": "Close", "x": 1260, "y": 10, "width": 20, "height": 20 } ] } } ``` --- ## Window Management ### List Windows See [List Windows](#list-windows) above. ### Window Size **`GET /api/v1/computers/{id}/desktop/window/{window_id}/size`** Returns the width and height of a specific window. ### Window Position **`GET /api/v1/computers/{id}/desktop/window/{window_id}/position`** Returns the x,y position of a specific window. ### Resize Window **`POST /api/v1/computers/{id}/desktop/window/{window_id}/resize`** | Field | Type | Required | Description | |-------|------|----------|-------------| | `width` | integer | Yes | New width in pixels | | `height` | integer | Yes | New height in pixels | ### Move Window **`POST /api/v1/computers/{id}/desktop/window/{window_id}/move`** | Field | Type | Required | Description | |-------|------|----------|-------------| | `x` | integer | Yes | New X position | | `y` | integer | Yes | New Y position | ### Maximize Window **`POST /api/v1/computers/{id}/desktop/window/{window_id}/maximize`** ### Minimize Window **`POST /api/v1/computers/{id}/desktop/window/{window_id}/minimize`** ### Close Window **`POST /api/v1/computers/{id}/desktop/window/{window_id}/close`** All window management endpoints return `{"success": true}` on `200 OK`. --- ## Common Errors All desktop endpoints share these error responses: | Status | Code | Description | |--------|------|-------------| | 400 | `invalid computer id` | UUID format invalid | | 403 | `FORBIDDEN` | Not a member of this computer | | 404 | `NOT_FOUND` | Computer does not exist | | 409 | `COMPUTER_NOT_RUNNING` | Computer is stopped or provisioning | | 502 | `AGENT_UNAVAILABLE` | Cannot reach the computer command service | --- ## See also - [Computers API](/docs/api-reference/computers/) - create, start, stop, clone computers - [Exec API](/docs/api-reference/exec/) - run bash/Python without desktop interaction - [Files API](/docs/api-reference/files/) - read and write files in the VM - [Error Codes](/docs/api-reference/errors/) - complete error code reference --- # Egress (Security) API URL: https://miosa.ai/docs/api-reference/egress Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/egress Source: src/routes/docs/api-reference/egress/+page.md Description: API reference for the MIOSA Egress credential vault, network allowlist, OAuth Connect, and audit log. The Egress API powers the **Security** tab on every Computer and Sandbox: encrypted credential vault, OAuth Connect, network allowlist with audit-only / enforce modes, and a live-tail audit log. Base path: `/api/v1/egress` All endpoints require `Authorization: Bearer <msk_*>`. See [Authentication](/docs/authentication). For higher-level access, use the [SDKs](/docs/sdks/egress). For the architecture overview, walkthroughs, and white-label patterns, see the [Security guides](/docs/security). --- ## Secrets ### Create a secret **`POST /api/v1/egress/secrets`** Stores an encrypted credential and optionally binds it to a resource as an environment variable. The raw value is encrypted with ChaCha20-Poly1305 and never returned again. ```bash curl -X POST https://api.miosa.ai/api/v1/egress/secrets \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "openai_key", "value": "sk-proj-...", "type": "api_key", "scope": "user", "expose_as_env": "OPENAI_API_KEY" }' ``` Required body fields: `name`, `value`, `type`, `scope`. | Field | Values | Notes | |---|---|---| | `type` | `api_key`, `oauth_connect`, `oauth_machine` | OAuth flows are usually initiated via `/oauth/start` instead. | | `scope` | `tenant`, `workspace`, `user`, `external_user`, `external_workspace` | The 5-tier scoping model. | | `expose_as_env` | shell-safe upper-snake-case | If set, auto-creates a binding. | Optional scoping fields based on `scope`: `workspace_id`, `owner_user_id`, `external_user_id`, `external_workspace_id`. Returns `201 Created` with `{data: , binding: }`. The `data.preview` is the first-6 + last-4 of the value; the full value is never returned. ### List secrets **`GET /api/v1/egress/secrets`** Query params: `scope`, `workspace_id`, `owner_user_id`, `external_user_id`, `external_workspace_id`. Returns metadata only (no values). ### Get a secret **`GET /api/v1/egress/secrets/:id`** - metadata for one secret. ### Rotate **`PATCH /api/v1/egress/secrets/:id`** Re-encrypts with a new value. The bound `placeholder_token` does not change; every sandbox using it picks up the new value on its next outbound request. ```json { "value": "sk-proj-new-value", "expires_at": "2026-12-31T00:00:00Z" } ``` ### Delete **`DELETE /api/v1/egress/secrets/:id`** --- ## Bindings A binding links a secret to a Computer or Sandbox and surfaces it as an environment variable (or opaque header token). ### Create a binding **`POST /api/v1/egress/bindings`** ```json { "secret_id": "", "resource_id": "", "resource_type": "sandbox", "exposure": "env_var", "expose_as_env": "OPENAI_API_KEY" } ``` Returns the binding including its `placeholder_token` - the opaque `miosa-tok-…` value the workload sees in env. ### List + delete - `GET /api/v1/egress/bindings?resource_id=&resource_type=sandbox` - `DELETE /api/v1/egress/bindings/:id` --- ## OAuth Connect ### List provider catalog **`GET /api/v1/egress/oauth/providers`** - returns providers visible to the calling tenant (platform defaults merged with tenant overrides). No client secrets exposed. ### Start an OAuth flow **`POST /api/v1/egress/oauth/start`** ```json { "provider": "github", "expose_as_env": "GITHUB_TOKEN", "scope": "user", "owner_user_id": "" } ``` Returns `{authorize_url, state}`. Open `authorize_url` in a browser; the user approves; the upstream redirects to `/api/v1/egress/oauth/callback?code=&state=`. On success the tokens are encrypted into a new `oauth_connect` secret + binding. ### Status **`GET /api/v1/egress/oauth/status?state=`** - poll while the user is completing the flow. Returns `pending`, `complete`, or `failed`. ### Admin: tenant-override providers (white-label) - `POST /api/v1/egress/oauth/admin/providers` - register your own OAuth app credentials so end-users see your brand on the consent screen. - `DELETE /api/v1/egress/oauth/admin/providers/:id` Requires admin (`msk_a_*`) credentials. --- ## Network policies + allowlist ### Policies - `GET /api/v1/egress/policies` - list (optionally filter by `resource_id`) - `POST /api/v1/egress/policies` - create - `GET /api/v1/egress/policies/:id` - `PATCH /api/v1/egress/policies/:id` - set `mode` (`audit_only` / `enforce`), `default_action` (`allow` / `deny`) Each Computer or Sandbox resolves the policy in this order: resource override → tenant default. Tenant default starts in `audit_only` mode. ### Allowlist rules - `GET /api/v1/egress/allowlist?policy_id=` - `POST /api/v1/egress/allowlist` - `DELETE /api/v1/egress/allowlist/:id` ```json { "policy_id": "", "pattern": "*.openai.com", "kind": "wildcard", "method": "POST", "path_glob": "/v1/*", "action": "allow", "warn_only": false, "priority": 100 } ``` Pattern kinds: `exact` (literal host), `wildcard` (`*.example.com`), `cidr` (IP literal targets only). --- ## Audit log ### Query **`GET /api/v1/egress/audit`** Query params: `resource_id`, `host`, `action` (`allow`/`reject`/`stub`/`error`), `since` (relative like `1h` / `7d` or ISO timestamp), `until`, `limit` (default 100, max 1000), `external_user_id`, `external_workspace_id`. Each row includes the full identity chain (`tenant_id`, `workspace_id`, `owner_user_id`, `external_user_id`, `external_workspace_id`), request metadata (`host`, `method`, `path`, `sni`, `mode`, `remote_addr`), outcome (`action`, `rejected_by`, `status_code`, `duration_ms`, `bytes_in`, `bytes_out`, `error_message`), and the transform trace JSONB (`request_transforms`, `response_transforms`, `mcp`). ### Get a single event **`GET /api/v1/egress/audit/:id`** ### Allowlist suggestions **`GET /api/v1/egress/audit/suggestions?resource_id=`** - returns the hosts a resource has contacted recently, ranked by frequency. The basis for the one-click "Lock down with selected as allowlist" UX. ### Live tail (WebSocket) **`wss://api.miosa.ai/api/v1/egress/audit/resource/:resource_id?token=`** Subprotocol: `miosa-egress-audit-v1`. Pushes `{"type":"audit_event","data":{...}}` frames in real time as outbound requests are recorded. Tenant-scoped variant (admin only): `wss://api.miosa.ai/api/v1/egress/audit/tenant/:tenant_id?token=`. --- ## Rate limits + retention | Scope | Limit | |---|---| | All `/egress/*` endpoints | 300 req/min per API key | | Audit retention | 90 days for every tenant; longer on enterprise | | Audit live-tail (WebSocket) | unlimited; does not count against REST quota | --- ## Errors Same envelope as the rest of the API - `{"error": "...", "details": ...}` with the appropriate status code. | Status | When | |---|---| | `400` | invalid body, unknown scope, malformed env var name | | `401` | missing / invalid credential | | `403` | role does not allow the operation (e.g., creating tenant-scoped secret as a non-admin) | | `404` | secret / binding / policy / event not found in this tenant | | `409` | secret name collision within scope (unique partial index) | | `422` | scope ↔ identity column mismatch (e.g., scope=user without owner_user_id) | --- ## SDK shortcut All endpoints above are wrapped by the [Egress SDK reference](/docs/sdks/egress) with idiomatic methods in Python, TypeScript, Go, Java, and Elixir. --- # Error Codes URL: https://miosa.ai/docs/api-reference/errors Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/errors Source: src/routes/docs/api-reference/errors/+page.md Description: Every error code the MIOSA API returns, what it means, and how to handle it. Every MIOSA API error follows a single envelope shape. The HTTP status indicates the broad class; the `code` field is the machine-readable reason. ```json { "error": { "code": "NOT_FOUND", "message": "sandbox not found", "request_id": "req_01jv..." } } ``` Include `request_id` whenever you contact support. It correlates the request across all internal services. --- ## Authentication errors Returned by the `ApiKeyAuth` plug before the request reaches any controller. | Code | HTTP | Meaning | What to do | |---|---|---|---| | `invalid_token` | 401 | `Authorization` header missing, malformed, or the key/JWT is invalid | Verify the header is `Authorization: Bearer msk_...` | | `invalid_token` | 401 | Refresh token used on a non-auth endpoint | Use a workspace API key (`msk_*`), not a refresh token | | `insufficient_scope` | 403 | Key exists but lacks the required scope for this operation | Adjust key scopes in the dashboard | | `rate_limit_exceeded` | 429 | Too many requests from this key or IP | Back off and retry after `Retry-After` seconds | All API endpoints are rate-limited. General API: 300 req/min per workspace. Auth endpoints: 20 req/min. Retry-After is included in 429 responses. --- ## General resource errors These codes appear across all resource types (computers, sandboxes, deployments, workspaces, etc.). | Code | HTTP | Meaning | What to do | |---|---|---|---| | `NOT_FOUND` | 404 | Resource does not exist or belongs to a different tenant | Confirm the ID and that the key's tenant owns the resource | | `FORBIDDEN` | 403 | Authenticated but not authorized to access this resource | Check membership or ownership | | `INVALID_ID` | 400 | Path parameter is not a valid UUID | Use a UUID v4 ID, not a slug or name | | `MISSING_PARAM` | 400 | A required request body field is absent | Check the field name in the request body schema | | `VALIDATION_ERROR` | 422 | One or more fields failed schema validation | Inspect `details` for per-field messages | | `VALIDATION_FAILED` | 422 | Changeset validation failed (alias of `VALIDATION_ERROR` in some controllers) | Inspect `details` | | `TENANT_RESOLUTION_FAILED` | 422 | Cannot determine the tenant for the authenticated identity | The API key may belong to a workspace that has been deleted or suspended | | `INTERNAL_ERROR` | 500 | Unexpected server-side error | Retry with exponential backoff; report if persistent | --- ## Sandbox errors | Code | HTTP | Meaning | What to do | |---|---|---|---| | `INSUFFICIENT_CREDITS` | 402 | Not enough credits to provision this sandbox | Top up credits in the dashboard or reduce the resource request | | `SANDBOX_LIMIT_REACHED` | 409 | Tenant has hit the concurrent sandbox cap (default 10) | Destroy idle sandboxes or contact support to raise the limit | | `SANDBOX_NOT_RUNNING` | 409 | Operation requires the sandbox to be in `running` state | Wait until state is `running`; poll `GET /sandboxes/{id}` or subscribe to events | | `AGENT_UNAVAILABLE` | 502 | The computer command service is reachable but not responding | Wait and retry; if persistent, destroy and recreate the sandbox | | `INVALID_TEMPLATE` | 400 | `template_id` is not a recognized template | Use `GET /api/v1/sandbox-templates` to list valid IDs | | `MISSING_PATH` | 400 | `path` field is required but absent | Include `path` in the request body | | `INVALID_PATH` | 400 | `path` is not a valid absolute filesystem path | Use an absolute path (starts with `/`) | | `MISSING_FILE` | 400 | Multipart upload missing the `file` part | Include the file part in the multipart form | | `INVALID_BODY` | 400 | Request body could not be parsed or decoded | Verify JSON or base64 encoding | | `MISSING_MODE` | 400 | `chmod` call missing `mode` field | Include an octal mode string, e.g. `"0644"` | | `INVALID_PORT` | 422 | Expose port is outside `1`-`65535` | Use a port in the valid range | | `MISSING_PARAM` | 400 | `port` omitted on `/expose` and template has no default preview port | Provide `port` explicitly | | `SANDBOX_NOT_RUNNING` | 409 | `/deploy` called but sandbox is not running | Confirm sandbox state before promoting | | `SANDBOX_RUNTIME_UNAVAILABLE` | 409 | Sandbox is marked running but has no VM IP yet | Retry after a few seconds | --- ## Sandbox template build errors | Code | HTTP | Meaning | What to do | |---|---|---|---| | `TEMPLATE_NOT_FOUND` | 404 | Template ID does not exist | Verify the template ID | | `BUILD_NOT_FOUND` | 404 | Build record does not exist | Verify the build ID | | `BUILD_ALREADY_TERMINAL` | 409 | Cannot cancel a build that is already `ready`, `failed`, or `cancelled` | No action needed; build is in a terminal state | | `BUILD_NOT_RETRYABLE` | 409 | Build is not in a state that allows retry (`failed` or `cancelled` only) | Only `failed` or `cancelled` builds may be retried | | `UNAUTHORIZED` | 401 | SSE ticket missing, invalid, or expired | Re-issue a ticket via `POST /auth/sse-ticket` and reconnect | --- ## Computer errors | Code | HTTP | Meaning | What to do | |---|---|---|---| | `INSUFFICIENT_CREDITS` | 402 | Not enough credits to start this computer | Top up credits | | `COMPUTER_NOT_RUNNING` | 409 | Desktop or exec operation requires a running computer | Start the computer first | | `COMPUTER_NOT_FOUND` | 404 | Computer does not exist in this tenant | Verify the computer ID | | `AGENT_UNAVAILABLE` | 502 | computer command service unreachable | Computer may be starting; retry after a few seconds | --- ## Deployment errors | Code | HTTP | Meaning | What to do | |---|---|---|---| | `DEPLOYMENT_NOT_FOUND` | 404 | Deployment ID does not exist | Verify the deployment ID | | `VALIDATION_ERROR` | 422 | Deployment fields failed validation | Check `details` for field-level messages | | `SANDBOX_NOT_RUNNING` | 409 | Publishing requires a running sandbox | Confirm sandbox state | | `RUNTIME_TARGET_UNAVAILABLE` | 409 | Sandbox has no IP yet - cannot route to it | Retry after a few seconds | | `RUNTIME_LOGS_UNAVAILABLE` | 502 | Cannot fetch runtime logs from the sandbox | Retry or check sandbox health | | `EMPTY_ARTIFACT` | 422 | Publish output contained no files | Verify your build step produces output files | | `SERVICE_UNAVAILABLE` | 503 | A required internal service (e.g. encryption) is not configured | Contact support | --- ## Snapshot errors Snapshots are accessed under `/api/v1/computers/{id}/snapshots` and `/api/v1/sandboxes/{id}/snapshots`. | Code | HTTP | Meaning | What to do | |---|---|---|---| | `NOT_FOUND` (snapshot) | 404 | Snapshot does not exist or belongs to a different computer | Verify snapshot ID and computer ID | | `FORBIDDEN` | 403 | Authenticated tenant does not own this snapshot | Check ownership | | `already_deleted` | 409 | Snapshot has already been deleted | No action; already in terminal state | | `snapshot is not in ready state` | 409 | Restore attempted before snapshot reached `ready` | Poll snapshot status until `ready` | | `missing sse ticket` | 401 | SSE stream opened without a ticket | Issue a ticket via `POST /auth/sse-ticket` first | --- ## Custom domain errors | Code | HTTP | Meaning | What to do | |---|---|---|---| | `DOMAIN_NOT_FOUND` | 404 | Custom domain record not found | Verify the domain ID | | `COMPUTER_NOT_FOUND` | 404 | The computer this domain is attached to was not found | Verify the computer ID | | `FORBIDDEN` | 403 | Domain belongs to a different tenant | Verify ownership | | `VALIDATION_FAILED` | 422 | `fqdn` missing or invalid | Include a valid fully-qualified domain name | --- ## Workspace errors | Code | HTTP | Meaning | What to do | |---|---|---|---| | `VALIDATION_FAILED` | 422 | Workspace creation or update fields are invalid | Inspect `details` | | `TEMPLATE_NAME_TAKEN` | 409 | A template with that name already exists in this workspace | Choose a different name | --- ## Rate-limit and quota errors | Code | HTTP | Meaning | What to do | |---|---|---|---| | `rate_limit_exceeded` | 429 | General API rate limit hit | Respect the `Retry-After` response header | | `SANDBOX_LIMIT_REACHED` | 409 | Concurrent sandbox cap reached | Destroy idle sandboxes or upgrade plan | | `INSUFFICIENT_CREDITS` | 402 | Credit balance depleted | Purchase credits from the billing dashboard | All 429 responses include a `Retry-After` header (seconds). Honor it - repeated violations may result in temporary IP blocks. --- ## See also - [API Reference overview](/docs/api-reference/) - base URL, auth, and request format - [Sandboxes](/docs/api-reference/sandboxes/) - sandbox lifecycle and file operations - [Computers](/docs/api-reference/computers/) - computer CRUD and desktop control - [Deployments](/docs/api-reference/deployments/) - deployment and version errors --- # Events (SSE) URL: https://miosa.ai/docs/api-reference/events Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/events Source: src/routes/docs/api-reference/events/+page.md Description: Server-Sent Event streams for real-time updates from sandboxes, computers, deployments, and builds. MIOSA exposes SSE streams for resources that produce timed events: sandboxes, computer sessions, deployment builds, sandbox-template builds, and computer logs. All streams follow the same authentication and envelope pattern. --- ## Authentication pattern The browser `EventSource` API cannot send an `Authorization` header. Use the two-step ticket flow for all SSE connections: Diagram: sequenceDiagram participant Client participant MIOSA API Client->>MIOSA API: POST /api/v1/auth/sse-ticket
Authorization: Bearer msk_u_... MIOSA API-->>Client: { "ticket": "sset_..." } Client->>MIOSA API: GET ?ticket=sset_...
Accept: text/event-stream MIOSA API-->>Client: text/event-stream (open connection) **Step 1 - Mint a ticket:** ```bash curl -X POST https://api.miosa.ai/api/v1/auth/sse-ticket \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" # Response: { "ticket": "sset_01hx9z...", "expires_in": 60 } ``` Tickets are single-use and expire in 60 seconds. Mint one immediately before opening the `EventSource`. **Step 2 - Open the stream:** ```bash curl -N "https://api.miosa.ai/api/v1/sandboxes/{id}/events?ticket=sset_01hx9z..." \ -H "Accept: text/event-stream" ``` --- ## Live SSE streams | Stream | Endpoint | Description | |---|---|---| | Sandbox events | `GET /sandboxes/{id}/events` | Exec progress, file change notifications, preview readiness | | Computer agent events | `GET /computers/{id}/agent/events` | Agent task progress and computer lifecycle | | Computer logs | `GET /computers/{id}/logs/stream` | stdout/stderr from computer processes | | Sandbox logs | `GET /sandboxes/{id}/logs/stream` | stdout/stderr from sandbox template lifecycle | | Build events | `GET /sandbox-template-builds/{id}/logs/stream` | Custom template build progress | --- ## Event envelope All events arrive in standard SSE format: ``` event: data: id: ``` All payloads are JSON. The `id:` line allows clients to resume with `Last-Event-ID` on reconnect. --- ## Sandbox event types | Event | Payload | Description | |---|---|---| | `sandbox.started` | `{sandbox_id, region}` | Boot complete; ready to accept exec | | `sandbox.exec.started` | `{exec_id, command}` | Command began | | `sandbox.exec.stdout` | `{exec_id, line}` | stdout chunk | | `sandbox.exec.stderr` | `{exec_id, line}` | stderr chunk | | `sandbox.exec.completed` | `{exec_id, exit_code, duration_ms}` | Command exited | | `sandbox.preview.ready` | `{preview_id, url, port}` | Preview URL available | | `sandbox.idle` | `{sandbox_id, last_activity_at}` | Going to auto-suspend | | `sandbox.suspended` | `{sandbox_id}` | Auto-suspended after idle window | --- ## Deployment event types | Event | Payload | Description | |---|---|---| | `deployment.publish.queued` | `{version_id}` | Publish accepted, waiting for builder | | `deployment.publish.building` | `{version_id, stage}` | Builder running | | `deployment.publish.uploading` | `{version_id, size_bytes}` | Release artifact upload | | `deployment.publish.promoting` | `{version_id}` | Switching active version | | `deployment.publish.completed` | `{version_id, deployment_id, url}` | Live | | `deployment.publish.failed` | `{version_id, error_code, message}` | Build or health-check failure | --- ## Computer event types | Event | Payload | Description | |---|---|---| | `computer.started` | `{computer_id, region}` | VM booted, desktop ready | | `computer.stream.token_ready` | `{token, expires_at}` | New short-lived stream credential | | `computer.idle` | `{computer_id, idle_seconds}` | Approaching auto-stop | | `computer.stopped` | `{computer_id, reason}` | Stopped (manual or auto) | --- ## Reconnect and replay Send `Last-Event-ID: <ULID>` on the resubscribe request. The server replays all events since that ID. Events are retained for **15 minutes** per resource. ```bash curl -N "https://api.miosa.ai/api/v1/sandboxes/{id}/events?ticket=sset_..." \ -H "Accept: text/event-stream" \ -H "Last-Event-ID: 01hwqz4b3c2v1x..." ``` --- ## Error handling If the connection drops or the ticket expires, the next request returns `401`. Mint a fresh ticket and resubscribe: ```typescript async function openStream(sandboxId: string): Promise { const { ticket } = await fetch('/api/v1/auth/sse-ticket', { method: 'POST', headers: { Authorization: `Bearer ${process.env.MIOSA_API_KEY}` }, }).then(r => r.json()); const es = new EventSource( `https://api.miosa.ai/api/v1/sandboxes/${sandboxId}/events/stream?ticket=${ticket}` ); es.addEventListener('error', () => { es.close(); setTimeout(() => openStream(sandboxId), 1000); }); es.addEventListener('sandbox.exec.completed', (e) => { console.log('Exec done:', JSON.parse(e.data)); }); } ``` --- ## See also Create sandboxes, run exec, write files, open previews. [Sandboxes →](/docs/develop/sandboxes/) Builder, Release, Version - the full deploy pipeline. [Publishing →](/docs/deploy/publishing/) Desktop lifecycle, screenshot, click, keyboard APIs. [Computers →](/docs/computers/overview/) API keys, browser tokens, SSE tickets. [Authentication →](/docs/authentication/) --- # Exec API URL: https://miosa.ai/docs/api-reference/exec Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/exec Source: src/routes/docs/api-reference/exec/+page.md Description: API reference for executing bash commands and Python code on MIOSA computers. Run commands and scripts directly on a computer without desktop interaction. Both endpoints block until the command exits and return combined stdout+stderr. Base path: `/api/v1/computers/{id}/exec` The computer must be in `"running"` status. Commands run as the default user inside the VM. Maximum timeout is 300 s. Rate limit: 300 req/min per workspace. For long-running output, prefer the streaming exec endpoint which avoids holding an HTTP connection open for the full duration. --- ## Execute Bash Command **`POST /api/v1/computers/{id}/exec`** Runs a shell command on the computer. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `command` | string | Yes | Shell command to execute | | `timeout` | integer | No | Timeout in seconds (default: 30, max: 300) | ### Response - `200 OK` ```json { "output": "total 24\ndrwxr-xr-x 6 user user 4096 Apr 11 10:00 .\n", "exit_code": 0 } ``` The `output` field contains combined stdout and stderr. ### Errors | Status | Code | Description | |--------|------|-------------| | 409 | `COMPUTER_NOT_RUNNING` | Computer is not running | | 502 | - | In-VM agent unreachable | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/exec \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command": "ls -la /workspace", "timeout": 30}' ``` ### Examples ```bash # Install a package curl -X POST https://api.miosa.ai/api/v1/computers/{id}/exec \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command": "sudo apt-get install -y jq", "timeout": 120}' # Check disk space curl -X POST https://api.miosa.ai/api/v1/computers/{id}/exec \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command": "df -h"}' # Multi-line script curl -X POST https://api.miosa.ai/api/v1/computers/{id}/exec \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command": "cd /workspace && mkdir -p project && echo done"}' ``` --- ## Execute Python Code **`POST /api/v1/computers/{id}/exec/python`** Runs Python code on the computer. The code is written to a temporary file and executed with `python3`. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `code` | string | Yes | Python source code | | `timeout` | integer | No | Timeout in seconds (default: 30, max: 300) | ### Response - `200 OK` ```json { "output": "42\n", "exit_code": 0 } ``` ### How It Works 1. Code is written to `/tmp/miosa_exec_<random>.py` 2. Executed via `timeout <N> python3 /tmp/miosa_exec_<random>.py 2>&1` 3. Temporary file is deleted after execution 4. stdout and stderr are captured in `output` ### Errors | Status | Code | Description | |--------|------|-------------| | 409 | `COMPUTER_NOT_RUNNING` | Computer is not running | | 502 | - | In-VM agent unreachable | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/exec/python \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "import sys\nprint(f\"Python {sys.version}\")\nprint(2 + 2)", "timeout": 30 }' ``` ### Examples ```bash # JSON processing curl -X POST https://api.miosa.ai/api/v1/computers/{id}/exec/python \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "import json\ndata = {\"hello\": \"world\"}\nprint(json.dumps(data, indent=2))", "timeout": 10 }' # File processing curl -X POST https://api.miosa.ai/api/v1/computers/{id}/exec/python \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "import os\nfor f in os.listdir(\"/workspace\"):\n print(f)", "timeout": 10 }' # Long-running computation curl -X POST https://api.miosa.ai/api/v1/computers/{id}/exec/python \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "total = sum(range(10_000_000))\nprint(f\"Sum: {total}\")", "timeout": 60 }' ``` --- ## Terminal ### Create Terminal Session **`POST /api/v1/computers/{id}/terminal`** Creates a WebSocket-based terminal session on the computer. #### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `cols` | integer | No | Terminal columns (default: 80) | | `rows` | integer | No | Terminal rows (default: 24) | | `shell` | string | No | Shell to use (default: `/bin/bash`) | #### Response - `201 Created` ```json { "data": { "session_id": "default", "ws_url": "wss://my-computer.sandbox.miosa.ai/ws/terminal/550e8400-.../default?auth=stream_token", "stream_auth": "stream_token", "expires_at": 1712700060, "computer_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/terminal \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"cols": 80, "rows": 24, "shell": "/bin/bash"}' ``` --- ### Resize Terminal **`POST /api/v1/computers/{id}/pty/{session_id}/resize`** Resizes an active terminal session. #### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `cols` | integer | Yes | New column count | | `rows` | integer | Yes | New row count | #### Response - `200 OK` ```json { "status": "ok" } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/pty/{session_id}/resize \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"cols": 120, "rows": 40}' ``` --- ## Timeout Behavior - Commands exceeding the timeout are killed with SIGTERM - The maximum timeout is **300 seconds** (5 minutes) - If no timeout is specified, the default is **30 seconds** - The timeout value is clamped: `min(requested, 300)` ## Pre-installed Software The default template includes: - Python 3 with pip - Node.js (if installed via selected_apps) - Common CLI tools: curl, wget, git, vim, jq - System utilities: htop, tree, zip/unzip Install additional packages via the bash exec endpoint: ```bash # Python packages {"command": "pip install requests pandas numpy"} # System packages {"command": "sudo apt-get install -y postgresql-client"} ``` --- ## See also - [Streaming Exec](/docs/api-reference/streaming-exec/) - SSE stream for long-running commands - [Computers API](/docs/api-reference/computers/) - computer CRUD and lifecycle - [Files API](/docs/api-reference/files/) - read and write files without exec - [Error Codes](/docs/api-reference/errors/) - `COMPUTER_NOT_RUNNING`, `AGENT_UNAVAILABLE` --- # Files API URL: https://miosa.ai/docs/api-reference/files Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/files Source: src/routes/docs/api-reference/files/+page.md Description: API reference for file upload, download, list, export, and delete operations. Manage files on a running computer. All paths are restricted to `/workspace`, `/home`, `/root`, `/tmp`, `/opt`, and `/srv`. Base path: `/api/v1/computers/{id}/files` The computer must be in `"running"` status. All operations go through the computer command service. --- ## Upload a File **`POST /api/v1/computers/{id}/files/upload`** Uploads a file to the computer via multipart form data. ### Request Content-Type: `multipart/form-data` | Field | Type | Required | Description | |-------|------|----------|-------------| | `file` | file | Yes | File to upload | | `path` | string | No | Remote path. If ends with `/`, filename is appended. Default: `/workspace/` | ### Response - `201 Created` ```json { "success": true, "file": { "path": "/workspace/script.py", "filename": "script.py", "size_bytes": 1234 } } ``` ### Errors | Status | Code | Description | |--------|------|-------------| | 400 | `MISSING_FILE` | No file field in request | | 403 | `FORBIDDEN_PATH` | Path outside allowed directories | | 413 | `FILE_TOO_LARGE` | File exceeds 10 MB limit | | 502 | `AGENT_UNAVAILABLE` | In-VM agent unreachable | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/files/upload \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -F "file=@./script.py" \ -F "path=/workspace/scripts/" ``` --- ## List Directory **`GET /api/v1/computers/{id}/files`** Lists files and directories at a given path. ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | No | Directory to list. Default: `/workspace` | ### Response - `200 OK` ```json { "files": [ { "name": "Documents", "type": "directory", "size_bytes": 4096, "modified_at": "2026-04-11T10:30:00Z" }, { "name": "script.py", "type": "file", "size_bytes": 1234, "modified_at": "2026-04-11T10:25:00Z" } ], "path": "/workspace" } ``` ### File Entry Fields | Field | Type | Description | |-------|------|-------------| | `name` | string | File or directory name | | `type` | string | `"file"` or `"directory"` | | `size_bytes` | integer | Size in bytes | | `modified_at` | ISO 8601 | Last modification time | ```bash curl "https://api.miosa.ai/api/v1/computers/{id}/files?path=/workspace" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Download a File **`GET /api/v1/computers/{id}/files/download`** Downloads a file from the computer as binary data. ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Full path to the file | ### Response - `200 OK` Binary file content. Content-Type is inferred from the file extension. The `Content-Disposition` header is set for download. ### Errors | Status | Code | Description | |--------|------|-------------| | 400 | `MISSING_PATH` | No path parameter | | 403 | `FORBIDDEN_PATH` | Path outside allowed directories | | 502 | `AGENT_UNAVAILABLE` | In-VM agent unreachable | ```bash curl "https://api.miosa.ai/api/v1/computers/{id}/files/download?path=/workspace/output.txt" \ -H "Authorization: Bearer $MIOSA_API_KEY" \ --output output.txt ``` --- ## Export a File **`POST /api/v1/computers/{id}/files/export`** Returns file metadata and base64-encoded content in a single response. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `path` | string | Yes | Full path to the file | ### Response - `200 OK` ```json { "success": true, "file": { "path": "/workspace/data.json", "filename": "data.json", "size_bytes": 5678, "content_type": "application/json", "modified_at": "2026-04-11T10:30:00Z" }, "content_base64": "eyJrZXkiOiAidmFsdWUifQ==" } ``` ### Errors | Status | Code | Description | |--------|------|-------------| | 400 | `MISSING_PATH` | No path in request body | | 403 | `FORBIDDEN_PATH` | Path outside allowed directories | | 404 | `FILE_NOT_FOUND` | File does not exist or is empty | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/files/export \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"path": "/workspace/data.json"}' ``` --- ## Delete a File **`DELETE /api/v1/computers/{id}/files`** Removes a file from the computer. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `path` | string | Yes | Full path to the file to delete | ### Response - `200 OK` ```json { "success": true, "path": "/workspace/old-file.txt" } ``` ### Errors | Status | Code | Description | |--------|------|-------------| | 400 | `MISSING_PATH` | No path in request body | | 403 | `FORBIDDEN_PATH` | Path outside allowed directories | | 502 | `AGENT_UNAVAILABLE` | In-VM agent unreachable | ```bash curl -X DELETE https://api.miosa.ai/api/v1/computers/{id}/files \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"path": "/workspace/old-file.txt"}' ``` --- ## Path Security All paths are validated before execution: 1. **Expansion** - `Path.expand` resolves `..`, `~`, and symlinks 2. **Prefix check** - Expanded path must start with `/workspace`, `/home`, `/root`, `/tmp`, `/opt`, or `/srv` 3. **Rejection** - Paths outside these prefixes return `403 FORBIDDEN_PATH` This prevents path traversal attacks. For example, `/workspace/../../etc/passwd` expands to `/etc/passwd` and is rejected. --- # Filesystem API URL: https://miosa.ai/docs/api-reference/filesystem Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/filesystem Source: src/routes/docs/api-reference/filesystem/+page.md Description: Complete API reference for file and directory operations on MIOSA computers - upload, download, stat, mkdir, rename, copy, chmod, and readdir. The Filesystem API gives you full programmatic access to files and directories inside a running computer. All operations are proxied through the computer command service and restricted to allowed paths. Base path: `/api/v1/computers/{id}/files` **Allowed paths:** `/workspace`, `/home`, `/root`, `/tmp`, `/opt`, `/srv` The computer must be in `"running"` status. Paths outside the allowed prefixes return `403 FORBIDDEN_PATH`. --- ## Quick Start ```typescript const client = new Miosa(); // Write a file await client.files.write(computerId, { path: '/workspace/hello.py', content: 'print("Hello from MIOSA")', }); // List the directory const { files } = await client.files.list(computerId, '/workspace'); // Download it back const content = await client.files.download(computerId, '/workspace/hello.py'); ``` --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `POST` | `/files/upload` | Upload a file (multipart) | | `POST` | `/files/write` | Write text/binary content directly | | `GET` | `/files` | List directory (`readdir`) | | `GET` | `/files/download` | Download a file | | `POST` | `/files/export` | Download with metadata (base64) | | `DELETE` | `/files` | Delete a file | | `GET` | `/files/stat` | Stat a file or directory | | `POST` | `/files/mkdir` | Create a directory | | `POST` | `/files/rename` | Rename or move a file/directory | | `POST` | `/files/copy` | Copy a file | | `POST` | `/files/chmod` | Change file permissions | --- ## Upload a File **`POST /api/v1/computers/{id}/files/upload`** Multipart upload - use this for binary files or large payloads. ### Request - `multipart/form-data` | Field | Type | Required | Description | |-------|------|----------|-------------| | `file` | file | Yes | File to upload | | `path` | string | No | Remote path. If ends with `/`, filename is appended. Default: `/workspace/` | ### Response - `201 Created` ```json { "success": true, "file": { "path": "/workspace/script.py", "filename": "script.py", "size_bytes": 1234 } } ``` ### Errors | Status | Code | Description | |--------|------|-------------| | 400 | `MISSING_FILE` | No file field in request | | 403 | `FORBIDDEN_PATH` | Path outside allowed directories | | 413 | `FILE_TOO_LARGE` | File exceeds 10 MB limit | | 502 | `AGENT_UNAVAILABLE` | In-VM agent unreachable | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/files/upload \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -F "file=@./script.py" \ -F "path=/workspace/scripts/" ``` --- ## Write a File **`POST /api/v1/computers/{id}/files/write`** Write text or base64-encoded binary content. Faster than multipart for programmatic use. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `path` | string | Yes | Full destination path | | `content` | string | Yes | File content (UTF-8 text or base64 when `encoding="base64"`) | | `encoding` | string | No | `"utf8"` (default) or `"base64"` | ### Response - `201 Created` ```json { "success": true, "file": { "path": "/workspace/config.json", "size_bytes": 256 } } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/files/write \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"path": "/workspace/config.json", "content": "{\"key\": \"value\"}"}' ``` --- ## List Directory (readdir) **`GET /api/v1/computers/{id}/files`** ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | No | Directory to list. Default: `/workspace` | ### Response - `200 OK` ```json { "files": [ { "name": "Documents", "type": "directory", "size_bytes": 4096, "permissions": "drwxr-xr-x", "modified_at": "2026-04-11T10:30:00Z" }, { "name": "script.py", "type": "file", "size_bytes": 1234, "permissions": "-rw-r--r--", "modified_at": "2026-04-11T10:25:00Z" } ], "path": "/workspace" } ``` ```bash curl "https://api.miosa.ai/api/v1/computers/{id}/files?path=/workspace" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Download a File **`GET /api/v1/computers/{id}/files/download`** ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Full path to the file | ### Response - `200 OK` Binary file content. `Content-Disposition` is set for browser download. ### Errors | Status | Code | Description | |--------|------|-------------| | 400 | `MISSING_PATH` | No path parameter | | 403 | `FORBIDDEN_PATH` | Path outside allowed directories | | 404 | `FILE_NOT_FOUND` | File does not exist | | 502 | `AGENT_UNAVAILABLE` | In-VM agent unreachable | ```bash curl "https://api.miosa.ai/api/v1/computers/{id}/files/download?path=/workspace/output.txt" \ -H "Authorization: Bearer $MIOSA_API_KEY" \ --output output.txt ``` --- ## Export a File **`POST /api/v1/computers/{id}/files/export`** Returns metadata and base64-encoded content in a single JSON response. Useful when you need the file and its metadata together without handling binary responses. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `path` | string | Yes | Full path to the file | ### Response - `200 OK` ```json { "success": true, "file": { "path": "/workspace/data.json", "filename": "data.json", "size_bytes": 5678, "content_type": "application/json", "modified_at": "2026-04-11T10:30:00Z" }, "content_base64": "eyJrZXkiOiAidmFsdWUifQ==" } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/files/export \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"path": "/workspace/data.json"}' ``` --- ## Delete a File **`DELETE /api/v1/computers/{id}/files`** ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `path` | string | Yes | Full path to delete | ### Response - `200 OK` ```json { "success": true, "path": "/workspace/old-file.txt" } ``` ```bash curl -X DELETE https://api.miosa.ai/api/v1/computers/{id}/files \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"path": "/workspace/old-file.txt"}' ``` --- ## Stat a File or Directory **`GET /api/v1/computers/{id}/files/stat`** Returns metadata without downloading file content. ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Full path | ### Response - `200 OK` ```json { "path": "/workspace/script.py", "type": "file", "size_bytes": 1234, "permissions": "-rw-r--r--", "owner": "user", "group": "user", "modified_at": "2026-04-11T10:25:00Z", "created_at": "2026-04-11T09:00:00Z" } ``` ### Errors | Status | Code | Description | |--------|------|-------------| | 404 | `FILE_NOT_FOUND` | Path does not exist | ```bash curl "https://api.miosa.ai/api/v1/computers/{id}/files/stat?path=/workspace/script.py" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Create a Directory **`POST /api/v1/computers/{id}/files/mkdir`** ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `path` | string | Yes | Directory path to create | | `recursive` | boolean | No | Create parent directories if missing (default: `false`) | ### Response - `201 Created` ```json { "success": true, "path": "/workspace/project/src" } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/files/mkdir \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"path": "/workspace/project/src", "recursive": true}' ``` --- ## Rename or Move **`POST /api/v1/computers/{id}/files/rename`** Renames a file or directory, or moves it to a different path. Both source and destination must be within allowed paths. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `source` | string | Yes | Current path | | `destination` | string | Yes | Target path | ### Response - `200 OK` ```json { "success": true, "source": "/workspace/old.txt", "destination": "/workspace/new.txt" } ``` ### Errors | Status | Code | Description | |--------|------|-------------| | 404 | `FILE_NOT_FOUND` | Source path does not exist | | 409 | `DESTINATION_EXISTS` | Destination already exists | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/files/rename \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"source": "/workspace/app.py", "destination": "/workspace/app_v2.py"}' ``` --- ## Copy a File **`POST /api/v1/computers/{id}/files/copy`** Copies a file to a new path. Directory copy is not supported - copy individual files. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `source` | string | Yes | Source file path | | `destination` | string | Yes | Destination file path | ### Response - `201 Created` ```json { "success": true, "source": "/workspace/template.py", "destination": "/workspace/project/main.py" } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/files/copy \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"source": "/workspace/template.py", "destination": "/workspace/project/main.py"}' ``` --- ## Change Permissions **`POST /api/v1/computers/{id}/files/chmod`** Sets UNIX file permissions. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `path` | string | Yes | File path | | `mode` | string | Yes | Octal permission string, e.g. `"755"` or `"644"` | ### Response - `200 OK` ```json { "success": true, "path": "/workspace/script.sh", "mode": "755" } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/files/chmod \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"path": "/workspace/script.sh", "mode": "755"}' ``` --- ## Path Security All paths are validated before execution: 1. **Expansion** - `Path.expand` resolves `..`, `~`, and symlinks 2. **Prefix check** - Must start with `/workspace`, `/home`, `/root`, `/tmp`, `/opt`, or `/srv` 3. **Rejection** - Paths outside these prefixes return `403 FORBIDDEN_PATH` `/workspace/../../etc/passwd` expands to `/etc/passwd` and is rejected. --- ## Common Recipes ### Scaffold a project directory ```typescript // Create structure await client.files.mkdir(computerId, { path: '/workspace/app/src', recursive: true }); await client.files.mkdir(computerId, { path: '/workspace/app/tests', recursive: true }); // Write files await client.files.write(computerId, { path: '/workspace/app/src/main.py', content: mainPy }); await client.files.write(computerId, { path: '/workspace/app/requirements.txt', content: reqs }); // Make entrypoint executable await client.files.chmod(computerId, { path: '/workspace/app/src/main.py', mode: '755' }); ``` ### Retrieve build artifacts ```python # List the build output directory files = client.files.list(computer_id, path="/workspace/app/dist") for f in files["files"]: if f["type"] == "file": content = client.files.export(computer_id, path=f"/workspace/app/dist/{f['name']}") save_artifact(f["name"], content["content_base64"]) ``` --- # Invites API URL: https://miosa.ai/docs/api-reference/invites Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/invites Source: src/routes/docs/api-reference/invites/+page.md Description: API reference for the workspace invite and org invite flows - send, list, revoke, preview, and accept email invitations. MIOSA has two parallel invite flows: - **Workspace invites** - invite a user to a specific workspace. If the email already belongs to an org member, the user is added directly (no email sent). If not, an invite email is dispatched and accepting it creates both the `tenant_members` and `workspace_members` rows atomically. - **Org invites** - invite a user to join the org (tenant) without pre-selecting a workspace. The accept step only creates the `tenant_members` row. Both flows produce opaque tokens. The `GET /invites/:token` and `GET /workspace-invites/:token` preview endpoints are **public** - they can be called without an API key to render a landing page before the user signs in. --- ## Workspace Invite Endpoints | Method | Path | Auth | Description | |--------|------|------|-------------| | `POST` | `/workspaces/{id}/invites` | Required | Send a workspace invite | | `GET` | `/workspaces/{id}/invites` | Required | List pending invites | | `DELETE` | `/workspaces/{id}/invites/{invite_id}` | Required | Revoke an invite | | `GET` | `/workspace-invites/{token}` | None (public) | Preview invite details | | `POST` | `/workspace-invites/{token}/accept` | Required | Accept the invite | ## Org Invite Endpoints | Method | Path | Auth | Description | |--------|------|------|-------------| | `POST` | `/tenants/{id}/invites` | Required (admin/owner) | Send an org invite | | `GET` | `/tenants/{id}/invites` | Required (admin/owner) | List pending org invites | | `DELETE` | `/tenants/{id}/invites/{invite_id}` | Required (admin/owner) | Revoke an org invite | | `GET` | `/invites/{token}` | None (public) | Preview org invite details | | `POST` | `/invites/{token}/accept` | Required | Accept the org invite | --- ## Workspace Invite Flow ### Send Workspace Invite **`POST /api/v1/workspaces/{id}/invites`** Sends an invite email and returns the invite record. If the `email` already belongs to a tenant member the user is added directly (response `type = "added"`, no email sent). #### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `email` | string | Yes | Email address to invite | | `role` | string | Yes | `owner`, `admin`, `member`, or `viewer` | #### Response - `201 Created` (new invite) ```json { "type": "invited", "data": { "id": "inv_abc123", "workspace_id": "ws-uuid", "tenant_id": "ten-uuid", "email": "alice@example.com", "role": "member", "invited_by": "usr_owner", "expires_at": "2026-05-29T09:00:00Z", "accepted_at": null, "inserted_at": "2026-05-22T09:00:00Z" } } ``` #### Response - `200 OK` (existing member added directly) ```json { "type": "added", "data": { "user_id": "usr_def456", "workspace_id": "ws-uuid", "role": "member", "joined_at": "2026-05-22T09:00:00Z", "added_by": "usr_owner" } } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/workspaces/{id}/invites \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "alice@example.com", "role": "member"}' ``` ```typescript const client = new Miosa(); const result = await client.workspaceInvites.create('ws-uuid', { email: 'alice@example.com', role: 'member', }); // result.type === 'invited' | 'added' ``` ```python client = miosa.Miosa() result = client.workspace_invites.create("ws-uuid", "alice@example.com", "member") print(result["type"]) # "invited" or "added" ``` ```go result, err := client.WorkspaceInvites.Create(ctx, "ws-uuid", miosa.CreateWorkspaceInviteInput{ Email: "alice@example.com", Role: "member", }) ``` ```elixir {:ok, result} = Miosa.WorkspaceInvites.create(client, "ws-uuid", "alice@example.com", "member") IO.inspect(result["type"]) ``` ```java Map result = miosa.workspaceInvites().create("ws-uuid", "alice@example.com", "member"); String type = (String) result.get("type"); // "invited" or "added" ``` --- ### List Workspace Invites **`GET /api/v1/workspaces/{id}/invites`** Returns all pending workspace invites (excludes accepted and revoked). #### Response - `200 OK` ```json { "data": [ { "id": "inv_abc123", "workspace_id": "ws-uuid", "tenant_id": "ten-uuid", "email": "alice@example.com", "role": "member", "invited_by": "usr_owner", "expires_at": "2026-05-29T09:00:00Z", "accepted_at": null, "inserted_at": "2026-05-22T09:00:00Z" } ] } ``` ```bash curl https://api.miosa.ai/api/v1/workspaces/{id}/invites \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` ```typescript const invites = await client.workspaceInvites.list('ws-uuid'); ``` ```python invites = client.workspace_invites.list("ws-uuid") ``` ```go invites, err := client.WorkspaceInvites.List(ctx, "ws-uuid") ``` ```elixir {:ok, invites} = Miosa.WorkspaceInvites.list(client, "ws-uuid") ``` ```java List> invites = miosa.workspaceInvites().list("ws-uuid"); ``` --- ### Revoke Workspace Invite **`DELETE /api/v1/workspaces/{id}/invites/{invite_id}`** Revokes a pending invite. Returns `409` if the invite was already accepted. #### Response - `200 OK` ```json { "invite_id": "inv_abc123", "revoked": true } ``` ```bash curl -X DELETE https://api.miosa.ai/api/v1/workspaces/{id}/invites/{invite_id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` ```typescript await client.workspaceInvites.revoke('ws-uuid', 'inv_abc123'); ``` ```python client.workspace_invites.revoke("ws-uuid", "inv_abc123") ``` ```go _, err := client.WorkspaceInvites.Revoke(ctx, "ws-uuid", "inv_abc123") ``` ```elixir {:ok, _} = Miosa.WorkspaceInvites.revoke(client, "ws-uuid", "inv_abc123") ``` ```java miosa.workspaceInvites().revoke("ws-uuid", "inv_abc123"); ``` --- ### Preview Workspace Invite (public) **`GET /api/v1/workspace-invites/{token}`** Returns invite metadata without requiring authentication. Use this to render the pre-auth landing page. #### Response - `200 OK` ```json { "data": { "workspace_name": "Dr. Smith Clinic", "role": "member", "expires_at": "2026-05-29T09:00:00Z", "expired": false, "accepted": false } } ``` ```bash curl https://api.miosa.ai/api/v1/workspace-invites/{token} ``` ```typescript const preview = await client.workspaceInvites.preview(token); if (!preview.expired) { // show accept button } ``` ```python preview = client.workspace_invites.preview(token) if not preview["expired"]: pass # show accept button ``` ```go preview, err := client.WorkspaceInvites.Preview(ctx, token) ``` ```elixir {:ok, preview} = Miosa.WorkspaceInvites.preview(client, token) unless preview["expired"], do: ... ``` ```java Map preview = miosa.workspaceInvites().preview(token); ``` --- ### Accept Workspace Invite **`POST /api/v1/workspace-invites/{token}/accept`** Accepts the invite on behalf of the authenticated user. If the user is new to the org, a `tenant_members` row is created atomically alongside the `workspace_members` row. The caller's API key email must match the invite email (case-insensitive). #### Response - `200 OK` ```json { "accepted": true, "workspace_id": "ws-uuid", "tenant_id": "ten-uuid", "role": "member" } ``` #### Errors | Status | Code | Cause | |--------|------|-------| | 400 | `INVALID_TOKEN` | Token not found or expired | | 400 | `REVOKED` | Invite was revoked | | 422 | `EMAIL_MISMATCH` | Authenticated user's email differs from invite email | ```bash curl -X POST https://api.miosa.ai/api/v1/workspace-invites/{token}/accept \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` ```typescript const result = await client.workspaceInvites.accept(token); console.log('joined workspace:', result.workspaceId); ``` ```python result = client.workspace_invites.accept(token) print("joined workspace:", result["workspace_id"]) ``` ```go result, err := client.WorkspaceInvites.Accept(ctx, token) ``` ```elixir {:ok, result} = Miosa.WorkspaceInvites.accept(client, token) IO.puts("joined workspace: #{result["workspace_id"]}") ``` ```java Map result = miosa.workspaceInvites().accept(token); ``` --- ## Org Invite Flow Org invites grant membership in the tenant (org) without assigning a specific workspace. Write operations require `admin` or `owner` role. ### Send Org Invite **`POST /api/v1/tenants/{id}/invites`** Dispatches an invite email. The response includes an `invite_url` that is host-aware - on white-label tenants it uses the tenant's custom domain. #### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `email` | string | Yes | Email address to invite | | `role` | string | Yes | `owner`, `admin`, or `member` | #### Response - `201 Created` ```json { "data": { "invite_id": "inv_xyz789", "email": "bob@example.com", "role": "member", "expires_at": "2026-05-29T09:00:00Z", "invite_url": "https://app.miosa.ai/invites/eyJ..." } } ``` ```bash curl -X POST https://api.miosa.ai/api/v1/tenants/{id}/invites \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "bob@example.com", "role": "member"}' ``` ```typescript const invite = await client.orgInvites.create(tenantId, { email: 'bob@example.com', role: 'member', }); console.log('invite URL:', invite.inviteUrl); ``` ```python invite = client.org_invites.create(tenant_id, "bob@example.com", "member") print("invite URL:", invite["invite_url"]) ``` ```go invite, err := client.OrgInvites.Create(ctx, tenantID, miosa.CreateOrgInviteInput{ Email: "bob@example.com", Role: "member", }) ``` ```elixir {:ok, invite} = Miosa.OrgInvites.create(client, tenant_id, "bob@example.com", "member") IO.puts("invite URL: #{invite["invite_url"]}") ``` ```java Map invite = miosa.orgInvites().create(tenantId, "bob@example.com", "member"); System.out.println("invite URL: " + invite.get("invite_url")); ``` --- ### List Org Invites **`GET /api/v1/tenants/{id}/invites`** Returns all pending org invites. ```bash curl https://api.miosa.ai/api/v1/tenants/{id}/invites \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` ```typescript const invites = await client.orgInvites.list(tenantId); ``` ```python invites = client.org_invites.list(tenant_id) ``` ```go invites, err := client.OrgInvites.List(ctx, tenantID) ``` ```elixir {:ok, invites} = Miosa.OrgInvites.list(client, tenant_id) ``` ```java List> invites = miosa.orgInvites().list(tenantId); ``` --- ### Revoke Org Invite **`DELETE /api/v1/tenants/{id}/invites/{invite_id}`** Revokes a pending org invite. Returns `409` if the invite was already accepted. ```bash curl -X DELETE https://api.miosa.ai/api/v1/tenants/{id}/invites/{invite_id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` ```typescript await client.orgInvites.revoke(tenantId, inviteId); ``` ```python client.org_invites.revoke(tenant_id, invite_id) ``` ```go _, err := client.OrgInvites.Revoke(ctx, tenantID, inviteID) ``` ```elixir {:ok, _} = Miosa.OrgInvites.revoke(client, tenant_id, invite_id) ``` ```java miosa.orgInvites().revoke(tenantId, inviteId); ``` --- ### Preview Org Invite (public) **`GET /api/v1/invites/{token}`** Returns invite metadata without authentication. #### Response - `200 OK` ```json { "data": { "email": "bob@example.com", "tenant_name": "Acme Corp", "role": "member", "expires_at": "2026-05-29T09:00:00Z", "expired": false, "accepted": false } } ``` ```bash curl https://api.miosa.ai/api/v1/invites/{token} ``` ```typescript const preview = await client.orgInvites.preview(token); console.log('invited to org:', preview.tenantName); ``` ```python preview = client.org_invites.preview(token) print("invited to org:", preview["tenant_name"]) ``` ```go preview, err := client.OrgInvites.Preview(ctx, token) ``` ```elixir {:ok, preview} = Miosa.OrgInvites.preview(client, token) IO.inspect(preview["tenant_name"]) ``` ```java Map preview = miosa.orgInvites().preview(token); ``` --- ### Accept Org Invite **`POST /api/v1/invites/{token}/accept`** Accepts an org invite on behalf of the authenticated user. Creates a `tenant_members` row. The caller's API key email must match the invite email (case-insensitive). #### Response - `200 OK` ```json { "accepted": true, "tenant_id": "ten-uuid", "tenant": { "id": "ten-uuid", "name": "Acme Corp" } } ``` #### Errors | Status | Code | Cause | |--------|------|-------| | 400 | `invalid_token` | Token not found or expired | | 422 | `EMAIL_MISMATCH` | Session email differs from invite email | ```bash curl -X POST https://api.miosa.ai/api/v1/invites/{token}/accept \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` ```typescript const result = await client.orgInvites.accept(token); console.log('joined org:', result.tenantId); ``` ```python result = client.org_invites.accept(token) print("joined org:", result["tenant_id"]) ``` ```go result, err := client.OrgInvites.Accept(ctx, token) ``` ```elixir {:ok, result} = Miosa.OrgInvites.accept(client, token) IO.puts("joined org: #{result["tenant_id"]}") ``` ```java Map result = miosa.orgInvites().accept(token); ``` --- ## Common Errors | Status | Code | Cause | |--------|------|-------| | 400 | `INVALID_TOKEN` / `invalid_token` | Token not found, expired, or revoked | | 400 | `REVOKED` | Invite was explicitly revoked | | 409 | `ALREADY_ACCEPTED` | Cannot revoke an invite that was already accepted | | 422 | `EMAIL_MISMATCH` | Authenticated user's email does not match the invite email | | 422 | `MISSING_EMAIL` | `email` field missing from request body | --- ## See also - [Workspace Members API](/docs/api-reference/members) - manage the roster directly (requires prior org membership) - [Workspaces API](/docs/api-reference/workspaces) - workspace CRUD - [Error Codes](/docs/api-reference/errors) - full error reference --- # Workspace Members API URL: https://miosa.ai/docs/api-reference/members Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/members Source: src/routes/docs/api-reference/members/+page.md Description: API reference for managing workspace member rosters - add, remove, and update roles for users inside a workspace. Workspace members are the user roster for a specific workspace. A user must already be an org member (`tenant_members` row) before they can be added to a workspace. The last `owner` in a workspace cannot be removed until another member is promoted to owner. Base path: `/api/v1/workspaces/:id/members` To invite a new user who is not yet an org member, use the [Workspace Invites API](/docs/api-reference/invites) instead. Accepting a workspace invite automatically creates the `tenant_members` row when the user is new to the org. --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/workspaces/{id}/members` | List workspace members | | `POST` | `/workspaces/{id}/members` | Add an existing org member to the workspace | | `PATCH` | `/workspaces/{id}/members/{user_id}` | Update a member's role | | `DELETE` | `/workspaces/{id}/members/{user_id}` | Remove a member | --- ## Roles | Role | Description | |------|-------------| | `owner` | Full admin including transfer and deletion | | `admin` | Can manage members and resources | | `member` | Can create and manage resources | | `viewer` | Read-only access | --- ## List Members **`GET /api/v1/workspaces/{id}/members`** Returns all members of the workspace. ### Response - `200 OK` ```json { "data": [ { "user_id": "usr_abc123", "email": "alice@example.com", "name": "Alice", "avatar_url": null, "role": "owner", "joined_at": "2026-05-01T10:00:00Z", "added_by": null }, { "user_id": "usr_def456", "email": "bob@example.com", "name": "Bob", "avatar_url": null, "role": "member", "joined_at": "2026-05-10T14:00:00Z", "added_by": "usr_abc123" } ] } ``` ```bash curl https://api.miosa.ai/api/v1/workspaces/{id}/members \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` ```typescript const client = new Miosa(); const members = await client.workspaceMembers.list('ws-uuid'); console.log(members); ``` ```python client = miosa.Miosa() members = client.workspace_members.list("ws-uuid") print(members) ``` ```go client := miosa.NewClient(os.Getenv("MIOSA_API_KEY")) members, err := client.WorkspaceMembers.List(ctx, "ws-uuid") ``` ```elixir client = Miosa.client("msk_u_...") {:ok, members} = Miosa.WorkspaceMembers.list(client, "ws-uuid") ``` ```java MiosaClient miosa = new MiosaClient("msk_u_..."); List> members = miosa.workspaceMembers().list("ws-uuid"); ``` --- ## Add Member **`POST /api/v1/workspaces/{id}/members`** Adds an existing org member to the workspace with the given role. The user must already hold a `tenant_members` row for this org. If they are not yet an org member, send a workspace invite via `POST /workspaces/{id}/invites` instead - accepting the invite creates the org membership atomically. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `user_id` | UUID | Yes | The user's ID - must already be a tenant member | | `role` | string | Yes | `owner`, `admin`, `member`, or `viewer` | ### Response - `201 Created` ```json { "data": { "user_id": "usr_def456", "workspace_id": "ws-uuid", "role": "member", "joined_at": "2026-05-22T09:00:00Z", "added_by": "usr_abc123" } } ``` ### Errors | Status | Code | Cause | |--------|------|-------| | 422 | `NOT_TENANT_MEMBER` | User is not a member of the parent org | | 422 | `MISSING_USER_ID` | `user_id` field missing | | 422 | `MISSING_ROLE` | `role` field missing | | 409 | - | User is already a workspace member | ```bash curl -X POST https://api.miosa.ai/api/v1/workspaces/{id}/members \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"user_id": "usr_def456", "role": "member"}' ``` ```typescript const record = await client.workspaceMembers.add('ws-uuid', { userId: 'usr_def456', role: 'member', }); ``` ```python record = client.workspace_members.add("ws-uuid", user_id="usr_def456", role="member") ``` ```go record, err := client.WorkspaceMembers.Add(ctx, "ws-uuid", miosa.AddWorkspaceMemberInput{ UserID: "usr_def456", Role: "member", }) ``` ```elixir {:ok, record} = Miosa.WorkspaceMembers.add(client, "ws-uuid", "usr_def456", "member") ``` ```java Map record = miosa.workspaceMembers().add("ws-uuid", "usr_def456", "member"); ``` --- ## Update Member Role **`PATCH /api/v1/workspaces/{id}/members/{user_id}`** Changes a workspace member's role. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `role` | string | Yes | New role: `owner`, `admin`, `member`, or `viewer` | ### Response - `200 OK` Updated workspace member record (same shape as the add response). ### Errors | Status | Code | Cause | |--------|------|-------| | 404 | - | User is not a member of this workspace | | 422 | `MISSING_ROLE` | `role` field missing | ```bash curl -X PATCH https://api.miosa.ai/api/v1/workspaces/{id}/members/{user_id} \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"role": "admin"}' ``` ```typescript const updated = await client.workspaceMembers.updateRole('ws-uuid', 'usr_def456', { role: 'admin', }); ``` ```python updated = client.workspace_members.update_role("ws-uuid", "usr_def456", role="admin") ``` ```go updated, err := client.WorkspaceMembers.UpdateRole(ctx, "ws-uuid", "usr_def456", miosa.UpdateWorkspaceMemberRoleInput{Role: "admin"}) ``` ```elixir {:ok, updated} = Miosa.WorkspaceMembers.update_role(client, "ws-uuid", "usr_def456", "admin") ``` ```java Map updated = miosa.workspaceMembers().updateRole("ws-uuid", "usr_def456", "admin"); ``` --- ## Remove Member **`DELETE /api/v1/workspaces/{id}/members/{user_id}`** Removes a user from the workspace. This does not remove them from the parent org. ### Response - `200 OK` ```json { "deleted": true } ``` ### Errors | Status | Code | Cause | |--------|------|-------| | 404 | - | User is not a member of this workspace | | 409 | `LAST_OWNER` | Cannot remove the last workspace owner - promote another member first | ```bash curl -X DELETE https://api.miosa.ai/api/v1/workspaces/{id}/members/{user_id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` ```typescript await client.workspaceMembers.remove('ws-uuid', 'usr_def456'); ``` ```python client.workspace_members.remove("ws-uuid", "usr_def456") ``` ```go err := client.WorkspaceMembers.Remove(ctx, "ws-uuid", "usr_def456") ``` ```elixir {:ok, _} = Miosa.WorkspaceMembers.remove(client, "ws-uuid", "usr_def456") ``` ```java miosa.workspaceMembers().remove("ws-uuid", "usr_def456"); ``` --- ## Common Errors | Status | Code | Cause | |--------|------|-------| | 403 | - | Workspace belongs to a different tenant | | 404 | - | Workspace or user not found | | 409 | `LAST_OWNER` | Refusing to remove the sole workspace owner | | 422 | `NOT_TENANT_MEMBER` | User must join the org before joining the workspace | | 422 | `MISSING_USER_ID` | `user_id` body field is required | | 422 | `MISSING_ROLE` | `role` body field is required | --- ## See also - [Workspace Invites API](/docs/api-reference/invites) - invite users by email (creates org membership on accept) - [Workspaces API](/docs/api-reference/workspaces) - workspace CRUD - [Error Codes](/docs/api-reference/errors) - full error reference --- # Network Policy API URL: https://miosa.ai/docs/api-reference/network-policy Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/network-policy Source: src/routes/docs/api-reference/network-policy/+page.md Description: API reference for configuring per-computer network access controls on MIOSA computers. Network policies control what external hosts and ports a computer can reach. Use them to sandbox untrusted workloads, enforce egress restrictions, or prevent AI agents from making unexpected outbound connections. Base path: `/api/v1/computers/{id}/network-policy` Network policies apply at the microVM level. Changing a policy takes effect within seconds without restarting the computer. --- ## Quick Start ```typescript const client = new Miosa(); // Restrict to only allow HTTPS to GitHub and PyPI await client.networkPolicy.update(computerId, { mode: 'allowlist', rules: [ { host: 'github.com', port: 443, protocol: 'tcp' }, { host: 'pypi.org', port: 443, protocol: 'tcp' }, { host: 'files.pythonhosted.org', port: 443, protocol: 'tcp' }, ], }); ``` ```bash curl -X PUT https://api.miosa.ai/api/v1/computers/{id}/network-policy \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "mode": "allowlist", "rules": [ { "host": "github.com", "port": 443, "protocol": "tcp" } ] }' ``` --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/computers/{id}/network-policy` | Get current network policy | | `PUT` | `/computers/{id}/network-policy` | Set network policy (replaces existing) | | `DELETE` | `/computers/{id}/network-policy` | Reset to default (unrestricted) | --- ## Get Network Policy **`GET /api/v1/computers/{id}/network-policy`** ### Response - `200 OK` ```json { "data": { "computer_id": "...", "mode": "unrestricted", "rules": [], "updated_at": "2026-04-11T00:00:00Z" } } ``` ### Policy Modes | Mode | Description | |------|-------------| | `unrestricted` | Default. All outbound traffic allowed | | `allowlist` | Only listed hosts/ports permitted | | `denylist` | All traffic permitted except listed rules | | `isolated` | All outbound blocked (DNS and MIOSA internal excluded) | ```bash curl https://api.miosa.ai/api/v1/computers/{id}/network-policy \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Set Network Policy **`PUT /api/v1/computers/{id}/network-policy`** Replaces the entire policy. Partial updates are not supported - send all rules on every PUT. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `mode` | string | Yes | `"unrestricted"`, `"allowlist"`, `"denylist"`, or `"isolated"` | | `rules` | array | No | Required when mode is `"allowlist"` or `"denylist"` | #### Rule Object | Field | Type | Required | Description | |-------|------|----------|-------------| | `host` | string | Yes | Hostname or CIDR block (e.g. `10.0.0.0/8`) | | `port` | integer | No | Port number. Omit to match all ports | | `protocol` | string | No | `"tcp"`, `"udp"`, or `"any"` (default) | ### Response - `200 OK` Updated policy object. ### Errors | Status | Error | Cause | |--------|-------|-------| | 400 | `rules required for allowlist/denylist mode` | Empty rules for a filtering mode | | 404 | `computer not found` | Computer does not exist | ```bash # Full isolation except for GitHub curl -X PUT https://api.miosa.ai/api/v1/computers/{id}/network-policy \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "mode": "allowlist", "rules": [ { "host": "github.com", "port": 443, "protocol": "tcp" }, { "host": "objects.githubusercontent.com", "port": 443, "protocol": "tcp" } ] }' ``` --- ## Reset to Default **`DELETE /api/v1/computers/{id}/network-policy`** Removes any custom policy. The computer reverts to unrestricted outbound access. ### Response - `200 OK` ```json { "data": { "computer_id": "...", "mode": "unrestricted" } } ``` ```bash curl -X DELETE https://api.miosa.ai/api/v1/computers/{id}/network-policy \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Common Recipes ### Sandbox an untrusted AI task ```python # Lock down before running untrusted code client.network_policy.update(computer_id, mode="isolated") # Run the task result = client.exec(computer_id, command="python3 /workspace/untrusted.py") # Restore after client.network_policy.delete(computer_id) ``` ### Allow only package registries for a build environment ```typescript const packageHosts = [ { host: 'registry.npmjs.org', port: 443, protocol: 'tcp' as const }, { host: 'pypi.org', port: 443, protocol: 'tcp' as const }, { host: 'files.pythonhosted.org', port: 443, protocol: 'tcp' as const }, { host: 'pkg.go.dev', port: 443, protocol: 'tcp' as const }, { host: 'proxy.golang.org', port: 443, protocol: 'tcp' as const }, ]; await client.networkPolicy.update(computerId, { mode: 'allowlist', rules: packageHosts, }); ``` ### Block a known malicious IP range ```typescript await client.networkPolicy.update(computerId, { mode: 'denylist', rules: [ { host: '185.220.0.0/16' }, ], }); ``` --- # OpenComputers API URL: https://miosa.ai/docs/api-reference/open-computers Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/open-computers Source: src/routes/docs/api-reference/open-computers/+page.md Description: Register and control your own machines (BYOC) via the MIOSA API - exec, files, tunnels, AI agents, inference clusters, and secrets. OpenComputers lets you register physical or virtual machines you already own (Mac, Linux, Windows) and control them through MIOSA's API - run commands, manage files, expose HTTP tunnels, dispatch AI agents, build inference clusters, and manage secrets. Base path: `/api/v1/opencomputers` Verbs supported: **GET** (list/show), **POST** (register/create/exec/dispatch), **PATCH** (update tags/tunnels), **DELETE** (revoke/cancel). Rate limit: 300 req/min per workspace. Exec jobs and agent sessions run asynchronously - poll or stream for results rather than holding the connection. OpenComputers hosts are registered by installing the `miosa-host` agent on the target machine. The **host key** returned on registration is shown exactly once - store it securely. --- ## Hosts ### List hosts **`GET /api/v1/opencomputers/hosts`** | Parameter | Type | Description | |-----------|------|-------------| | `page` | integer | Page number (default: 1) | | `per_page` | integer | Items per page (default: 20, max: 100) | ```json { "data": [ { "id": "host_abc123", "name": "my-mac", "region": "us-east", "status": "online", "tenant_id": "t_abc", "labels": { "env": "prod" }, "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z" } ], "meta": { "total": 1, "page": 1, "per_page": 20 } } ``` **Host statuses:** `pending` | `online` | `offline` | `error` | `revoked` --- ### Register a host **`POST /api/v1/opencomputers/hosts`** | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | Display name for the host | | `region` | string | No | Region label | | `labels` | object | No | Key-value metadata | ```json { "id": "host_abc123", "name": "my-mac", "host_key": "hk_xxxxxxxxxxxxxxxx", "status": "pending", "tenant_id": "t_abc", "labels": {}, "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z" } ``` `host_key` is returned **only once** at registration time. The host agent uses it to authenticate. Store it immediately - it cannot be retrieved later. --- ### Get a host **`GET /api/v1/opencomputers/hosts/{id}`** --- ### Revoke a host **`DELETE /api/v1/opencomputers/hosts/{id}`** Returns `204 No Content`. The host agent will no longer be able to authenticate. --- ### Host event stream (SSE) **`GET /api/v1/opencomputers/hosts/{id}/events`** Streams real-time lifecycle events from the host as Server-Sent Events. ``` event: status_change data: {"type":"status_change","host_id":"host_abc","data":{"status":"online"},"timestamp":"..."} ``` --- ## Jobs Run shell commands on a registered host and retrieve output. ### Run a job **`POST /api/v1/opencomputers/hosts/{id}/exec`** | Field | Type | Required | Description | |-------|------|----------|-------------| | `command` | string | Yes | Command to execute | | `args` | string[] | No | Arguments (passed separately from shell expansion) | | `env` | string[] | No | Environment variables in `KEY=VALUE` format | | `cwd` | string | No | Working directory | | `timeout` | integer | No | Timeout in seconds (default: 60) | ```json { "id": "job_xyz", "host_id": "host_abc123", "status": "completed", "command": "npm test", "args": [], "exit_code": 0, "stdout": "All tests passed.\n", "stderr": "", "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z", "completed_at": "2026-01-01T00:00:05Z" } ``` **Job statuses:** `queued` | `running` | `completed` | `failed` | `cancelled` --- ### List jobs **`GET /api/v1/opencomputers/hosts/{id}/exec`** --- ### Get a job **`GET /api/v1/opencomputers/hosts/{id}/exec/{job_id}`** --- ### Stream job output (SSE) **`GET /api/v1/opencomputers/hosts/{id}/exec/{job_id}/stream`** Streams stdout/stderr in real time while the job is running. --- ### Cancel a job **`DELETE /api/v1/opencomputers/hosts/{id}/exec/{job_id}`** Returns `204 No Content`. --- ## File System Manage files and directories on a registered host. | Endpoint | Method | Description | |----------|--------|-------------| | `/opencomputers/hosts/{id}/fs/list` | GET | List directory (`?path=`) | | `/opencomputers/hosts/{id}/fs/stat` | GET | Stat a path (`?path=`) | | `/opencomputers/hosts/{id}/fs/download` | GET | Download a file (`?path=`) | | `/opencomputers/hosts/{id}/fs/upload` | POST | Upload a file (multipart, `?path=`) | | `/opencomputers/hosts/{id}/fs/delete` | DELETE | Delete file/dir (`?path=`) | | `/opencomputers/hosts/{id}/fs/mkdir` | POST | Create directory (`{"path":"..."}`) | **List response:** ```json { "path": "/workspace/projects", "entries": [ { "name": "my-app", "path": "/workspace/projects/my-app", "size": 0, "is_dir": true, "modified_at": "2026-01-01T00:00:00Z" } ] } ``` --- ## Terminal ### Issue a terminal ticket **`POST /api/v1/opencomputers/hosts/{id}/terminal/ticket`** Returns a short-lived WebSocket authentication ticket. Connect to `ws_url` immediately using the ticket as a query parameter. ```json { "ticket": "tk_abc123", "ws_url": "wss://api.miosa.ai/opencomputers/ws/terminal?ticket=tk_abc123", "expires_at": "2026-01-01T00:00:30Z" } ``` --- ## Desktop (VNC) ### Issue a desktop ticket **`POST /api/v1/opencomputers/hosts/{id}/desktop/ticket`** Same shape as the terminal ticket. Connect to `ws_url` with the ticket to start a VNC session. --- ## Tunnels Expose local ports on the host over MIOSA-managed public URLs. ### List tunnels **`GET /api/v1/opencomputers/hosts/{id}/tunnels`** ### Create a tunnel **`POST /api/v1/opencomputers/hosts/{id}/tunnels`** | Field | Type | Required | Description | |-------|------|----------|-------------| | `target_port` | integer | Yes | Local port to expose | | `auth_mode` | string | No | `public` \| `tenant_only` \| `password` (default: `public`) | | `slug` | string | No | Custom slug for the public URL | ```json { "id": "tun_abc", "host_id": "host_abc123", "slug": "my-app-dev", "target_port": 3000, "auth_mode": "public", "public_url": "https://api.miosa.ai/t/my-app-dev", "enabled": true, "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z" } ``` ### Get / Update / Delete a tunnel **`GET /api/v1/opencomputers/hosts/{id}/tunnels/{tunnel_id}`** **`PATCH /api/v1/opencomputers/hosts/{id}/tunnels/{tunnel_id}`** | Field | Type | Description | |-------|------|-------------| | `target_port` | integer | Change target port | | `auth_mode` | string | Change auth mode | | `enabled` | boolean | Enable or disable the tunnel | **`DELETE /api/v1/opencomputers/hosts/{id}/tunnels/{tunnel_id}`** - `204 No Content` --- ## Agents Dispatch an AI agent to complete a task autonomously on the host. ### Dispatch an agent **`POST /api/v1/opencomputers/hosts/{id}/agent/dispatch`** | Field | Type | Required | Description | |-------|------|----------|-------------| | `task` | string | Yes | Natural-language task description | | `model_id` | string | No | Override the default model | | `max_turns` | integer | No | Maximum conversation turns (default: 20) | | `context` | object | No | Additional context key-value pairs | ```json { "id": "sess_abc", "host_id": "host_abc123", "task": "Run the test suite and fix any failing tests", "status": "pending", "max_turns": 20, "turns_used": 0, "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z", "completed_at": null, "error": null } ``` **Session statuses:** `pending` | `running` | `completed` | `failed` | `cancelled` ### List / Get sessions **`GET /api/v1/opencomputers/hosts/{id}/agent/sessions`** **`GET /api/v1/opencomputers/hosts/{id}/agent/sessions/{session_id}`** ### Stream agent events (SSE) **`GET /api/v1/opencomputers/hosts/{id}/agent/sessions/{session_id}/stream`** ### Cancel a session **`DELETE /api/v1/opencomputers/hosts/{id}/agent/sessions/{session_id}`** - `204 No Content` --- ## Inference Clusters Group multiple hosts to serve an LLM over an OpenAI-compatible endpoint. ### List clusters **`GET /api/v1/opencomputers/clusters`** ### Create a cluster **`POST /api/v1/opencomputers/clusters`** | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | Cluster name | | `model` | string | Yes | Model to serve (e.g. `llama3:70b`) | | `host_ids` | string[] | Yes | IDs of hosts in the cluster | ```json { "id": "cl_abc", "name": "my-cluster", "model": "llama3:70b", "slug": "my-cluster", "status": "active", "host_ids": ["host_abc123"], "inference_url": "https://api.miosa.ai/inference/my-cluster/v1", "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z" } ``` The `inference_url` is OpenAI-compatible: ``` POST {inference_url}/chat/completions ``` ### Get / Start / Stop / Delete **`GET /api/v1/opencomputers/clusters/{id}`** **`POST /api/v1/opencomputers/clusters/{id}/start`** **`POST /api/v1/opencomputers/clusters/{id}/stop`** **`DELETE /api/v1/opencomputers/clusters/{id}`** - `204 No Content` --- ## Secrets Store encrypted key-value secrets accessible to the host agent at runtime. ### Tenant-scoped secrets | Endpoint | Method | Description | |----------|--------|-------------| | `/opencomputers/secrets` | GET | List tenant secrets | | `/opencomputers/secrets` | POST | Create a secret | | `/opencomputers/secrets/{id}` | PATCH | Update value or description | | `/opencomputers/secrets/{id}` | DELETE | Delete a secret | ### Host-scoped secrets | Endpoint | Method | Description | |----------|--------|-------------| | `/opencomputers/hosts/{id}/secrets` | GET | List host secrets | | `/opencomputers/hosts/{id}/secrets` | POST | Create a host secret | | `/opencomputers/hosts/{id}/secrets/{secret_id}` | DELETE | Delete a host secret | **Create request:** ```json { "name": "GITHUB_TOKEN", "value": "ghp_xxx", "description": "CI token" } ``` **Response (value is never returned):** ```json { "id": "sec_abc", "name": "GITHUB_TOKEN", "description": "CI token", "host_id": null, "tenant_id": "t_abc", "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z" } ``` --- ## SDK Examples ```typescript const miosa = new Miosa({ apiKey: process.env.MIOSA_API_KEY }); // Register a host - save host_key immediately const host = await miosa.openComputers.hosts.create({ name: 'my-mac' }); console.log('Host key (save this!):', host.host_key); // Run a command const job = await miosa.openComputers.jobs.run(host.id, { command: 'npm test', }); console.log('Exit code:', job.exit_code); console.log('Output:', job.stdout); // Expose a local dev server const tunnel = await miosa.openComputers.tunnels.create(host.id, { target_port: 3000, }); console.log('Public URL:', tunnel.public_url); // Dispatch an AI agent const session = await miosa.openComputers.agents.dispatch(host.id, { task: 'Run the test suite and fix any failing tests', max_turns: 30, }); console.log('Session:', session.id, session.status); ``` ```python client = miosa.Miosa(api_key="msk_u_...") # Register a host host = client.open_computers.hosts.create( miosa.resources.open_computers.types.HostCreateParams(name="my-mac") ) print("Host key:", host.host_key) # Run a command from miosa.resources.open_computers.types import JobRunParams job = client.open_computers.jobs.run(host.id, JobRunParams(command="npm test")) print(f"Exit code: {job.exit_code}, stdout: {job.stdout}") # Create a tunnel from miosa.resources.open_computers.types import TunnelCreateParams tunnel = client.open_computers.tunnels.create(host.id, TunnelCreateParams(target_port=3000)) print("Public URL:", tunnel.public_url) ``` ```go client := miosa.NewClient(os.Getenv("MIOSA_API_KEY")) // Register a host host, err := client.OpenComputers.Hosts.Create(ctx, miosa.CreateHostInput{ Name: "my-mac", }) if err != nil { log.Fatal(err) } fmt.Println("Host key:", *host.HostKey) // Run a command job, err := client.OpenComputers.Jobs.Run(ctx, host.ID, miosa.RunJobInput{ Command: "npm test", }) if err != nil { log.Fatal(err) } fmt.Printf("Exit code: %d\nStdout: %s\n", *job.ExitCode, *job.Stdout) // Create a tunnel tunnel, err := client.OpenComputers.Tunnels.Create(ctx, host.ID, miosa.CreateTunnelInput{ TargetPort: 3000, }) fmt.Println("Public URL:", tunnel.PublicURL) ``` ```elixir client = Miosa.client("msk_u_...") # Register a host {:ok, host} = Miosa.OpenComputers.Hosts.create(client, %{name: "my-mac"}) IO.puts("Host key: #{host["host_key"]}") # Run a command {:ok, job} = Miosa.OpenComputers.Jobs.run(client, host["id"], %{command: "npm test"}) IO.puts("Exit code: #{job["exit_code"]}") # Create a tunnel {:ok, tunnel} = Miosa.OpenComputers.Tunnels.create(client, host["id"], %{target_port: 3000}) IO.puts("Public URL: #{tunnel["public_url"]}") ``` ```java MiosaClient miosa = new MiosaClient(System.getenv("MIOSA_API_KEY")); // Register a host HostData host = miosa.openComputers().hosts() .create(new CreateHostParams("my-mac")); System.out.println("Host key: " + host.hostKey); // Run a command JobData job = miosa.openComputers().jobs() .run(host.id, new RunJobParams("npm test")); System.out.printf("Exit code: %d%nStdout: %s%n", job.exitCode, job.stdout); // Create a tunnel TunnelData tunnel = miosa.openComputers().tunnels() .create(host.id, new CreateTunnelParams(3000)); System.out.println("Public URL: " + tunnel.publicUrl); ``` --- ## Common Errors | Status | Code | Cause | |--------|------|-------| | 404 | `NOT_FOUND` | Host or resource does not exist in this tenant | | 403 | `FORBIDDEN` | Authenticated but not authorized | | 400 | `INVALID_ID` | Path parameter is not a valid ID | | 409 | - | Host is offline; command cannot be dispatched | | 502 | - | Host agent is unreachable | --- ## See also - [Computers API](/docs/api-reference/computers/) - managed MIOSA VMs vs. BYOC hosts - [Exec API](/docs/api-reference/exec/) - exec on managed computers - [Regions](/docs/api-reference/regions/) - region slugs accepted on host creation - [Error Codes](/docs/api-reference/errors/) - complete error code reference --- # Preview Tokens API URL: https://miosa.ai/docs/api-reference/preview-tokens Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/preview-tokens Source: src/routes/docs/api-reference/preview-tokens/+page.md Description: Generate short-lived signed tokens for iframe embedding without exposing an API key. Preview tokens (`mp_*`) are short-lived signed credentials that allow you to embed a running sandbox in an ` ``` The token is verified by the sandbox proxy on every request during the token's lifetime. Once it expires, the iframe will show an access-denied page. --- # Projects API URL: https://miosa.ai/docs/api-reference/projects Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/projects Source: src/routes/docs/api-reference/projects/+page.md Description: API reference for creating and managing projects inside MIOSA workspaces. A **Project** is the app, website, lead magnet, document, workflow, or customer build inside a workspace. Projects own sandboxes, computers, deployments, databases, storage buckets, volumes, functions, jobs, auth configuration, integrations, and custom domains. Base path: `/api/v1/projects` If a resource create call includes `project_slug`, `project_name`, or `external_project_id`, MIOSA resolves or creates the project before creating the resource. You can also create projects explicitly with this API. ## Endpoints | Method | Path | Description | |---|---|---| | `GET` | `/projects` | List projects for the organization | | `POST` | `/projects` | Create a project inside a workspace | | `GET` | `/projects/{id}` | Get one project | | `PATCH` | `/projects/{id}` | Update a project | | `GET` | `/projects/{id}/preview-domain` | Read the project preview domain | | `PUT` | `/projects/{id}/preview-domain` | Set the project preview domain | | `DELETE` | `/projects/{id}/preview-domain` | Clear the project preview domain | | `GET` | `/projects/{id}/preview-domain/verify` | Check DNS readiness | | `GET` | `/workspaces/{id}/projects` | List projects in one workspace | --- ## List Projects **`GET /api/v1/projects`** ### Query Parameters | Parameter | Type | Description | |---|---|---| | `workspace_id` | UUID | Limit results to one workspace | ### Response - `200 OK` ```json { "data": [ { "id": "660e8400-e29b-41d4-a716-446655440001", "tenant_id": "tnt_abc123", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "external_workspace_id": "clinic_123", "external_project_id": "project_789", "name": "Lead Magnet", "slug": "lead-magnet", "description": null, "metadata": {}, "settings": {}, "created_at": "2026-05-18T10:00:00Z", "updated_at": "2026-05-18T10:00:00Z" } ], "total": 1 } ``` ```bash curl "https://api.miosa.ai/api/v1/projects?workspace_id=550e8400-e29b-41d4-a716-446655440000" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Create a Project **`POST /api/v1/projects`** A project must belong to a workspace. Supply one of `workspace_id`, `workspace_slug`, or `external_workspace_id`. ### Request Body | Field | Type | Required | Description | |---|---|---|---| | `workspace_id` | UUID | Conditional | Existing MIOSA workspace ID | | `workspace_slug` | string | Conditional | Existing workspace slug | | `external_workspace_id` | string | Conditional | Your external workspace/customer ID | | `name` | string | Yes | Human-readable project name | | `slug` | string | No | URL-safe identifier. Auto-derived from `name` if omitted. | | `external_project_id` | string | No | Your project/app/document ID | | `description` | string | No | Optional description | | `metadata` | object | No | Caller metadata stored on the project | ### Example ```bash curl -X POST https://api.miosa.ai/api/v1/projects \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "workspace_slug": "dr-smith-clinic", "name": "Lead Magnet", "slug": "lead-magnet", "external_workspace_id": "clinic_123", "external_project_id": "project_789" }' ``` ### Response - `201 Created` Full project object. ### Errors | Status | Code | Cause | |---|---|---| | 400 | `WORKSPACE_REQUIRED` | No `workspace_id`, `workspace_slug`, or `external_workspace_id` was supplied | | 404 | `WORKSPACE_NOT_FOUND` | Workspace selector did not resolve inside the organization | | 422 | `VALIDATION_FAILED` | Name, slug, or external project ID failed validation | --- ## Get a Project **`GET /api/v1/projects/{id}`** Returns one project if it belongs to the authenticated organization. ```bash curl https://api.miosa.ai/api/v1/projects/{id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Update a Project **`PATCH /api/v1/projects/{id}`** ### Request Body | Field | Type | Description | |---|---|---| | `name` | string | New display name | | `slug` | string | New URL-safe slug, unique inside the workspace | | `description` | string | New description (`null` to clear) | | `external_project_id` | string | Your project/app/document ID | | `external_workspace_id` | string | Your customer/workspace ID | | `metadata` | object | Replacement metadata map | | `settings` | object | Replacement settings map. Prefer the preview-domain endpoint for domain changes. | ```bash curl -X PATCH https://api.miosa.ai/api/v1/projects/{id} \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Summer Lead Magnet"}' ``` --- ## Use Projects on Resource Create Most create endpoints accept the same ownership fields: ```json { "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "project_id": "660e8400-e29b-41d4-a716-446655440001", "external_workspace_id": "clinic_123", "external_project_id": "project_789" } ``` You can use slugs instead of UUIDs: ```json { "workspace_slug": "dr-smith-clinic", "workspace_name": "Dr. Smith Clinic", "project_slug": "lead-magnet", "project_name": "Lead Magnet" } ``` This applies to sandboxes, computers, deployments, databases, storage buckets, volumes, edge functions, cron jobs, project auth, and integrations. Derived records inherit ownership automatically. --- ## Project Preview Domain **`PUT /api/v1/projects/{id}/preview-domain`** Sets a project-level base domain for generated URLs. This overrides the workspace preview domain and organization preview domain for resources in this project. ```bash curl -X PUT https://api.miosa.ai/api/v1/projects/{id}/preview-domain \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"preview_domain":"program.drsmithclinic.com"}' ``` Response: ```json { "scope": "project", "id": "660e8400-e29b-41d4-a716-446655440001", "preview_domain": "program.drsmithclinic.com", "effective_domain": "program.drsmithclinic.com", "status": "pending_dns", "dns_status": "pending", "url_examples": { "default_preview": "https://.program.drsmithclinic.com", "port_preview": "https://3000-.sandbox.program.drsmithclinic.com", "deployment": "https://.program.drsmithclinic.com" } } ``` Required DNS records: | Record type | Name | Value | |---|---|---| | `CNAME` | `*` | `proxy.miosa.ai` | | `CNAME` | `*.sandbox` | `proxy.miosa.ai` | Verify: ```bash curl https://api.miosa.ai/api/v1/projects/{id}/preview-domain/verify \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` Clear and inherit from the workspace/organization fallback: ```bash curl -X DELETE https://api.miosa.ai/api/v1/projects/{id}/preview-domain \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` ## See also - [Workspaces API](/docs/api-reference/workspaces/) - parent resource - [Ownership and Attribution](/docs/platform/attribution/) - how IDs flow to usage and events - [Sandboxes API](/docs/api-reference/sandboxes/) - create resources inside a project - [Deployments API](/docs/api-reference/deployments/) - publish projects to URLs --- # Quotas API URL: https://miosa.ai/docs/api-reference/quotas Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/quotas Source: src/routes/docs/api-reference/quotas/+page.md Description: Set per-end-user resource limits for white-label multi-tenant deployments. Quotas let you cap how many sandboxes an individual end-user of your product can create and run. They are keyed by `external_user_id` - the same opaque identifier you pass when creating sandboxes on behalf of your users. Base path: `/api/v1/quotas/external/{external_user_id}` Quotas are enforced at sandbox creation time. Setting a quota does not affect sandboxes already running. Deleting a quota removes all limits for that user. --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/quotas/external/{external_user_id}` | Get quota and current usage | | `PUT` | `/api/v1/quotas/external/{external_user_id}` | Create or update a quota | | `DELETE` | `/api/v1/quotas/external/{external_user_id}` | Remove quota (unlimited) | --- ## Get Quota **`GET /api/v1/quotas/external/{external_user_id}`** Returns the configured limits and live usage counters for the user. ### Auth ``` Authorization: Bearer msk_... ``` ### Response - `200 OK` ```json { "data": { "external_user_id": "user_clinic_42", "limits": { "max_sandboxes": 10, "max_concurrent": 3, "max_storage_gb": 50, "max_credit_cents": 5000 }, "usage": { "total_sandboxes": 4, "concurrent": 1 } } } ``` | Field | Type | Description | |-------|------|-------------| | `limits.max_sandboxes` | integer\|null | Lifetime sandbox creation limit | | `limits.max_concurrent` | integer\|null | Max sandboxes running simultaneously | | `limits.max_storage_gb` | integer\|null | Total attached storage cap in GiB | | `limits.max_credit_cents` | integer\|null | Spend cap in cents (platform credits) | | `usage.total_sandboxes` | integer | Total sandboxes created by this user | | `usage.concurrent` | integer | Currently running sandboxes | A `null` limit means no cap is applied for that dimension. --- ## Set Quota **`PUT /api/v1/quotas/external/{external_user_id}`** Creates or replaces the quota. All limit fields are optional - omit a field to leave it uncapped. ### Request Body ```json { "max_sandboxes": 10, "max_concurrent": 3, "max_storage_gb": 50, "max_credit_cents": 5000 } ``` ### Response - `200 OK` Same shape as GET response. --- ## Delete Quota **`DELETE /api/v1/quotas/external/{external_user_id}`** Removes all limits for the user. Future sandbox creation is governed only by your tenant plan. ### Response - `204 No Content` --- ## Errors | Status | Code | Cause | |--------|------|-------| | 404 | `not_found` | No quota exists for this `external_user_id` (GET/DELETE) | | 422 | validation errors | A limit value is non-integer or negative | --- ## Examples ```bash # Set curl -X PUT https://api.miosa.ai/api/v1/quotas/external/user_clinic_42 \ -H "Authorization: Bearer msk_live_..." \ -H "Content-Type: application/json" \ -d '{"max_sandboxes": 10, "max_concurrent": 3}' # Get curl https://api.miosa.ai/api/v1/quotas/external/user_clinic_42 \ -H "Authorization: Bearer msk_live_..." # Remove curl -X DELETE https://api.miosa.ai/api/v1/quotas/external/user_clinic_42 \ -H "Authorization: Bearer msk_live_..." ``` ```python client = miosa.Miosa() # Set quota when user signs up client.quotas.set("user_clinic_42", max_sandboxes=10, max_concurrent=3) # Check quota + usage quota = client.quotas.get("user_clinic_42") print(quota.usage.concurrent) # Remove limits client.quotas.delete("user_clinic_42") ``` ```typescript const client = new Miosa(); // Set await client.quotas.set('user_clinic_42', { maxSandboxes: 10, maxConcurrent: 3, maxStorageGb: 50, }); // Get const quota = await client.quotas.get('user_clinic_42'); console.log(quota.usage.concurrent); // Remove await client.quotas.delete('user_clinic_42'); ``` --- # Regions API URL: https://miosa.ai/docs/api-reference/regions Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/regions Source: src/routes/docs/api-reference/regions/+page.md Description: List available regions and pin Computers and Sandboxes to a specific region. MIOSA Computers and Sandboxes run in geographic regions. Every Computer (and every Sandbox, since a Sandbox is a lightweight Computer flavor) is created in exactly one region and stays there for its lifetime. Base path: `/api/v1/regions` --- ## Overview - **3 regions are live today.** All have equal status - none are preview, none are restricted. - **Default region** when you don't specify one: `us-west-la`. - **All VM sizes** (XS, S, M, L, XL) are available in every region for both Computers and Sandboxes. - **Latency** is roughly equidistant for US users - pick by data residency or proximity preference rather than performance. - **No multi-region resources.** A Computer lives in exactly one region. To run workloads in multiple regions, create multiple Computers. --- ## Available Regions | Slug | Location | City | Status | |------------------|-------------------|------------------|--------| | `us-west-la` | US West | Los Angeles, CA | Live | | `us-east-ny` | US East | New York, NY | Live | | `us-mia` | US East 2 | Miami | Live | The region slug is what you pass on create and what comes back on read - every Computer and Sandbox response includes a `region` field, e.g. `"region": "us-west-la"`. --- ## Specify a region on create Pass `region` in the request body when creating a Computer or Sandbox. If omitted, the default (`us-west-la`) is used. ```python computer = client.computers.create( name="ny-build-box", size="M", region="us-east-ny", ) print(computer.region) # "us-east-ny" ``` ```typescript const computer = await client.computers.create({ name: 'ny-build-box', size: 'M', region: 'us-east-ny', }); console.log(computer.region); // "us-east-ny" ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "ny-build-box", "size": "M", "region": "us-east-ny" }' ``` The same `region` field works on `POST /api/v1/sandboxes`. ### Errors | Status | Error | Cause | |--------|----------------------|------------------------------------------------| | 400 | `invalid_region` | Slug not in the list above | | 409 | `region_unavailable` | Transient capacity issue - retry or pick another | --- ## Default region behavior If `region` is omitted from the create request: - Computers default to **`us-west-la`**. - Sandboxes default to **`us-west-la`**. - The response still includes the resolved `region` so you always know where the resource lives. You can change your tenant's default region in dashboard settings - that override applies to API calls that omit `region`. --- ## List regions **`GET /api/v1/regions`** Returns the regions available to your tenant. Useful for region pickers in UIs. ### Response - `200 OK` ```json { "regions": [ { "slug": "us-west-la", "name": "US West (Los Angeles)", "city": "Los Angeles", "country": "US", "status": "live", "default": true }, { "slug": "us-east-ny", "name": "US East (New York)", "city": "New York", "country": "US", "status": "live", "default": false }, { "slug": "us-mia", "name": "US East 2 (Miami)", "city": "Miami", "country": "US", "status": "live", "default": false } ] } ``` ### Response Fields | Field | Type | Description | |------------|---------|-------------------------------------------------------| | `slug` | string | Stable identifier - pass this as `region` on create | | `name` | string | Human-readable label (safe to render in UIs) | | `city` | string | City the region is hosted in | | `country` | string | Two-letter ISO country code | | `status` | string | Always `"live"` today - reserved for future states | | `default` | boolean | `true` for exactly one region (the tenant default) | ```bash curl https://api.miosa.ai/api/v1/regions \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` The list endpoint is unauthenticated-friendly - you can use a short-lived ticket or a public token to populate region pickers without exposing your `msk_*` key. --- ## FAQ ### Why these 3 regions? We picked Los Angeles, New York, and Miami to cover the US west coast, east coast, and central corridor. The three sites are roughly equidistant for the median US user, and they each sit on independent network and power footprints - so a regional outage at one doesn't take the others down. ### How do I pick a region? Pick by **data residency** first. If you have a customer or compliance requirement that pins data to a coast, that decides for you. If you have no residency requirement, pick by **proximity to your users or your CI runners** - shorter physical distance means lower RTT for VNC streaming and PTY round-trips. Compute performance itself is identical across regions. ### Can a Computer span multiple regions? No. A Computer (or Sandbox) lives in exactly one region for its lifetime. To run workloads in multiple regions: - Create one Computer per region. - Use OpenComputers federation if you want a single logical workspace across them (see [OpenComputers](/docs/api-reference/open-computers/)). There's no live migration between regions today. ### Will more regions ship? Yes - EU and APAC regions are on the roadmap. When they ship they'll appear in `GET /api/v1/regions` with the same `"status": "live"` shape. Watch the [changelog](/docs/changelog/) for announcements. --- ## See also - [Computers API](/docs/api-reference/computers/) - pass `region` on create - [Sandboxes API](/docs/api-reference/sandboxes/) - sandbox region support - [OpenComputers](/docs/api-reference/open-computers/) - federate BYOC hosts across regions --- # API Reference / Releases URL: https://miosa.ai/docs/api-reference/releases Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/releases Source: src/routes/docs/api-reference/releases/+page.md Description: The immutable build artifact - static tarball or dynamic rootfs / OCI image, sha256-keyed. First-class `deployment_releases` records and their dedicated endpoints arrive in Phase 2B of the deployment refactor. Today, release content is referenced via the `artifact_uri` / `artifact_sha256` fields on `deployment_versions`. This page documents the steady-state target. A **Release** is the physical artifact produced by a build - what a [Version](/docs/api-reference/versions/) references. See [Releases](/docs/deploy/releases/) for the conceptual model. ## Endpoints (Phase 2B) ```http GET /api/v1/releases/:id GET /api/v1/deployments/:id/versions/:version_id/release ``` Releases are not typically created directly - they're produced by [Publish](/docs/api-reference/deployments/#publish). The endpoints above are for inspection and audit. ## Release shape ```json { "id": "rel_...", "deployment_version_id": "ver_...", "service_id": "svc_...", "tenant_id": "...", "kind": "static", "storage_uri": "s3://miosa-releases/dep_xyz/rel_abc.tar.zst", "sha256": "a5e6f0c1...", "size_bytes": 184320, "start_command": null, "port": null, "metadata": { "build_log_uri": "s3://...", "build_duration_ms": 8420 }, "created_at": "2026-05-14T18:19:48Z" } ``` For dynamic releases: `kind: "oci" | "rootfs"`, `start_command` and `port` populated. ## Immutability Releases are content-addressed by sha256. A release with sha256 `abc...` is always the same bytes. This is what makes [Rollback](/docs/deploy/rollback/) trivial and horizontal scaling identical across instances. A release is never mutated. To "change" it, publish a new version, which produces a new release. ## Garbage collection Static release artifacts older than the retention window (default 30 days for archived versions) are eligible for GC. The version row remains for audit; the artifact itself may be removed from object storage. If you attempt to rollback to a version whose artifact has been GC'd, MIOSA returns `410 Gone`. Active versions (any deployment's `active_version_id`) are never GC'd. ## Permissions | Action | Scope | |---|---| | Get | `deployments:read` | ## See also - [Versions](/docs/api-reference/versions/) - parent resource - [Deployments](/docs/api-reference/deployments/) - top-level resource - [Releases](/docs/deploy/releases/) - conceptual model --- # Sandbox Env API URL: https://miosa.ai/docs/api-reference/sandbox-env Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/sandbox-env Source: src/routes/docs/api-reference/sandbox-env/+page.md Description: CRUD for per-sandbox environment variables, encrypted at rest. The sandbox env API manages a key-value store of environment variables attached to a sandbox. Variables are encrypted at rest and injected into the sandbox process environment on start. The API never returns plaintext values - only key names and metadata. Base path: `/api/v1/sandboxes/{id}/env` Variable names must match `[A-Z][A-Z0-9_]*` - uppercase letters, digits, and underscores, starting with a letter. This mirrors POSIX shell environment variable conventions. --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/sandboxes/{id}/env` | List all env vars (names only, no values) | | `PUT` | `/api/v1/sandboxes/{id}/env` | Bulk set or update env vars | | `DELETE` | `/api/v1/sandboxes/{id}/env/{key}` | Delete a single env var | --- ## List Env Vars **`GET /api/v1/sandboxes/{id}/env`** Returns all variable names. Values are never returned. ### Auth Requires `sandboxes:read` scope. ``` Authorization: Bearer msk_... ``` ### Response - `200 OK` ```json { "data": [ { "key": "DATABASE_URL", "encrypted": true }, { "key": "API_SECRET", "encrypted": true }, { "key": "LOG_LEVEL", "encrypted": false } ] } ``` --- ## Set Env Vars **`PUT /api/v1/sandboxes/{id}/env`** Bulk upsert. Existing keys are updated; new keys are created. Keys not present in the request are left unchanged. ### Auth Requires `sandboxes:write` scope. ``` Authorization: Bearer msk_... ``` ### Request Body ```json { "vars": [ { "key": "DATABASE_URL", "value": "postgres://...", "encrypted": true }, { "key": "LOG_LEVEL", "value": "debug" } ] } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `vars[].key` | string | Yes | Variable name (`[A-Z][A-Z0-9_]*`) | | `vars[].value` | string | Yes | Variable value | | `vars[].encrypted` | boolean | No | Store encrypted at rest (default: `true`) | ### Response - `200 OK` ```json { "data": [ { "key": "DATABASE_URL", "encrypted": true }, { "key": "LOG_LEVEL", "encrypted": false } ] } ``` --- ## Delete Env Var **`DELETE /api/v1/sandboxes/{id}/env/{key}`** Removes a single variable. ### Response - `200 OK` ```json { "deleted": true, "name": "LOG_LEVEL" } ``` --- ## Errors | Status | Code | Cause | |--------|------|-------| | 400 | `INVALID_ID` | Sandbox ID is not a valid UUID | | 400 | `invalid vars` | `vars` missing or a key fails name validation | | 404 | `NOT_FOUND` | Sandbox does not exist or wrong tenant | | 404 | `NOT_FOUND` | Key does not exist (on DELETE) | --- ## Examples ```bash # Set curl -X PUT https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/env \ -H "Authorization: Bearer msk_live_..." \ -H "Content-Type: application/json" \ -d '{ "vars": [ {"key": "DATABASE_URL", "value": "postgres://user:pass@host/db"}, {"key": "LOG_LEVEL", "value": "info"} ] }' # List curl https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/env \ -H "Authorization: Bearer msk_live_..." # Delete curl -X DELETE https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/env/LOG_LEVEL \ -H "Authorization: Bearer msk_live_..." ``` ```python client = miosa.Miosa() # Set client.sandboxes.env.set("sbx_abc123", { "DATABASE_URL": "postgres://user:pass@host/db", "LOG_LEVEL": "info", }) # List (names only) keys = client.sandboxes.env.list("sbx_abc123") # Delete one client.sandboxes.env.delete("sbx_abc123", "LOG_LEVEL") ``` ```typescript const client = new Miosa(); // Set await client.sandboxes.env.set('sbx_abc123', { DATABASE_URL: 'postgres://user:pass@host/db', LOG_LEVEL: 'info', }); // List const { data } = await client.sandboxes.env.list('sbx_abc123'); // Delete await client.sandboxes.env.delete('sbx_abc123', 'LOG_LEVEL'); ``` --- # Sandbox Files (Advanced) API URL: https://miosa.ai/docs/api-reference/sandbox-files-advanced Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/sandbox-files-advanced Source: src/routes/docs/api-reference/sandbox-files-advanced/+page.md Description: File tree traversal, batch write, and SSE file-watch for IDE-grade sandbox file operations. These advanced file operations extend the core [Files API](/docs/api-reference/files) with IDE-oriented capabilities: recursive tree listing, atomic batch writes, and a live file-change event stream. Base path: `/api/v1/sandboxes/{id}/files` These routes use `:sandbox_id` as the path parameter internally but are accessed as `/sandboxes/{id}/files/...` - the `{id}` segment is the sandbox UUID. --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/sandboxes/{id}/files/tree` | Recursive file tree | | `POST` | `/api/v1/sandboxes/{id}/files/write-many` | Batch write multiple files | | `GET` | `/api/v1/sandboxes/{id}/files/watch` | SSE stream of file-change events | --- ## File Tree **`GET /api/v1/sandboxes/{id}/files/tree`** Returns a recursive directory listing up to a configurable depth. Useful for populating an IDE file-explorer panel. ### Auth ``` Authorization: Bearer msk_... ``` ### Query Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `path` | string | `/workspace` | Root directory to list | | `depth` | integer | `3` | Max traversal depth (1-10) | | `include_hidden` | boolean | `false` | Include dot-files and dot-directories | ### Response - `200 OK` ```json { "data": { "path": "/workspace", "type": "directory", "children": [ { "name": "src", "path": "/workspace/src", "type": "directory", "children": [ { "name": "index.ts", "path": "/workspace/src/index.ts", "type": "file", "size": 1842, "modified_at": "2026-05-26T09:12:01Z" } ] }, { "name": "package.json", "path": "/workspace/package.json", "type": "file", "size": 512, "modified_at": "2026-05-25T18:00:00Z" } ] } } ``` --- ## Batch Write **`POST /api/v1/sandboxes/{id}/files/write-many`** Writes multiple files in a single request. All writes are applied atomically from the guest's perspective - either all succeed or an error is returned for the failing file. Maximum 50 files per request. Files larger than 90 KB should be uploaded individually via `POST /files/write`. ### Request Body ```json { "files": [ { "path": "/workspace/src/index.ts", "content": "export const hello = () => 'world';\n", "encoding": "utf8" }, { "path": "/workspace/src/utils.ts", "content": "ZXhwb3J0IGZ1bmN0aW9uIG5vb3AoKSB7fQo=", "encoding": "base64" } ] } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `files[].path` | string | Yes | Absolute path inside the sandbox | | `files[].content` | string | Yes | File content (UTF-8 text or base64-encoded binary) | | `files[].encoding` | string | No | `"utf8"` (default) or `"base64"` | ### Response - `200 OK` ```json { "data": { "written": 2, "paths": [ "/workspace/src/index.ts", "/workspace/src/utils.ts" ] } } ``` ### Errors | Status | Code | Cause | |--------|------|-------| | 400 | `INVALID_BODY` | `files` is missing or not an array | | 400 | `TOO_MANY_FILES` | More than 50 files in one request | | 400 | `INVALID_PATH` | A path contains a null byte or traverses outside the sandbox | --- ## File Watch (SSE) **`GET /api/v1/sandboxes/{id}/files/watch`** Streams file-system change events as SSE. Useful for syncing editor state in real time. ### Auth ``` Authorization: Bearer msk_... ``` (Browser `EventSource` cannot set this header - use a server-side client or a scoped session token.) ### Query Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `path` | string | `/workspace` | Directory to watch | ### Response - `200 OK` (`text/event-stream`) ``` id: wev_001 event: file.changed data: {"path":"/workspace/src/index.ts","event":"modified","ts":"2026-05-26T10:14:23Z"} id: wev_002 event: file.changed data: {"path":"/workspace/src/new-file.ts","event":"created","ts":"2026-05-26T10:14:24Z"} : heartbeat ``` **Event values for `data.event`:** | Value | Trigger | |-------|---------| | `created` | New file or directory | | `modified` | File content changed | | `deleted` | File or directory removed | | `renamed` | File moved or renamed | --- ## Examples ```bash # File tree (depth 2) curl "https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/files/tree?depth=2" \ -H "Authorization: Bearer msk_live_..." # Batch write curl -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/files/write-many \ -H "Authorization: Bearer msk_live_..." \ -H "Content-Type: application/json" \ -d '{ "files": [ {"path": "/workspace/hello.ts", "content": "console.log(\"hello\");\n"} ] }' # File watch (SSE) curl -N -H "Authorization: Bearer msk_live_..." \ -H "Accept: text/event-stream" \ "https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/files/watch?path=/workspace/src" ``` ```python client = miosa.Miosa() # Tree tree = client.sandboxes.files.tree("sbx_abc123", path="/workspace", depth=3) # Batch write client.sandboxes.files.write_many("sbx_abc123", [ {"path": "/workspace/index.ts", "content": "export {};"}, {"path": "/workspace/utils.ts", "content": "export function noop() {}"}, ]) ``` ```typescript const client = new Miosa(); // File tree const tree = await client.sandboxes.files.tree('sbx_abc123', { path: '/workspace', depth: 3, includeHidden: false, }); // Batch write await client.sandboxes.files.writeMany('sbx_abc123', [ { path: '/workspace/index.ts', content: 'export {};' }, { path: '/workspace/utils.ts', content: 'export function noop() {}' }, ]); ``` --- # Sandbox Processes API URL: https://miosa.ai/docs/api-reference/sandbox-processes Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/sandbox-processes Source: src/routes/docs/api-reference/sandbox-processes/+page.md Description: Start, monitor, and stream logs for long-running background processes inside sandboxes. The processes API lets you launch long-running background commands inside a sandbox and inspect their output - useful for starting dev servers, watchers, or any process that runs beyond a single exec call. Base path: `/api/v1/sandboxes/{id}/processes` Processes are managed by the sandbox runtime and survive for the lifetime of the sandbox. They are not preserved across snapshots or forks. --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `POST` | `/api/v1/sandboxes/{id}/processes` | Start a process | | `GET` | `/api/v1/sandboxes/{id}/processes` | List processes | | `GET` | `/api/v1/sandboxes/{id}/processes/{pid}` | Get process status | | `DELETE` | `/api/v1/sandboxes/{id}/processes/{pid}` | Stop a process | | `GET` | `/api/v1/sandboxes/{id}/processes/{pid}/logs` | Fetch recent log lines | | `GET` | `/api/v1/sandboxes/{id}/processes/{pid}/stream` | SSE live log stream | --- ## Start a Process **`POST /api/v1/sandboxes/{id}/processes`** ### Auth ``` Authorization: Bearer msk_... ``` ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `cmd` | string | Yes | Command to run (passed to `/bin/sh -c`) | | `cwd` | string | No | Working directory (default: `/workspace`) | | `env` | object | No | Additional environment variables | | `name` | string | No | Human-readable label | ```json { "cmd": "npm run dev -- --port 3000", "cwd": "/workspace", "name": "dev-server" } ``` ### Response - `201 Created` ```json { "data": { "pid": "proc_01hwxyz...", "name": "dev-server", "cmd": "npm run dev -- --port 3000", "cwd": "/workspace", "status": "running", "started_at": "2026-05-26T10:14:23Z", "exited_at": null, "exit_code": null } } ``` --- ## List Processes **`GET /api/v1/sandboxes/{id}/processes`** ### Response - `200 OK` ```json { "data": [ { "pid": "proc_01hwxyz...", "name": "dev-server", "status": "running", "started_at": "2026-05-26T10:14:23Z", "exited_at": null, "exit_code": null } ] } ``` --- ## Get Process Status **`GET /api/v1/sandboxes/{id}/processes/{pid}`** ### Response - `200 OK` ```json { "data": { "pid": "proc_01hwxyz...", "name": "dev-server", "cmd": "npm run dev -- --port 3000", "cwd": "/workspace", "status": "exited", "started_at": "2026-05-26T10:14:23Z", "exited_at": "2026-05-26T10:22:01Z", "exit_code": 0 } } ``` **Process status values:** | Status | Description | |--------|-------------| | `running` | Process is alive | | `exited` | Process exited normally | | `killed` | Process was stopped via DELETE | | `error` | Process failed to start | --- ## Stop a Process **`DELETE /api/v1/sandboxes/{id}/processes/{pid}`** Sends `SIGTERM` followed by `SIGKILL` after a grace period. ### Response - `204 No Content` --- ## Fetch Logs **`GET /api/v1/sandboxes/{id}/processes/{pid}/logs`** Returns the last N lines of stdout+stderr. ### Query Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `tail` | integer | `200` | Number of lines to return | ### Response - `200 OK` ```json { "data": { "lines": [ " > Local: http://localhost:3000", " > Network: http://0.0.0.0:3000", " ready in 743ms" ] } } ``` --- ## Live Log Stream (SSE) **`GET /api/v1/sandboxes/{id}/processes/{pid}/stream`** Streams stdout+stderr as SSE. Uses `text/event-stream` - requires Bearer token auth. ``` id: log_001 event: log data: {"line":" ready in 743ms","ts":"2026-05-26T10:14:24Z"} : heartbeat ``` --- ## Errors | Status | Code | Cause | |--------|------|-------| | 400 | `INVALID_ID` | Sandbox ID is not a valid UUID | | 404 | `NOT_FOUND` | Sandbox or process does not exist | | 409 | `SANDBOX_NOT_RUNNING` | Sandbox must be in `running` state to start processes | | 500 | `PROCESS_START_FAILED` | Runtime failed to spawn the process | --- ## Examples ```bash # Start curl -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/processes \ -H "Authorization: Bearer msk_live_..." \ -H "Content-Type: application/json" \ -d '{"cmd": "npm run dev -- --port 3000", "name": "dev-server"}' # List curl https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/processes \ -H "Authorization: Bearer msk_live_..." # Logs curl "https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/processes/proc_01hwxyz/logs?tail=50" \ -H "Authorization: Bearer msk_live_..." # Stop curl -X DELETE https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/processes/proc_01hwxyz \ -H "Authorization: Bearer msk_live_..." ``` ```python client = miosa.Miosa() # Start a dev server proc = client.sandboxes.processes.start( "sbx_abc123", cmd="npm run dev -- --port 3000", name="dev-server", ) # Wait a moment then check logs time.sleep(3) logs = client.sandboxes.processes.logs("sbx_abc123", proc.pid, tail=50) for line in logs: print(line) # Stop client.sandboxes.processes.stop("sbx_abc123", proc.pid) ``` ```typescript const client = new Miosa(); // Start const proc = await client.sandboxes.processes.start('sbx_abc123', { cmd: 'npm run dev -- --port 3000', name: 'dev-server', }); // Poll status let status = proc.status; while (status === 'running') { await new Promise(r => setTimeout(r, 2000)); const updated = await client.sandboxes.processes.get('sbx_abc123', proc.pid); status = updated.status; } // Logs const { data } = await client.sandboxes.processes.logs('sbx_abc123', proc.pid); console.log(data.lines.join('\n')); ``` --- # Sandbox Shares API URL: https://miosa.ai/docs/api-reference/sandbox-shares Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/sandbox-shares Source: src/routes/docs/api-reference/sandbox-shares/+page.md Description: Create and manage public share URLs for sandboxes. Sandbox shares are publicly accessible URLs that give read-only access to a running sandbox. Unlike preview tokens, share URLs are not scoped to an iframe and do not require the recipient to hold any credentials. Share tokens use the prefix `ms_` followed by a base64url-encoded payload. The public URL pattern is: ``` https://{sandbox-id}.sandboxes.miosa.ai?ms=ms_ ``` --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `POST` | `/api/v1/sandboxes/{id}/shares` | Create a share URL | | `GET` | `/api/v1/sandboxes/{id}/shares` | List active shares | | `DELETE` | `/api/v1/sandboxes/{id}/shares/{share_id}` | Revoke a share | --- ## Create a Share **`POST /api/v1/sandboxes/{id}/shares`** Creates a public share link. The `share_url` field is safe to distribute - it contains the token as a query parameter. ### Auth ``` Authorization: Bearer msk_... ``` ### Request Body | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | `expires_in` | integer | No | `3600` | TTL in seconds | | `scope` | string | No | `"read"` | Must be `"read"` (only supported value) | ```json { "expires_in": 86400 } ``` ### Response - `201 Created` ```json { "share_id": "shr_01hx9m7n3k...", "share_url": "https://sbx-abc123.sandboxes.miosa.ai?ms=ms_eyJ...", "expires_at": "2026-05-27T12:00:00Z", "scope": "read" } ``` | Field | Type | Description | |-------|------|-------------| | `share_id` | string | Opaque share identifier used to revoke | | `share_url` | string | Publicly accessible URL with token embedded | | `expires_at` | string | ISO 8601 expiry, or `null` for no expiry | | `scope` | string | Always `"read"` | --- ## List Shares **`GET /api/v1/sandboxes/{id}/shares`** Returns all active (non-revoked, non-expired) shares for the sandbox. ### Response - `200 OK` ```json { "data": [ { "share_id": "shr_01hx9m7n3k...", "share_url": "https://sbx-abc123.sandboxes.miosa.ai?ms=ms_eyJ...", "expires_at": "2026-05-27T12:00:00Z", "scope": "read" } ] } ``` --- ## Revoke a Share **`DELETE /api/v1/sandboxes/{id}/shares/{share_id}`** Immediately invalidates the share. Any visitor using the share URL will receive a 403 after revocation. ### Response - `204 No Content` No response body. --- ## Errors | Status | Code | Cause | |--------|------|-------| | 404 | `sandbox_not_found` | Sandbox does not exist or wrong tenant | | 403 | `forbidden` | Tenant mismatch | | 404 | `not_found` | Share ID does not exist (on DELETE) | | 422 | `invalid_scope` | `scope` value is not `"read"` | --- ## Examples ```bash # Create curl -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/shares \ -H "Authorization: Bearer msk_live_..." \ -H "Content-Type: application/json" \ -d '{"expires_in": 86400}' # List curl https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/shares \ -H "Authorization: Bearer msk_live_..." # Revoke curl -X DELETE https://api.miosa.ai/api/v1/sandboxes/sbx_abc123/shares/shr_01hx9m7n3k \ -H "Authorization: Bearer msk_live_..." ``` ```python client = miosa.Miosa() # Create share = client.sandboxes.shares.create("sbx_abc123", expires_in=86400) print(share.share_url) # List shares = client.sandboxes.shares.list("sbx_abc123") # Revoke client.sandboxes.shares.delete("sbx_abc123", share.share_id) ``` ```typescript const client = new Miosa(); // Create const share = await client.sandboxes.shares.create('sbx_abc123', { expiresIn: 86400, }); console.log(share.shareUrl); // List const shares = await client.sandboxes.shares.list('sbx_abc123'); // Revoke await client.sandboxes.shares.delete('sbx_abc123', share.shareId); ``` --- # Sandbox Templates API URL: https://miosa.ai/docs/api-reference/sandbox-templates Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/sandbox-templates Source: src/routes/docs/api-reference/sandbox-templates/+page.md Description: Browse the template marketplace and launch sandboxes from pre-built images. Sandbox templates are pre-built images with toolchains, dependencies, and configuration baked in. Launching from a template is faster than installing packages at runtime and ensures reproducible environments. Templates are read-only marketplace items managed by MIOSA. To build your own custom templates, see the [Templates guide](/docs/develop/templates). --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/sandbox-templates` | List available templates | | `GET` | `/api/v1/sandbox-templates/{id}` | Get a specific template | | `POST` | `/api/v1/sandboxes` | Create sandbox from a template (via `template_id`) | --- ## List Templates **`GET /api/v1/sandbox-templates`** Returns all templates available to your tenant, including official MIOSA templates and any custom templates you have built. ### Auth ``` Authorization: Bearer msk_... ``` ### Response - `200 OK` ```json { "data": [ { "id": "tpl_debian12", "name": "Debian 12 (Bookworm)", "slug": "debian-12-sandbox-v8", "description": "Debian Bookworm with Node 20, Python 3.11, Go 1.22, and Rust 1.78 pre-installed.", "tags": ["debian", "node", "python", "go", "official"], "memory_mb": 4096, "vcpus": 2, "created_at": "2026-05-11T00:00:00Z", "is_official": true }, { "id": "tpl_nextjs", "name": "Next.js 14", "slug": "nextjs-14", "description": "Next.js 14 App Router starter with Tailwind CSS.", "tags": ["nextjs", "react", "typescript"], "memory_mb": 4096, "vcpus": 2, "created_at": "2026-04-01T00:00:00Z", "is_official": true } ] } ``` | Field | Type | Description | |-------|------|-------------| | `id` | string | Template ID | | `name` | string | Human-readable template name | | `slug` | string | Short identifier used in the `image` field | | `description` | string | What is pre-installed | | `tags` | string[] | Searchable tags | | `memory_mb` | integer | Default sandbox memory allocation | | `vcpus` | integer | Default vCPU count | | `is_official` | boolean | MIOSA-maintained template | --- ## Get Template **`GET /api/v1/sandbox-templates/{id}`** Returns a single template by ID. ### Response - `200 OK` Same shape as a single item from the list response. --- ## Create Sandbox from Template Use the standard sandbox creation endpoint with the template `slug` as the `image` field: **`POST /api/v1/sandboxes`** ```json { "image": "debian-12-sandbox-v8", "memory_mb": 4096, "vcpus": 2 } ``` See the [Sandboxes API](/docs/api-reference/sandboxes) for the full sandbox creation reference. --- ## Errors | Status | Code | Cause | |--------|------|-------| | 404 | `not_found` | Template ID does not exist | --- ## Examples ```bash # List templates curl https://api.miosa.ai/api/v1/sandbox-templates \ -H "Authorization: Bearer msk_live_..." # Get one template curl https://api.miosa.ai/api/v1/sandbox-templates/tpl_debian12 \ -H "Authorization: Bearer msk_live_..." # Create sandbox from template curl -X POST https://api.miosa.ai/api/v1/sandboxes \ -H "Authorization: Bearer msk_live_..." \ -H "Content-Type: application/json" \ -d '{"image": "debian-12-sandbox-v8"}' ``` ```python client = miosa.Miosa() # Browse templates templates = client.sandbox_templates.list() for t in templates: print(t.slug, "-", t.description) # Launch from template sandbox = client.sandboxes.create(image="debian-12-sandbox-v8") ``` ```typescript const client = new Miosa(); // Browse const templates = await client.sandboxTemplates.list(); for (const t of templates) { console.log(t.slug, '-', t.description); } // Launch const sandbox = await client.sandboxes.create({ image: 'debian-12-sandbox-v8', }); ``` --- # Sandboxes API URL: https://miosa.ai/docs/api-reference/sandboxes Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/sandboxes Source: src/routes/docs/api-reference/sandboxes/+page.md Description: API reference for creating, managing, and executing code in MIOSA sandboxes - ephemeral isolated microVMs. Sandboxes are ephemeral isolated microVMs that restore from a pre-seeded snapshot, accept exec and file operations, expose previews, and are billed by compute/runtime usage. They are designed for AI-agent code execution workloads, artifact generation, and app previews. Base path: `/api/v1/sandboxes` All endpoints require `Authorization: Bearer <api_key>`. Get a key from the [MIOSA dashboard](https://app.miosa.ai/settings/api-keys). For higher-level access, use the official SDKs: [Python](/docs/sdks/python/), [TypeScript](/docs/sdks/typescript/), [Go](/docs/sdks/go/), [Java](/docs/sdks/java/), [Elixir](/docs/sdks/elixir/). --- ## List Sandbox Templates **`GET /api/v1/sandbox-templates`** Returns the platform-managed sandbox template catalog. Pass one of these IDs as `template_id` when creating a sandbox. The catalog describes what your app should run: workdir, install command, start command, preview port, readiness probe, and artifact paths. ```bash curl https://api.miosa.ai/api/v1/sandbox-templates \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` ### Response - `200 OK` ```json { "default_template_id": "miosa-sandbox", "data": [ { "id": "nextjs", "name": "Next.js", "image_id": "miosa-sandbox", "category": "web", "cpu_count": 2, "memory_mb": 2048, "disk_mb": 4096, "workdir": "/workspace", "preview_port": 3000, "install_command": "npm install", "start_command": "npm run dev -- --hostname 0.0.0.0 --port 3000", "readiness_probe": { "type": "http", "url": "http://127.0.0.1:3000" } } ] } ``` Use `GET /api/v1/sandbox-templates/{id}` to fetch one template. Add `?include_aliases=true` on the list endpoint to include compatibility aliases such as `python-3.12`, `react-vite`, and `static`. --- ## Sandbox Template BuildSpec Phase 4 introduces the public BuildSpec contract for reusable sandbox templates. The API validates and normalizes BuildSpecs, persists tenant-owned template records, queues template build records, and runs a builder worker that turns the normalized BuildSpec into a root filesystem artifact. ### Get the BuildSpec schema **`GET /api/v1/sandbox-templates/build-spec`** ```bash curl https://api.miosa.ai/api/v1/sandbox-templates/build-spec \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` ### Validate a BuildSpec **`POST /api/v1/sandbox-templates/validate`** ```bash curl -X POST https://api.miosa.ai/api/v1/sandbox-templates/validate \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "build_spec": { "from": "node:22-bookworm", "vcpu": 2, "memoryMib": 2048, "diskMib": 8192, "steps": [ {"run": "corepack enable"}, {"workdir": "/workspace"} ], "env": {"NODE_ENV": "development"}, "workdir": "/workspace", "user": "root", "startCmd": "pnpm dev --host 0.0.0.0 --port 3000", "readyCmd": "curl -f http://127.0.0.1:3000", "previewPort": 3000, "artifactPaths": ["/workspace"] } }' ``` Response: ```json { "valid": true, "build_spec": { "from": "node:22-bookworm", "vcpu": 2, "memoryMib": 2048, "diskMib": 8192, "steps": [{"run": "corepack enable"}, {"workdir": "/workspace"}], "env": {"NODE_ENV": "development"}, "workdir": "/workspace", "user": "root", "startCmd": "pnpm dev --host 0.0.0.0 --port 3000", "readyCmd": "curl -f http://127.0.0.1:3000", "previewPort": 3000, "artifactPaths": ["/workspace"] } } ``` Validation errors use stable codes: | Code | Meaning | |------|---------| | `REQUIRED` | A required field such as `from` is missing. | | `UNSUPPORTED_BASE_IMAGE` | Alpine/distroless bases are rejected for public agent templates. | | `OUT_OF_RANGE` | Resource values are outside tenant-safe defaults. | | `INVALID_INTEGER` | Numeric fields are not integers. | | `INVALID_PORT` | Preview port is outside `1..65535`. | | `INVALID_PATH` | Workdir or artifact path is not absolute. | | `INVALID_STEP` | A build step is not a supported object. | ### Create a custom template **`POST /api/v1/sandbox-templates`** ```bash curl -X POST https://api.miosa.ai/api/v1/sandbox-templates \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Agent Web App", "slug": "agent-web-app", "description": "Reusable Node app sandbox", "build_spec": { "from": "node:22-bookworm", "vcpu": 2, "memoryMib": 2048, "diskMib": 8192, "startCmd": "pnpm dev --host 0.0.0.0 --port 3000", "readyCmd": "curl -f http://127.0.0.1:3000", "previewPort": 3000, "artifactPaths": ["/workspace"] } }' ``` ### Queue a template build **`POST /api/v1/sandbox-templates/{template_id}/builds`** ```bash curl -X POST https://api.miosa.ai/api/v1/sandbox-templates/tpl_123/builds \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` Build records start in `queued`. The builder worker moves them through `building`, `snapshotting`, `certifying`, `distributing`, and then `ready` or `failed`. `ready` means the rootfs exists, a memory snapshot has been seeded, certification passed, and the rootfs/snapshot artifacts have been published to the fleet artifact source. Each compute host reconciles or fetches the artifacts before it boots that custom template. After that, pass the template slug to `POST /api/v1/sandboxes` as `template_id`. If a custom template exists but is not ready, sandbox creation returns `409 TEMPLATE_NOT_READY`. If snapshot seeding fails, the build fails with `SNAPSHOT_FAILED`; if certification fails, it fails with `CERTIFICATION_FAILED`. ### Read template builds ```bash curl https://api.miosa.ai/api/v1/sandbox-templates/tpl_123/builds \ -H "Authorization: Bearer $MIOSA_API_KEY" curl https://api.miosa.ai/api/v1/sandbox-template-builds/build_123 \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` ### Observe and control template builds ```bash curl https://api.miosa.ai/api/v1/sandbox-template-builds/build_123/logs \ -H "Authorization: Bearer $MIOSA_API_KEY" # Browsers/EventSource cannot send Authorization headers. Issue a short-lived # SSE ticket first, then pass it as ?ticket=... curl -X POST https://api.miosa.ai/api/v1/auth/sse-ticket \ -H "Authorization: Bearer $MIOSA_API_KEY" curl -N "https://api.miosa.ai/api/v1/sandbox-template-builds/build_123/logs/stream?ticket=$SSE_TICKET" \ -H "Accept: text/event-stream" curl -X POST https://api.miosa.ai/api/v1/sandbox-template-builds/build_123/cancel \ -H "Authorization: Bearer $MIOSA_API_KEY" curl -X POST https://api.miosa.ai/api/v1/sandbox-template-builds/build_123/retry \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` `/logs` returns persisted lifecycle events from build metadata. `/logs/stream` opens an SSE stream that first replays persisted events, then emits live `build_event` messages until the build reaches `ready`, `failed`, or `cancelled`. Example SSE payload: ```text event: build_event data: {"event":"snapshotting","state":"snapshotting","at":"2026-05-13T10:00:00Z"} ``` Retry is allowed for `failed` or `cancelled` builds. Cancel is accepted for non-terminal builds and records `BUILD_CANCELLED`. ### BuildSpec fields | Field | Type | Description | |-------|------|-------------| | `from` | string | OCI base image. Debian/Ubuntu/Bookworm bases are recommended. | | `vcpu` | integer | Template-owned vCPU count. | | `memoryMib` | integer | Template-owned memory in MiB. | | `diskMib` | integer | Template-owned root disk in MiB. | | `steps` | array | Ordered build steps. Each step may define `run`, `workdir`, or `env`. | | `env` | object | Runtime-default environment variables. | | `workdir` | string | Runtime working directory. Defaults to `/workspace`. | | `user` | string | Runtime user. Defaults to `root`. | | `startCmd` | string | Long-running command used by `/template/start`. | | `readyCmd` | string | Readiness command for the started app. | | `previewPort` | integer | Default preview port for `/expose` and template start. | | `artifactPaths` | array | Paths returned by the artifact manifest. | --- ## Create a Sandbox **`POST /api/v1/sandboxes`** Spawns a new sandbox VM. The API returns the sandbox record immediately after the create request is accepted; poll `GET /api/v1/sandboxes/{id}` or subscribe to events until `state` is `running` and `ready` is `true`. The current production alias, `miosa-sandbox`, points at `miosa-sandbox`. The latest verified production run measured the standard client-owned lifecycle at **947 ms p50 command-ready TTI** and the production run path at **512 ms p50 command-ready TTI** across 100 sandboxes at concurrency 10. See [Benchmarks](/docs/benchmarks/) for methodology. ### Auth Bearer token required. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `template_id` | string | No | Boot template. Defaults to `miosa-sandbox`. See available templates below. | | `cpu_count` | integer | No | vCPU count. Default `1`, default cap `4`. | | `memory_mb` | integer | No | RAM in MB. Default `1024`, default cap `8192`. | | `disk_mb` | integer | No | Disk in MB. Default `3072`, default cap `10240`. | | `timeout_sec` | integer | No | Max wall-clock seconds before force-destroy. Day-long/always-on use is available when billing and policy allow it. | | `env` | object | No | Key-value env vars injected at boot. | | `metadata` | object | No | Arbitrary caller-supplied metadata stored on the record. | | `auto_start` | boolean | No | If `true`, MIOSA starts the selected template after the sandbox reaches `running`. Generated-app platforms usually keep this false, write files first, then call `/template/start`. | | `workspace_id` | UUID | No | Existing MIOSA workspace that owns the sandbox. Defaults to the organization default workspace. | | `workspace_slug` | string | No | Existing or auto-created workspace slug. | | `workspace_name` | string | No | Workspace display name if auto-created. | | `project_id` | UUID | No | Existing MIOSA project that owns the sandbox. Defaults to the workspace default project. | | `project_slug` | string | No | Existing or auto-created project slug inside the workspace. | | `project_name` | string | No | Project display name if auto-created. | | `external_workspace_id` | string | No | Your customer/account/workspace ID. | | `external_user_id` | string | No | Your end-user ID. | | `external_project_id` | string | No | Your project/app/document ID. | **Available templates:** | `template_id` | Description | |---------------|-------------| | `miosa-sandbox` | Stable production alias for the current sandbox image. | | `nextjs` | Next.js app preview profile. | | `vite-react` | Vite React app preview profile. | | `python` | Python script/artifact generation profile. | | `streamlit` | Streamlit data app preview profile. | | `gradio` | Gradio ML/demo app preview profile. | | `static-html` | Static HTML/CSS/JS preview profile. | ### Request Headers | Header | Description | |--------|-------------| | `Idempotency-Key` | Client-generated key (UUID recommended). Same key within 24 h returns the existing sandbox instead of creating a new one. | ### Response - `201 Created` ```json { "id": "sbx_01j9xr2t4fk8me3n5q", "tenant_id": "tnt_abc123", "owner_id": "usr_def456", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "project_id": "660e8400-e29b-41d4-a716-446655440001", "external_workspace_id": "clinic_123", "external_user_id": "dr-smith-456", "external_project_id": "project_789", "template_id": "miosa-sandbox", "image_id": "miosa-sandbox", "state": "provisioning", "ready": false, "cpu_count": 1, "memory_mb": 1024, "disk_size_mb": 3072, "boot_path": null, "boot_ms": null, "preview_url": "https://sbx01j9x.sandbox.miosa.app", "timeout_sec": 300, "total_runtime_sec": null, "metadata": {}, "created_at": "2026-04-25T10:00:00Z", "started_at": null, "ready_at": null, "destroyed_at": null } ``` ### Errors | Status | Code | Cause | |--------|------|-------| | 400 | `INVALID_TEMPLATE` | `template_id` is not a recognized template. | | 402 | `INSUFFICIENT_CREDITS` | Not enough credits to provision the VM. | | 409 | `SANDBOX_LIMIT_EXCEEDED` | Tenant has reached the concurrent sandbox limit (default 10). | | 422 | `VALIDATION_ERROR` | Invalid field values (for example, a resource request above the tenant's plan cap). | --- ## Create and Run **`POST /api/v1/sandboxes/run`** Creates a sandbox, waits for it to become `running`, executes the first command, and returns the sandbox record plus the command result in one request. Use this for agent loops where the first useful action is "create a sandbox and run code now." This is the production fast path used by MIOSA's current sandbox benchmark: **512 ms p50**, **992 ms p95**, **1.300 s p99** command-ready TTI across 100 sandboxes at concurrency 10. ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/run \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: run-2026-06-09-001" \ -d '{ "template_id": "miosa-sandbox", "command": "python3 -c \"print(1+1)\"", "cpu_count": 1, "memory_mb": 1024, "timeout_sec": 300, "wait_timeout_ms": 30000, "metadata": { "agent_run": "abc123" } }' ``` ### Request Body `/run` accepts the same sandbox creation fields as `POST /api/v1/sandboxes`, plus: | Field | Type | Required | Description | |-------|------|----------|-------------| | `command` | string | Yes | First command to execute after the sandbox reaches `running`. | | `cwd` | string | No | Working directory for the command. Defaults to `/workspace`. | | `timeout` | integer | No | Command timeout in milliseconds. | | `wait_timeout_ms` | integer | No | Max time to wait for sandbox readiness before returning `504 SANDBOX_READY_TIMEOUT`. | ### Response - `201 Created` ```json { "data": { "id": "sbx_01j9xr2t4fk8me3n5q", "state": "running", "ready": true, "template_id": "miosa-sandbox", "cpu_count": 1, "memory_mb": 1024, "boot_path": "warm", "boot_ms": 34 }, "exec": { "stdout": "2\n", "stderr": "", "exit_code": 0 }, "timings": { "server_wait_and_exec_ms": 512 } } ``` ### Errors | Status | Code | Cause | |--------|------|-------| | 400 | `MISSING_PARAM` | `command` is missing. | | 400 | `INVALID_TEMPLATE` | `template_id` is not recognized. | | 402 | `INSUFFICIENT_CREDITS` | Not enough credits to provision the VM. | | 409 | `SANDBOX_LIMIT_EXCEEDED` | Tenant has reached the concurrent sandbox limit. | | 409 | `SANDBOX_NOT_RUNNING` | The sandbox failed to reach a running state for exec. | | 502 | `SANDBOX_BOOT_FAILED` | VM boot failed before the command could run. | | 502 | `AGENT_UNAVAILABLE` | The sandbox agent was not reachable for exec. | | 504 | `SANDBOX_READY_TIMEOUT` | The sandbox did not become ready before `wait_timeout_ms`. | --- ## Start a Template App **`POST /api/v1/sandboxes/{id}/template/start`** After your platform writes generated files into `/workspace`, call this endpoint to run the selected template lifecycle. MIOSA runs the template install command, launches the start command in the background, stores PID/log paths, and returns the preview URL. ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/{id}/template/start \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"install":true}' ``` Response: ```json { "data": { "status": "started", "template_id": "nextjs", "workdir": "/workspace", "preview_port": 3000, "preview_url": "https://abc12345.sandbox.miosa.app", "logs_path": "/tmp/miosa-run/template.log", "pid_path": "/tmp/miosa-run/template.pid", "artifact_paths": ["/workspace"] } } ``` You can override `install_command`, `start_command`, `port`, or `workdir` in the request body when your generated project needs a custom command. --- ## Get Artifacts **`GET /api/v1/sandboxes/{id}/artifacts`** Returns the template artifact contract and current lifecycle metadata. ```bash curl https://api.miosa.ai/api/v1/sandboxes/{id}/artifacts \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` The response includes `preview.url`, `preview.port`, artifact paths, and template lifecycle log/PID paths. ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "template_id": "miosa-sandbox", "workspace_slug": "dr-smith-clinic", "workspace_name": "Dr. Smith Clinic", "project_slug": "lead-magnet", "project_name": "Lead Magnet", "external_workspace_id": "clinic_123", "external_user_id": "dr-smith-456", "external_project_id": "project_789", "memory_mb": 1024, "env": {"MY_VAR": "hello"} }' ``` --- ## List Sandboxes **`GET /api/v1/sandboxes`** Returns all sandboxes belonging to the authenticated tenant. ### Query Parameters | Parameter | Type | Description | |---|---|---| | `workspace_id` | UUID | Filter to one MIOSA workspace | | `project_id` | UUID | Filter to one MIOSA project | | `external_workspace_id` | string | Filter by your customer/account ID | | `external_user_id` | string | Filter by your end-user ID | | `external_project_id` | string | Filter by your project/app/document ID | | `state` | string | Filter by lifecycle state: `provisioning`, `running`, `paused`, `destroyed`, `error`. | ### Auth Bearer token required. ### Response - `200 OK` ```json { "data": [ { "id": "sbx_01j9xr2t4fk8me3n5q", "template_id": "miosa-sandbox", "state": "running", "cpu_count": 1, "memory_mb": 1024, "created_at": "2026-04-25T10:00:00Z" } ] } ``` ```bash curl "https://api.miosa.ai/api/v1/sandboxes?state=running" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Get a Sandbox **`GET /api/v1/sandboxes/{id}`** ### Auth Bearer token required. ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | `id` | string | Sandbox ID. | ### Response - `200 OK` Full sandbox object (same shape as the create response with current `state`). ### Errors | Status | Code | Cause | |--------|------|-------| | 404 | `NOT_FOUND` | Sandbox does not exist or belongs to a different tenant. | ```bash curl https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Destroy a Sandbox **`DELETE /api/v1/sandboxes/{id}`** Terminates the VM immediately and settles billing. ### Auth Bearer token required. ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | `id` | string | Sandbox ID. | ### Response - `200 OK` ```json { "id": "sbx_01j9xr2t4fk8me3n5q", "state": "destroyed", "total_runtime_sec": 42 } ``` ### Errors | Status | Code | Cause | |--------|------|-------| | 404 | `NOT_FOUND` | Sandbox does not exist or belongs to a different tenant. | ```bash curl -X DELETE https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Execute a Command **`POST /api/v1/sandboxes/{id}/exec`** Runs a shell command inside the sandbox. Blocks until the process exits. ### Auth Bearer token required. ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | `id` | string | Sandbox ID. Must be in `running` state. | ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `command` | string | Yes | Shell command to execute. | | `timeout` | integer | No | Per-command timeout in seconds. Default `30`, max `300`. | | `working_dir` | string | No | Working directory inside the VM. Default `/root`. | | `env` | object | No | Env vars for this invocation only. | ### Response - `200 OK` ```json { "data": { "sandbox_id": "sbx_01j9xr2t4fk8me3n5q", "stdout": "2\n", "stderr": "", "exit_code": 0 } } ``` ### Errors | Status | Code | Cause | |--------|------|-------| | 409 | `SANDBOX_NOT_RUNNING` | Sandbox is not in `running` state. | | 504 | - | Command timed out. | ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/exec \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command": "python3 -c \"print(1+1)\"", "timeout": 30}' ``` --- ## Stream Command Output **`POST /api/v1/sandboxes/{id}/exec/stream`** Runs a command and streams stdout/stderr events as Server-Sent Events. Use this for installer output, long-running agent tasks, and build logs that should show progress before the command exits. Request body is the same as `/exec`. ```bash curl -N -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/exec/stream \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Accept: text/event-stream" \ -H "Content-Type: application/json" \ -d '{"command":"npm install && npm run build","working_dir":"/workspace","timeout":300}' ``` Example events: ```text event: stdout data: {"line":"added 342 packages"} event: exit data: {"exit_code":0} ``` --- ## Open a Terminal Session **`POST /api/v1/sandboxes/{id}/terminal`** Creates a PTY session inside a running sandbox and returns a short-lived stream token for browser WebSocket clients. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `cols` | integer | No | Initial terminal width. Defaults to `80`. | | `rows` | integer | No | Initial terminal height. Defaults to `24`. | | `shell` | string | No | Shell command. Defaults to login bash when available. | ### Response - `201 Created` ```json { "session_id": "52d1df5a0a3a0018", "ws_url": "wss://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/terminal/stream", "stream_auth": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9...", "stream_auth_expires_at": "2026-05-13T12:30:00Z" } ``` Open the WebSocket with: ```text wss://api.miosa.ai/api/v1/sandboxes/{id}/terminal/stream?session_id={session_id}&cols=120&rows=32&token={stream_auth} ``` Clients should send raw terminal input as binary frames. Resize events are sent as JSON text frames: ```json {"type":"resize","cols":120,"rows":32} ``` Delete the session when finished: ```bash curl -X DELETE https://api.miosa.ai/api/v1/sandboxes/{id}/terminal/{session_id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Expose a Preview Port **`POST /api/v1/sandboxes/{id}/expose`** Returns a public, tenant-aware preview URL for a server running inside the sandbox. Use this when an agent generated a Vite/Next/FastAPI/Flask app and started it on a local port. Inside the sandbox, bind dev servers to `0.0.0.0`, not `localhost`: ```bash npm run dev -- --host 0.0.0.0 --port 5173 python -m http.server 8000 --bind 0.0.0.0 ``` ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `port` | integer | No | Port inside the sandbox to expose. If omitted, MIOSA uses the template lifecycle preview port when available. Must be `1` through `65535` when provided. | ### Response - `200 OK` ```json { "url": "https://5173-sbx01j9x.sandbox.miosa.app" } ``` If the tenant has a white-label preview domain configured, the same endpoint returns that domain instead of the platform default. Default app ports such as `3000`, `5173`, `8080`, `8000`, and `80` may be returned as `https://{slug}.sandbox.{domain}`; non-default ports use `https://{port}-{slug}.sandbox.{domain}`. ### Errors | Status | Code | Cause | |--------|------|-------| | 400 | `MISSING_PARAM` | `port` was omitted and the sandbox template has no default preview port. | | 409 | `SANDBOX_NOT_RUNNING` | Sandbox is not in `running` state. | | 422 | `INVALID_PORT` | Port is outside `1` through `65535`. | ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/expose \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"port": 5173}' ``` For static artifacts such as PDFs, images, Markdown, CSV, or ZIP files, write them under `/workspace` and download them through the files API. For web apps, start a server and expose the port. --- ## Promote a Sandbox to a Deployment **`POST /api/v1/sandboxes/{id}/deploy`** Promotes a running sandbox into a persistent deployment URL. This is the publish step for generated-app platforms: the sandbox remains the runtime behind the deployment route, its auto-destroy timer is cancelled, and the deployment is marked `running` against the sandbox VM IP and preview port. Use this after your generated app is serving successfully through `/template/start` or `/expose`. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | Human-readable deployment name. Used to create the deployment slug. | | `port` | integer | No | Runtime port to route. Defaults to the template lifecycle preview port, then `80`. | | `domain` | string | No | Custom domain to attach. | | `custom_domain` | string | No | Alias for `domain`, used by frontend clients. | ### Response - `201 Created` ```json { "deployment_id": "dep_01j9xr2t4fk8me3n5q", "url": "https://my-app-a1b2c3.acme.miosa.app", "state": "running" } ``` Managed deployment URLs are tenant-scoped: `https://{deployment-slug}.{tenant-slug}.miosa.app`. MIOSA persists the sandbox runtime target on the deployment and reconciles running routes, so a temporary proxy restart or admin API outage can be repaired from the database. ### Errors | Status | Code | Cause | |--------|------|-------| | 400 | `MISSING_PARAM` | `name` was omitted. | | 409 | `SANDBOX_NOT_RUNNING` | Sandbox is not in `running` state. | | 409 | `SANDBOX_RUNTIME_UNAVAILABLE` | Sandbox is marked running but has no VM IP yet. | | 422 | `INVALID_PORT` | Port is outside `1` through `65535`. | | 422 | `VALIDATION_ERROR` | Deployment record validation failed. | ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/deploy \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"my-app","port":8000}' ``` --- ## Upload a File **`POST /api/v1/sandboxes/{id}/files`** Writes a file to the sandbox filesystem. Two request formats are accepted. ### Auth Bearer token required. ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | `id` | string | Sandbox ID. Must be in `running` state. | ### Request Body - JSON | Field | Type | Required | Description | |-------|------|----------|-------------| | `path` | string | Yes | Absolute destination path inside the sandbox. | | `content` | string | Yes | Base64-encoded file content. | ```json { "path": "/workspace/script.py", "content": "cHJpbnQoJ2hlbGxvJyk=" } ``` ### Request Body - Multipart Alternatively, send `multipart/form-data` with fields: | Field | Description | |-------|-------------| | `path` | Absolute destination path. | | `file` | File part containing the raw content. | ### Response - `200 OK` ```json { "data": { "sandbox_id": "sbx_01j9xr2t4fk8me3n5q", "path": "/workspace/script.py", "size": 16 } } ``` ### Errors | Status | Code | Cause | |--------|------|-------| | 409 | `SANDBOX_NOT_RUNNING` | Sandbox is not in `running` state. | | 413 | - | File exceeds the 100 MB upload limit. | ```bash # JSON (base64-encoded content) curl -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/files \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"path\": \"/workspace/script.py\", \"content\": \"$(base64 -w0 script.py)\"}" # Multipart curl -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/files \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -F "path=/workspace/script.py" \ -F "file=@script.py" ``` --- ## Download a File **`GET /api/v1/sandboxes/{id}/files/{path}`** Downloads a file from the sandbox filesystem. ### Auth Bearer token required. ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | `id` | string | Sandbox ID. Must be in `running` state. | | `path` | string | URL-encoded path to the file inside the sandbox (leading `/` stripped). | ### Response - `200 OK` Raw file bytes with `Content-Type: application/octet-stream`. ### Errors | Status | Code | Cause | |--------|------|-------| | 404 | `FILE_NOT_FOUND` | Path does not exist inside the sandbox. | | 409 | `SANDBOX_NOT_RUNNING` | Sandbox is not in `running` state. | ```bash curl -o output.json \ "https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/files/workspace%2Foutput.json" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## List Files **`GET /api/v1/sandboxes/{id}/files?path=/workspace`** Lists files and directories in a sandbox directory. ```bash curl "https://api.miosa.ai/api/v1/sandboxes/{id}/files?path=/workspace" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` Response: ```json { "data": { "path": "/workspace", "entries": [ {"name": "index.html", "path": "/workspace/index.html", "is_dir": false, "size": 128} ] } } ``` --- ## Stat a File **`POST /api/v1/sandboxes/{id}/files/stat`** Returns metadata for one file or directory. ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/{id}/files/stat \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"path":"/workspace/index.html"}' ``` --- ## Read Logs **`GET /api/v1/sandboxes/{id}/logs`** Returns recent sandbox/template lifecycle logs. Pass `lines` to control the tail length. ```bash curl "https://api.miosa.ai/api/v1/sandboxes/{id}/logs?lines=200" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` Stream logs with SSE: ```bash curl -N "https://api.miosa.ai/api/v1/sandboxes/{id}/logs/stream" \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Accept: text/event-stream" ``` --- ## Snapshot Lifecycle **`POST /api/v1/sandboxes/{id}/snapshots`** Creates a named checkpoint for a running sandbox. ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/{id}/snapshots \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"comment":"before dependency upgrade"}' ``` List, inspect, restore, and delete snapshots: ```bash curl https://api.miosa.ai/api/v1/sandboxes/{id}/snapshots \ -H "Authorization: Bearer $MIOSA_API_KEY" curl https://api.miosa.ai/api/v1/sandboxes/{id}/snapshots/{snapshot_id} \ -H "Authorization: Bearer $MIOSA_API_KEY" curl -X POST https://api.miosa.ai/api/v1/sandboxes/{id}/restore/{snapshot_id} \ -H "Authorization: Bearer $MIOSA_API_KEY" curl -X DELETE https://api.miosa.ai/api/v1/sandboxes/{id}/snapshots/{snapshot_id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Pause and Resume **`POST /api/v1/sandboxes/{id}/pause`** Pauses a running sandbox using the snapshot path. Paused sandboxes keep their lifecycle record and can be resumed. ```bash curl -X POST https://api.miosa.ai/api/v1/sandboxes/{id}/pause \ -H "Authorization: Bearer $MIOSA_API_KEY" curl -X POST https://api.miosa.ai/api/v1/sandboxes/{id}/resume \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Subscribe to Events (SSE) **`GET /api/v1/sandboxes/{id}/events`** Opens a Server-Sent Events (SSE) stream that emits sandbox lifecycle events. The connection closes automatically when the sandbox reaches `destroyed` or `error`. ### Auth Bearer token required (sent as `Authorization: Bearer <key>` header). ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | `id` | string | Sandbox ID. | ### Response - `200 OK` (event stream) `Content-Type: text/event-stream` **Event: `state_changed`** ``` event: state_changed data: {"state":"running","previous_state":"provisioning","timestamp":"2026-04-25T10:00:07Z"} ``` **Event: `exec_output`** ``` event: exec_output data: {"exec_id":"exec_abc","stream":"stdout","data":"2\n","timestamp":"2026-04-25T10:00:10Z"} ``` **Event: `error`** ``` event: error data: {"message":"VM terminated unexpectedly","timestamp":"2026-04-25T10:05:00Z"} ``` ```bash curl -N "https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/events" \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Accept: text/event-stream" ``` --- ## Sandbox Object The full sandbox object returned by create, get, and list endpoints: | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique sandbox identifier. | | `tenant_id` | string | Owning tenant ID. | | `owner_id` | string | Creating user ID. | | `template_id` | string | Boot template name. | | `state` | string | Current lifecycle state. | | `cpu_count` | integer | Allocated vCPUs. | | `memory_mb` | integer | Allocated RAM in MB. | | `disk_size_mb` | integer | Allocated root disk in MB. | | `image_id` | string | Resolved rootfs image, for example `miosa-sandbox`. | | `boot_path` | string/null | Boot mechanism used, for example `snapshot` or `cold`. | | `boot_ms` | integer/null | Runtime boot time in milliseconds once known. | | `ready` | boolean | Whether command and readiness checks are accepting work. | | `ready_at` | string/null | ISO timestamp when `ready` became true. | | `preview_url` | string/null | Tenant-aware base preview URL for the sandbox slug. Use `/expose` for a specific port. | | `timeout_sec` | integer | Max seconds before force-destroy. | | `total_runtime_sec` | integer \| null | Billed seconds. Set only after destruction. | | `metadata` | object | Caller-supplied metadata. | | `created_at` | string | ISO-8601 creation timestamp. | | `started_at` | string \| null | ISO-8601 timestamp when VM entered `running`. | | `destroyed_at` | string \| null | ISO-8601 timestamp when VM was destroyed. | --- ## Lifecycle States ``` provisioning → running → destroyed running → paused → running * → error (terminal) ``` | State | Description | |-------|-------------| | `provisioning` | VM is booting. Exec and file operations are not yet available. | | `running` | VM is reachable. All operations allowed. | | `paused` | VM has a RAM snapshot on disk. Can be resumed. | | `destroyed` | Terminal. VM is gone, billing is settled. | | `error` | Terminal failure. Destroy and create a new sandbox. | --- ## Error Response Format All error responses use this shape: ```json { "error": { "code": "NOT_FOUND", "message": "sandbox not found", "details": null } } ``` The `x-request-id` response header is set on every request. Include it in support requests. --- ## Rate Limits | Limit | Value | |-------|-------| | Requests per minute per workspace | 300 | | Concurrent sandboxes per tenant | 10 (contact support to increase) | | Max sandbox runtime | Plan/policy dependent; day-long and always-on sandboxes are supported when billing policy allows it | | Default resource shape | 1 vCPU, 1 GB RAM, 3 GB disk | | Default resource caps | 4 vCPU, 8 GB RAM, 10 GB disk | | Max exec timeout | 300 s (5 minutes) | | Max file upload size | 100 MB | Rate-limited responses return HTTP `429` with a `Retry-After` header indicating seconds to wait. --- ## See also - [Python SDK](/docs/sdks/python/) - `miosa` - [TypeScript SDK](/docs/sdks/typescript/) - `@miosa/sdk` - [Go SDK](/docs/sdks/go/) - `github.com/miosa-ai/miosa-go` - [Java SDK](/docs/sdks/java/) - `ai.miosa:miosa-sdk` - [Elixir SDK](/docs/sdks/elixir/) - `:miosa` - [Events (SSE)](/docs/api-reference/events/) - SSE reference for other MIOSA resources - [Error Codes](/docs/api-reference/errors/) - full error code catalog including sandbox-specific codes --- # Services API URL: https://miosa.ai/docs/api-reference/services Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/services Source: src/routes/docs/api-reference/services/+page.md Description: API reference for managing long-running systemd services inside MIOSA computers. Services are persistent processes managed by systemd inside a computer. Use them for web servers, background workers, databases, or any daemon that should survive across exec calls and restart automatically on failure. Base path: `/api/v1/computers/{id}/services` Services map directly to systemd unit files (`/etc/systemd/system/miosa-{name}.service`) managed by the computer command service. The computer must be **running** to interact with services. --- ## Quick Start ```typescript const client = new Miosa(); // Register and start a web server const svc = await client.services.create(computerId, { name: 'webserver', command: 'python3 -m http.server 8080', cwd: '/workspace/app', restart: 'always', }); console.log(svc.status); // "starting" // Check status later const updated = await client.services.get(computerId, svc.id); console.log(updated.status); // "running" ``` ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/services \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "webserver", "command": "python3 -m http.server 8080", "cwd": "/workspace/app", "restart": "always" }' ``` --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `POST` | `/computers/{id}/services` | Create and start a service | | `GET` | `/computers/{id}/services` | List all services on a computer | | `GET` | `/computers/{id}/services/{name}` | Get a service by name | | `POST` | `/computers/{id}/services/{name}/start` | Start a stopped service | | `POST` | `/computers/{id}/services/{name}/stop` | Stop a running service | | `POST` | `/computers/{id}/services/{name}/restart` | Restart a service | | `DELETE` | `/computers/{id}/services/{name}` | Remove a service | --- ## Create a Service **`POST /api/v1/computers/{id}/services`** Writes the systemd unit file and starts the service. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | Service name. Pattern: `[a-z][a-z0-9-]{0,62}` | | `command` | string | Yes | Full command to run | | `env` | object | No | Environment variables as `{ "KEY": "value" }` | | `cwd` | string | No | Working directory (default: `/workspace`) | | `restart` | string | No | `"always"`, `"on-failure"` (default), or `"no"` | ### Response - `201 Created` ```json { "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "computer_id": "...", "tenant_id": "...", "name": "webserver", "command": "python3 -m http.server 8080", "env": {}, "cwd": "/workspace/app", "restart": "always", "status": "starting", "pid": null, "exit_code": null, "last_started_at": null, "last_exited_at": null, "created_at": "2026-04-11T00:00:00Z", "updated_at": "2026-04-11T00:00:00Z" } } ``` ### Status Values | Status | Description | |--------|-------------| | `stopped` | Registered but not started | | `starting` | Start dispatched to systemd | | `running` | systemd reports `ActiveState=active` | | `failed` | systemd reports `ActiveState=failed` | | `removed` | Unit file deleted; record kept for audit | ### Errors | Status | Error | Cause | |--------|-------|-------| | 409 | `a service with this name already exists` | Name must be unique per computer | | 422 | Validation error | Invalid name format or restart policy | | 502 | `AGENT_UNAVAILABLE` | In-VM agent unreachable | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/services \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "api-server", "command": "/workspace/app/server", "env": {"PORT": "3000", "NODE_ENV": "production"}, "cwd": "/workspace/app", "restart": "on-failure" }' ``` --- ## List Services **`GET /api/v1/computers/{id}/services`** ### Response - `200 OK` ```json { "data": [ { "id": "...", "name": "webserver", "status": "running", "pid": 12345, "restart": "always", "created_at": "2026-04-11T00:00:00Z" } ], "total": 1 } ``` ```bash curl https://api.miosa.ai/api/v1/computers/{id}/services \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Get a Service **`GET /api/v1/computers/{id}/services/{name}`** ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | `id` | UUID | Computer ID | | `name` | string | Service name | ### Response - `200 OK` Full service object (same as create response). ### Errors | Status | Error | Cause | |--------|-------|-------| | 404 | `service not found` | Does not exist on this computer | --- ## Start, Stop, Restart **`POST /api/v1/computers/{id}/services/{name}/start`** **`POST /api/v1/computers/{id}/services/{name}/stop`** **`POST /api/v1/computers/{id}/services/{name}/restart`** All return a `200 OK` with the updated service object. ### Errors | Status | Error | Cause | |--------|-------|-------| | 409 | `service is already running` | Cannot start a running service | | 409 | `service is not running` | Cannot stop a non-running service | | 502 | `AGENT_UNAVAILABLE` | In-VM agent unreachable | ```bash # Stop a service curl -X POST https://api.miosa.ai/api/v1/computers/{id}/services/webserver/stop \ -H "Authorization: Bearer $MIOSA_API_KEY" # Restart it curl -X POST https://api.miosa.ai/api/v1/computers/{id}/services/webserver/restart \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Remove a Service **`DELETE /api/v1/computers/{id}/services/{name}`** Stops the service and removes the systemd unit file. The database record transitions to `"removed"` status. ### Response - `200 OK` ```json { "data": { "name": "webserver", "status": "removed" } } ``` ```bash curl -X DELETE https://api.miosa.ai/api/v1/computers/{id}/services/webserver \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Common Recipes ### Deploy a Node.js app as a service ```typescript // Upload app files first await client.files.write(computerId, { path: '/workspace/app/server.js', content: serverJsContent, }); // Register and start const svc = await client.services.create(computerId, { name: 'node-app', command: 'node /workspace/app/server.js', env: { PORT: '3000' }, restart: 'on-failure', }); ``` ### Wait until a service is running ```python svc = client.services.create(computer_id, name="worker", command="/usr/bin/worker") while svc.status == "starting": time.sleep(1) svc = client.services.get(computer_id, svc.name) if svc.status != "running": raise RuntimeError(f"Service failed to start: {svc.status}") ``` ### Check for failed services in a fleet ```typescript const computers = await client.computers.list(); for (const computer of computers.data) { if (computer.status !== 'running') continue; const services = await client.services.list(computer.id); const failed = services.data.filter(s => s.status === 'failed'); if (failed.length > 0) { console.log(`${computer.name}: ${failed.length} failed services`); } } ``` --- # Snapshots API URL: https://miosa.ai/docs/api-reference/snapshots Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/snapshots Source: src/routes/docs/api-reference/snapshots/+page.md Description: API reference for creating, listing, and restoring microVM snapshots (checkpoints). Snapshots capture the full in-memory state of a running computer - CPU state, RAM, and filesystem - and store it durably. Restore a snapshot to spin up an identical computer in seconds. Base path: `/api/v1/computers/{id}/snapshots` Snapshots require the computer to be **running**. Creating a snapshot does not stop the computer. --- ## Quick Start ```typescript const client = new Miosa(); // Create a snapshot const snap = await client.snapshots.create(computerId, { comment: 'before-deploy-v2', }); // Poll until ready let status = snap.status; while (status !== 'ready') { await new Promise(r => setTimeout(r, 3000)); const updated = await client.snapshots.get(computerId, snap.id); status = updated.status; } // Restore - creates a new computer from the snapshot const restored = await client.snapshots.restore(computerId, snap.id); console.log(`Restored computer: ${restored.id}`); ``` ```bash # Create a snapshot curl -X POST https://api.miosa.ai/api/v1/computers/{id}/snapshots \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"comment": "before-deploy-v2"}' ``` --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `POST` | `/computers/{id}/snapshots` | Create a snapshot | | `GET` | `/computers/{id}/snapshots` | List snapshots for a computer | | `GET` | `/computers/{id}/snapshots/{snap_id}` | Get a snapshot | | `DELETE` | `/computers/{id}/snapshots/{snap_id}` | Delete a snapshot | | `POST` | `/computers/{id}/restore/{snap_id}` | Restore from snapshot | | `GET` | `/computers/{id}/snapshots/{snap_id}/events` | SSE progress stream | --- ## Create a Snapshot **`POST /api/v1/computers/{id}/snapshots`** Initiates an asynchronous snapshot. The returned status will be `"creating"`. Poll `GET /snapshots/{snap_id}` or subscribe to SSE events for progress. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `comment` | string | No | Human-readable label (max 500 chars) | ### Response - `201 Created` ```json { "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "computer_id": "...", "tenant_id": "...", "comment": "before-deploy-v2", "status": "creating", "state_size_bytes": null, "memory_size_bytes": null, "rootfs_size_bytes": null, "compressed_size_bytes": null, "s3_bucket": null, "s3_prefix": null, "error": null, "created_at": "2026-04-11T00:00:00Z", "updated_at": "2026-04-11T00:00:00Z" } } ``` ### Status State Machine ``` creating → uploading → ready any → failed ready → deleted ``` ### Errors | Status | Error | Cause | |--------|-------|-------| | 404 | `computer not found` | Computer does not exist or wrong tenant | | 409 | `computer is not running` | Snapshot requires running VM | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/snapshots \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"comment": "checkpoint-before-risky-op"}' ``` --- ## List Snapshots **`GET /api/v1/computers/{id}/snapshots`** ### Response - `200 OK` ```json { "data": [ { "id": "...", "comment": "before-deploy-v2", "status": "ready", "compressed_size_bytes": 524288000, "created_at": "2026-04-11T00:00:00Z" } ], "total": 1 } ``` ```bash curl https://api.miosa.ai/api/v1/computers/{id}/snapshots \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Get a Snapshot **`GET /api/v1/computers/{id}/snapshots/{snap_id}`** ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | `id` | UUID | Computer ID | | `snap_id` | UUID | Snapshot ID | ### Response - `200 OK` Full snapshot object (same shape as create response). ### Errors | Status | Error | Cause | |--------|-------|-------| | 403 | `forbidden` | Snapshot belongs to a different tenant | | 404 | `snapshot not found` | Does not exist | ```bash curl https://api.miosa.ai/api/v1/computers/{id}/snapshots/{snap_id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Delete a Snapshot **`DELETE /api/v1/computers/{id}/snapshots/{snap_id}`** Soft-deletes the snapshot (transitions to `"deleted"` status). S3 objects are cleaned up asynchronously. ### Response - `200 OK` ```json { "data": { "id": "...", "status": "deleted" } } ``` ### Errors | Status | Error | Cause | |--------|-------|-------| | 404 | `snapshot not found` | Does not exist | | 409 | `snapshot is not in a deletable state` | Snapshot is being restored | ```bash curl -X DELETE https://api.miosa.ai/api/v1/computers/{id}/snapshots/{snap_id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Restore from Snapshot **`POST /api/v1/computers/{id}/restore/{snap_id}`** Creates a new computer with the state from the snapshot. The original computer is unchanged. ### Response - `201 Created` ```json { "data": { "id": "new-computer-uuid", "name": "my-computer-restored", "status": "provisioning", "size": "small" }, "snapshot": { "id": "snap-uuid", "status": "restoring" } } ``` ### Errors | Status | Error | Cause | |--------|-------|-------| | 404 | `snapshot not found` | Does not exist | | 409 | `snapshot is not ready` | Snapshot must be in `ready` state | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/restore/{snap_id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## SSE Progress Stream **`GET /api/v1/computers/{id}/snapshots/{snap_id}/events`** Subscribe to real-time snapshot progress. Requires a short-lived ticket rather than a Bearer token (browsers cannot set Authorization on EventSource connections). ### Event Types | Event | Description | |-------|-------------| | `snapshot_creating` | VM writing memory and VM state to disk | | `snapshot_uploading` | Compressing and uploading to object storage | | `snapshot_ready` | Upload complete; snapshot is usable | | `snapshot_failed` | Unrecoverable error - see `error` field | --- ## Common Recipes ### Automated checkpoint before risky operations ```python from miosa import Miosa client = Miosa() snap = client.snapshots.create(computer_id, comment="pre-migration") while snap.status not in ("ready", "failed"): time.sleep(2) snap = client.snapshots.get(computer_id, snap.id) if snap.status == "failed": raise RuntimeError(f"Snapshot failed: {snap.error}") # Proceed with risky operation knowing you can restore do_migration(computer_id) ``` ### Snapshot-based parallelism (clone a baseline) ```typescript // Build a baseline environment once, clone it N times for parallel jobs const baseline = await client.snapshots.create(setupComputerId, { comment: 'baseline-env', }); // Each restore creates an independent computer from the same state const workers = await Promise.all( Array.from({ length: 5 }, () => client.snapshots.restore(setupComputerId, baseline.id) ) ); ``` --- ## See also - [Computers API](/docs/api-reference/computers/) - computer lifecycle and clone endpoint - [Events (SSE)](/docs/api-reference/events/) - stream snapshot progress events - [Error Codes](/docs/api-reference/errors/) - snapshot-specific error codes --- # Streaming Exec API URL: https://miosa.ai/docs/api-reference/streaming-exec Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/streaming-exec Source: src/routes/docs/api-reference/streaming-exec/+page.md Description: API reference for streaming command output in real-time from MIOSA computers via SSE. Streaming exec runs a command on a computer and delivers output line-by-line via Server-Sent Events. Use it for long-running builds, installs, or any command where you want incremental output rather than waiting for completion. Base path: `/api/v1/computers/{id}/exec/stream` Standard `POST /computers/{id}/exec` waits for the command to finish before returning. Streaming exec is better for commands that run longer than a few seconds or that you want to monitor interactively. --- ## Quick Start ```typescript const client = new Miosa(); // Stream a long-running build for await (const event of client.exec.stream(computerId, { command: 'pip install torch', timeout: 300, })) { if (event.type === 'output') { process.stdout.write(event.data.line); } else if (event.type === 'exit') { console.log(`Exit code: ${event.data.exitCode}`); break; } } ``` ```bash # Obtain a streaming ticket first, then connect TICKET=$(curl -s -X POST https://api.miosa.ai/api/v1/computers/{id}/exec/stream \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command": "pip install torch", "timeout": 300}' | jq -r .ticket) curl -N "https://api.miosa.ai/api/v1/computers/{id}/exec/stream/events?ticket=$TICKET" \ -H "Accept: text/event-stream" ``` --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `POST` | `/computers/{id}/exec/stream` | Start a streaming exec and obtain a ticket | | `GET` | `/computers/{id}/exec/stream/events` | SSE stream (requires ticket) | | `POST` | `/computers/{id}/exec` | Non-streaming exec (waits for completion) | | `POST` | `/computers/{id}/exec/python` | Non-streaming Python exec | --- ## Start Streaming Exec **`POST /api/v1/computers/{id}/exec/stream`** Starts the command and returns a short-lived ticket for the SSE stream. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `command` | string | Yes | Shell command to execute | | `timeout` | integer | No | Timeout in seconds (default: 30, max: 300) | | `shell` | string | No | Shell to use (default: `/bin/bash`) | ### Response - `202 Accepted` ```json { "ticket": "exec_short_lived_token", "expires_at": 1712700060, "stream_url": "/api/v1/computers/{id}/exec/stream/events?ticket=exec_short_lived_token" } ``` The ticket expires in 60 seconds - open the EventSource connection immediately. ### Errors | Status | Error | Cause | |--------|-------|-------| | 409 | `COMPUTER_NOT_RUNNING` | Computer is not running | | 502 | `AGENT_UNAVAILABLE` | In-VM agent unreachable | ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{id}/exec/stream \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command": "npm install", "timeout": 120}' ``` --- ## Connect to SSE Stream **`GET /api/v1/computers/{id}/exec/stream/events?ticket={ticket}`** ### Event Types | Event | Payload Fields | Description | |-------|---------------|-------------| | `output` | `line`, `stream` | A line of stdout or stderr | | `exit` | `exit_code`, `duration_ms` | Command finished; stream closes after this | | `timeout` | `timeout_seconds` | Command killed after timeout | | `error` | `message` | Internal error before command started | ### Event Payload Examples ``` event: output data: {"line": "Collecting torch\n", "stream": "stdout"} event: output data: {"line": " Downloading torch-2.3.0-...\n", "stream": "stdout"} event: exit data: {"exit_code": 0, "duration_ms": 45312} ``` ### `stream` Values | Value | Description | |-------|-------------| | `stdout` | Standard output | | `stderr` | Standard error | ```javascript // Browser const es = new EventSource(`/api/v1/computers/${id}/exec/stream/events?ticket=${ticket}`); es.addEventListener('output', e => { const { line, stream } = JSON.parse(e.data); console.log(`[${stream}] ${line}`); }); es.addEventListener('exit', e => { const { exit_code } = JSON.parse(e.data); console.log(`Exited with code ${exit_code}`); es.close(); }); es.addEventListener('error', () => { // Connection dropped or ticket expired es.close(); }); ``` --- ## Comparison: Streaming vs Non-Streaming | Aspect | `POST /exec` | `POST /exec/stream` | |--------|-------------|---------------------| | Response timing | After command completes | Immediate (ticket) | | Output delivery | All at once | Line by line via SSE | | Max timeout | 300 s | 300 s | | Best for | Quick commands | Long builds, installs | | SDK method | `exec.run()` | `exec.stream()` | --- ## Common Recipes ### Monitor a build ```python for event in client.exec.stream(computer_id, command="make build", timeout=300): if event.type == "output": print(event.data["line"], end="") elif event.type == "exit": if event.data["exit_code"] != 0: raise RuntimeError(f"Build failed with exit code {event.data['exit_code']}") print("Build succeeded") break ``` ### Capture both streams separately ```typescript const stdout: string[] = []; const stderr: string[] = []; for await (const event of client.exec.stream(computerId, { command: 'myapp 2>&1 | tee /tmp/output.log' })) { if (event.type === 'output') { if (event.data.stream === 'stdout') stdout.push(event.data.line); else stderr.push(event.data.line); } else if (event.type === 'exit') { break; } } ``` ### Timeout handling ```python try: for event in client.exec.stream(computer_id, command="long-task", timeout=60): if event.type == "timeout": print(f"Command killed after {event.data['timeout_seconds']}s") break elif event.type == "exit": break except Exception as e: print(f"Stream error: {e}") ``` --- # Tenant Branding API URL: https://miosa.ai/docs/api-reference/tenant-branding Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/tenant-branding Source: src/routes/docs/api-reference/tenant-branding/+page.md Description: Customize the logo, name, and colors shown on sandbox 404 and 502 error pages. The branding API controls the appearance of proxy-generated error pages (404, 502, gateway timeout) served within your custom preview domain. Set your logo, product name, support URL, and primary accent color so end-users see your brand - not MIOSA's. Base path: `/api/v1/tenant/branding` Branding changes are served immediately from cache. There is no propagation delay. --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/tenant/branding` | Get current branding | | `PUT` | `/api/v1/tenant/branding` | Set or replace branding | | `DELETE` | `/api/v1/tenant/branding` | Reset to MIOSA defaults | --- ## Get Branding **`GET /api/v1/tenant/branding`** ### Auth ``` Authorization: Bearer msk_... ``` ### Response - `200 OK` ```json { "branding": { "logo_url": "https://cdn.cliniciq.com/logo.png", "name": "ClinicIQ", "support_url": "https://cliniciq.com/support", "primary_color": "#0066FF" } } ``` Returns `{"branding": null}` if no custom branding has been set. --- ## Set Branding **`PUT /api/v1/tenant/branding`** Replaces all branding fields. Omitted fields are cleared to their defaults. ### Request Body Wrap fields under a `"branding"` key. | Field | Type | Required | Description | |-------|------|----------|-------------| | `logo_url` | string | No | HTTPS URL to your logo image | | `name` | string | No | Product or company name | | `support_url` | string | No | Link shown on error pages for user support | | `primary_color` | string | No | Hex color for CTA buttons (e.g. `"#0066FF"`) | ```json { "branding": { "logo_url": "https://cdn.cliniciq.com/logo.png", "name": "ClinicIQ", "support_url": "https://cliniciq.com/support", "primary_color": "#0066FF" } } ``` ### Response - `200 OK` ```json { "branding": { "logo_url": "https://cdn.cliniciq.com/logo.png", "name": "ClinicIQ", "support_url": "https://cliniciq.com/support", "primary_color": "#0066FF" } } ``` --- ## Delete Branding **`DELETE /api/v1/tenant/branding`** Resets all branding to MIOSA defaults. Error pages will show the MIOSA logo and color scheme. ### Response - `200 OK` ```json { "branding": null } ``` --- ## Errors | Status | Code | Cause | |--------|------|-------| | 404 | `tenant not found` | Tenant does not exist | | 422 | validation error | A field failed validation (e.g. invalid URL, invalid hex color) | --- ## Examples ```bash # Set branding curl -X PUT https://api.miosa.ai/api/v1/tenant/branding \ -H "Authorization: Bearer msk_live_..." \ -H "Content-Type: application/json" \ -d '{ "branding": { "logo_url": "https://cdn.cliniciq.com/logo.png", "name": "ClinicIQ", "support_url": "https://cliniciq.com/support", "primary_color": "#0066FF" } }' # Get curl https://api.miosa.ai/api/v1/tenant/branding \ -H "Authorization: Bearer msk_live_..." # Reset curl -X DELETE https://api.miosa.ai/api/v1/tenant/branding \ -H "Authorization: Bearer msk_live_..." ``` ```python client = miosa.Miosa() client.tenant.set_branding( logo_url="https://cdn.cliniciq.com/logo.png", name="ClinicIQ", support_url="https://cliniciq.com/support", primary_color="#0066FF", ) branding = client.tenant.get_branding() print(branding.name) ``` ```typescript const client = new Miosa(); await client.tenant.setBranding({ logoUrl: 'https://cdn.cliniciq.com/logo.png', name: 'ClinicIQ', supportUrl: 'https://cliniciq.com/support', primaryColor: '#0066FF', }); const branding = await client.tenant.getBranding(); console.log(branding?.name); ``` --- # Tenant Events (SSE) API URL: https://miosa.ai/docs/api-reference/tenant-events Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/tenant-events Source: src/routes/docs/api-reference/tenant-events/+page.md Description: Subscribe to a real-time server-sent event stream of all tenant-wide platform events. The tenant events stream delivers real-time Server-Sent Events (SSE) for all activity across your tenant - sandbox lifecycle changes, deployments, webhook deliveries, and more. Subscribe once and receive events for all your resources. Base path: `/api/v1/events/stream` This endpoint uses `text/event-stream` (SSE). Set `Accept: text/event-stream` on your request, or use a native `EventSource` client. The `Authorization` header cannot be set by browser `EventSource`. Use a server-side client or pass an SSE ticket obtained from `POST /api/v1/auth/sse-ticket` for browser usage. --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/events/stream` | Open tenant-wide SSE event stream | --- ## Open Event Stream **`GET /api/v1/events/stream`** Upgrades to an SSE stream. The connection stays open until the client disconnects or the server closes it. A `: heartbeat` comment is sent every 15 seconds to keep the connection alive through proxies. ### Auth ``` Authorization: Bearer msk_... ``` ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `types` | string | No | Comma-separated glob patterns to filter events (e.g. `sandbox.*,webhook.delivered`) | ### Response - `200 OK` (`text/event-stream`) Each event is a standard SSE frame: ``` id: evt_01hwxyz... event: sandbox.ready data: {"type":"sandbox.ready","id":"sbx_abc123","status":"running","tenant_id":"...","ts":"2026-05-26T10:14:23Z"} id: evt_01hwyza... event: deployment.succeeded data: {"type":"deployment.succeeded","id":"dep_xyz456","url":"https://my-app.miosa.app","ts":"2026-05-26T10:15:01Z"} : heartbeat ``` ### Event Shape All events share a common envelope: ```json { "type": "sandbox.ready", "id": "sbx_abc123", "tenant_id": "ten_...", "ts": "2026-05-26T10:14:23Z" } ``` Additional fields vary by event type (see table below). --- ## Event Types ### Sandbox Events | Type | Description | Extra Fields | |------|-------------|-------------| | `sandbox.created` | Sandbox provisioned | `image`, `workspace_id` | | `sandbox.ready` | Sandbox reached `running` status | `status` | | `sandbox.error` | Sandbox failed to start | `error`, `reason` | | `sandbox.destroyed` | Sandbox deleted | - | | `sandbox.paused` | Sandbox vCPUs suspended | - | | `sandbox.resumed` | Sandbox vCPUs resumed | - | ### Computer Events | Type | Description | Extra Fields | |------|-------------|-------------| | `computer.created` | Computer provisioned | `size`, `workspace_id` | | `computer.running` | Computer reached `running` status | - | | `computer.stopped` | Computer stopped | - | | `computer.deleted` | Computer deleted | - | ### Deployment Events | Type | Description | Extra Fields | |------|-------------|-------------| | `deployment.build_started` | Build job enqueued | `version_id` | | `deployment.succeeded` | Build completed and promoted | `url`, `version_id` | | `deployment.failed` | Build or promotion failed | `error` | | `deployment.rollback` | Version rolled back | `from_version`, `to_version` | ### Webhook Events | Type | Description | Extra Fields | |------|-------------|-------------| | `webhook.delivered` | Outgoing webhook POST succeeded | `webhook_id`, `status_code` | | `webhook.failed` | Outgoing webhook POST failed | `webhook_id`, `attempt`, `error` | ### Filtering with `types` Pass a comma-separated list of glob patterns. The `*` wildcard matches any suffix segment: ``` ?types=sandbox.* # all sandbox events ?types=sandbox.*,computer.running ?types=deployment.succeeded,deployment.failed ``` An empty `types` parameter (or no parameter) delivers all events. --- ## Reconnection On disconnect, reconnect using the `Last-Event-ID` header set to the last `id` you received. The server will resume from that cursor, replaying up to 60 seconds of backlogged events. ``` GET /api/v1/events/stream Authorization: Bearer msk_... Last-Event-ID: evt_01hwxyz... ``` --- ## Examples ```bash # All events curl -N -H "Authorization: Bearer msk_live_..." \ -H "Accept: text/event-stream" \ "https://api.miosa.ai/api/v1/events/stream" # Sandbox events only curl -N -H "Authorization: Bearer msk_live_..." \ -H "Accept: text/event-stream" \ "https://api.miosa.ai/api/v1/events/stream?types=sandbox.*" ``` ```python headers = { "Authorization": "Bearer msk_live_...", "Accept": "text/event-stream", } with httpx.stream("GET", "https://api.miosa.ai/api/v1/events/stream", params={"types": "sandbox.*"}, headers=headers) as r: for line in r.iter_lines(): if line.startswith("data:"): event = json.loads(line[5:].strip()) print(event["type"], event["id"]) ``` ```typescript // Server-side (Node.js / Deno) - EventSource doesn't support custom headers in browsers const es = new EventSource( 'https://api.miosa.ai/api/v1/events/stream?types=sandbox.*', { headers: { Authorization: 'Bearer msk_live_...' } } ); es.addEventListener('sandbox.ready', (e) => { const data = JSON.parse(e.data); console.log('Sandbox ready:', data.id); }); es.addEventListener('sandbox.error', (e) => { const data = JSON.parse(e.data); console.error('Sandbox failed:', data.id, data.error); }); es.onerror = () => es.close(); ``` --- # Tenant Preview Domain API URL: https://miosa.ai/docs/api-reference/tenant-preview-domain Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/tenant-preview-domain Source: src/routes/docs/api-reference/tenant-preview-domain/+page.md Description: Configure a custom apex domain for sandbox preview URLs (white-label). The preview domain API lets you map your own apex domain (e.g. `cliniciq.com`) to sandbox preview URLs. When configured, sandboxes created under your tenant will be accessible at `{sandbox-slug}.{your-domain}` instead of the default `*.sandboxes.miosa.ai`. **Auth note:** These endpoints accept both `msk_*` API keys and JWT tokens. This matters for white-label integrations where you programmatically manage the domain from a backend service. Only apex domains are accepted (e.g. `cliniciq.com`, not `app.cliniciq.com` or `https://cliniciq.com`). Wildcard and subdomain inputs are rejected with `422 invalid_domain`. --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/tenant/preview-domain` | Get current domain and verification status | | `PUT` | `/api/v1/tenant/preview-domain` | Set or replace the preview domain | | `POST` | `/api/v1/tenant/preview-domain/verify` | Recheck DNS verification | | `DELETE` | `/api/v1/tenant/preview-domain` | Remove the custom domain | --- ## Get Preview Domain **`GET /api/v1/tenant/preview-domain`** Returns the configured domain and its DNS verification status. ### Auth ``` Authorization: Bearer msk_... ``` ### Response - `200 OK` (domain configured) ```json { "preview_domain": "cliniciq.com", "status": "configured", "dns_instructions": { "type": "CNAME", "name": "*.cliniciq.com", "value": "proxy.sandboxes.miosa.ai" } } ``` ### Response - `200 OK` (not configured) ```json { "preview_domain": null, "default_domain": "sandboxes.miosa.ai", "status": "not_configured", "dns_instructions": null } ``` | Field | Type | Description | |-------|------|-------------| | `preview_domain` | string\|null | Configured apex domain, or `null` | | `default_domain` | string | Fallback domain used when no custom domain is set | | `status` | string | `"not_configured"`, `"pending_dns"`, or `"configured"` | | `dns_instructions` | object\|null | CNAME record to add at your DNS registrar | --- ## Set Preview Domain **`PUT /api/v1/tenant/preview-domain`** Sets or replaces the custom preview domain. DNS verification is asynchronous - status transitions from `pending_dns` to `configured` once the CNAME resolves correctly. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `preview_domain` | string | Yes | Apex domain (e.g. `"cliniciq.com"`) | ```json { "preview_domain": "cliniciq.com" } ``` ### Response - `200 OK` ```json { "preview_domain": "cliniciq.com", "status": "pending_dns", "dns_status": "pending", "dns_instructions": { "type": "CNAME", "name": "*.cliniciq.com", "value": "proxy.sandboxes.miosa.ai" } } ``` ### DNS Setup Add a wildcard CNAME at your registrar: | Type | Name | Value | |------|------|-------| | `CNAME` | `*` (or `*.cliniciq.com`) | `proxy.sandboxes.miosa.ai` | --- ## Verify DNS **`POST /api/v1/tenant/preview-domain/verify`** Triggers an immediate DNS re-check. Returns the current verification status without waiting for the background poller. ### Response - `200 OK` ```json { "preview_domain": "cliniciq.com", "status": "configured", "dns_status": "verified" } ``` --- ## Delete Preview Domain **`DELETE /api/v1/tenant/preview-domain`** Removes the custom domain. Sandbox preview URLs revert to `*.sandboxes.miosa.ai`. ### Response - `200 OK` ```json { "preview_domain": null, "status": "removed" } ``` --- ## Errors | Status | Code | Cause | |--------|------|-------| | 400 | `preview_domain is required` | Missing body field | | 422 | `invalid_domain` | Protocol prefix, wildcard, path, or subdomain detected | | 404 | `tenant not found` | Tenant does not exist | --- ## Examples ```bash # Set curl -X PUT https://api.miosa.ai/api/v1/tenant/preview-domain \ -H "Authorization: Bearer msk_live_..." \ -H "Content-Type: application/json" \ -d '{"preview_domain": "cliniciq.com"}' # Verify curl -X POST https://api.miosa.ai/api/v1/tenant/preview-domain/verify \ -H "Authorization: Bearer msk_live_..." # Get curl https://api.miosa.ai/api/v1/tenant/preview-domain \ -H "Authorization: Bearer msk_live_..." # Delete curl -X DELETE https://api.miosa.ai/api/v1/tenant/preview-domain \ -H "Authorization: Bearer msk_live_..." ``` ```python client = miosa.Miosa() # Set result = client.tenant.set_preview_domain("cliniciq.com") print(result.dns_instructions) # Poll until verified while True: domain = client.tenant.get_preview_domain() if domain.status == "configured": break time.sleep(30) client.tenant.verify_preview_domain() ``` ```typescript const client = new Miosa(); // Set const result = await client.tenant.setPreviewDomain('cliniciq.com'); console.log(result.dnsInstructions); // Get const domain = await client.tenant.getPreviewDomain(); console.log(domain.status); // "pending_dns" | "configured" // Force re-check await client.tenant.verifyPreviewDomain(); // Remove await client.tenant.deletePreviewDomain(); ``` --- # Usage Rollup API URL: https://miosa.ai/docs/api-reference/usage-rollup Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/usage-rollup Source: src/routes/docs/api-reference/usage-rollup/+page.md Description: Aggregate sandbox compute usage and spend by external user, project, or workspace. The usage rollup endpoint aggregates metered usage across sandboxes and computers, grouped by an identifier of your choice. Use it to build per-user billing dashboards, enforce spend policies, or attribute costs to projects. Base path: `/api/v1/usage` Results are limited to 1000 rows per request. For high-cardinality tenants, narrow the period or use a more specific `group_by`. --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/usage` | Aggregate usage for a period | --- ## Get Usage Rollup **`GET /api/v1/usage`** ### Auth ``` Authorization: Bearer msk_... ``` ### Query Parameters | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `group_by` | string | No | `external_user_id` | Dimension to aggregate by | | `period` | string | No | `30d` | Predefined period or `custom` | | `start` | string | No | - | ISO 8601 start (required when `period=custom`) | | `end` | string | No | - | ISO 8601 end (required when `period=custom`) | **`group_by` values:** | Value | Description | |-------|-------------| | `external_user_id` | Group by the `external_user_id` you set on sandboxes | | `external_project_id` | Group by `external_project_id` | | `workspace_id` | Group by MIOSA workspace | **`period` values:** | Value | Description | |-------|-------------| | `7d` | Last 7 days | | `30d` | Last 30 days | | `mtd` | Month-to-date (calendar month, resets on the 1st) | | `custom` | Arbitrary range - requires `start` and `end` | ### Response - `200 OK` ```json { "period_start": "2026-04-26T00:00:00Z", "period_end": "2026-05-26T00:00:00Z", "results": [ { "external_user_id": "user_clinic_42", "sandbox_seconds": 14400, "computer_seconds": 0, "storage_gb_hours": 2.5, "credit_cents": 312 }, { "external_user_id": "user_clinic_99", "sandbox_seconds": 3600, "computer_seconds": 7200, "storage_gb_hours": 0.0, "credit_cents": 108 } ] } ``` | Field | Type | Description | |-------|------|-------------| | `period_start` | string | ISO 8601 start of aggregation window | | `period_end` | string | ISO 8601 end of aggregation window | | `results[].{group_by}` | string | Value of the grouping dimension | | `results[].sandbox_seconds` | number | vCPU-seconds used by sandboxes | | `results[].computer_seconds` | number | vCPU-seconds used by computers | | `results[].storage_gb_hours` | number | GiB-hours of attached storage | | `results[].credit_cents` | number | Total cost in platform credit cents | Results are ordered by `credit_cents` descending. --- ## Errors | Status | Code | Cause | |--------|------|-------| | 422 | `invalid group_by` | `group_by` is not one of the accepted values | | 422 | `invalid period` | `period` is not one of the accepted values | | 422 | `invalid_custom_range` | `start` or `end` is missing or not valid ISO 8601 when `period=custom` | --- ## Examples ```bash # 30-day rollup by user curl "https://api.miosa.ai/api/v1/usage?group_by=external_user_id&period=30d" \ -H "Authorization: Bearer msk_live_..." # Custom range curl "https://api.miosa.ai/api/v1/usage?group_by=external_user_id&period=custom&start=2026-05-01T00:00:00Z&end=2026-05-26T00:00:00Z" \ -H "Authorization: Bearer msk_live_..." # Group by project curl "https://api.miosa.ai/api/v1/usage?group_by=external_project_id&period=mtd" \ -H "Authorization: Bearer msk_live_..." ``` ```python client = miosa.Miosa() # Monthly rollup per user results = client.usage.rollup( group_by="external_user_id", period="30d", ) for row in results: print(f"{row.external_user_id}: {row.credit_cents / 100:.2f} USD") ``` ```typescript const client = new Miosa(); const { results } = await client.usage.rollup({ groupBy: 'external_user_id', period: '30d', }); for (const row of results) { console.log(`${row.externalUserId}: $${(row.creditCents / 100).toFixed(2)}`); } ``` --- # API Reference / Versions URL: https://miosa.ai/docs/api-reference/versions Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/versions Source: src/routes/docs/api-reference/versions/+page.md Description: Immutable deployment version sub-resource. List, get, promote, archive. A **Deployment Version** is the immutable record of one publish. See [Versions](/docs/deploy/versions/) for the conceptual model. ## Endpoints ```http GET /api/v1/deployments/:id/versions GET /api/v1/deployments/:id/versions/:version_id POST /api/v1/deployments/:id/versions/:version_id/promote ``` ## List ```http GET /api/v1/deployments/:id/versions GET /api/v1/deployments/:id/versions?state=ready GET /api/v1/deployments/:id/versions?workspace_id=550e8400-e29b-41d4-a716-446655440000 GET /api/v1/deployments/:id/versions?external_workspace_id=clinic_123 GET /api/v1/deployments/:id/versions?limit=50&cursor=... ``` Response: ```json { "data": [ { "id": "ver_...", "deployment_id": "dep_...", "version_number": 17, "kind": "static", "state": "ready", "artifact_sha256": "a5e6f0c1...", "source_sha256": "f1a3b8c2...", "promoted_at": "2026-05-14T18:20:00Z", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "project_id": "660e8400-e29b-41d4-a716-446655440001", "external_workspace_id": "clinic_123", "external_user_id": "dr-smith-456", "created_at": "2026-05-14T18:19:48Z" } ], "next_cursor": "..." } ``` ## Get ```http GET /api/v1/deployments/:id/versions/:version_id ``` Returns the full version row including `artifact_manifest`, `runtime_image`, `runtime_command`, `runtime_port`, `health_check_path`, `build_log_uri`. ## Promote Make a specific ready version the active one for the deployment. ```http POST /api/v1/deployments/:id/versions/:version_id/promote ``` Body: empty, or optionally `{ "environment": "production" }`. Scopes: `deployments:write`. Requires `Idempotency-Key` for safety on retries. Promotion is different from publish: - **Publish** creates a new version from a sandbox source. - **Promote** points an existing ready version as active in an environment. Use promote for canary / staged rollouts: publish to a non-production environment first, validate, then promote to production. ## States | State | Meaning | |---|---| | `created` | Row exists, build not started | | `building` | Build in progress | | `ready` | Build succeeded, artifact uploaded, promotable | | `failed` | Build or health check failed | | `archived` | Promoted-out-of; still bootable for rollback | State transitions are one-way. Once `ready` or `failed`, a version stays that way. ## Permissions | Action | Scope | |---|---| | List, Get | `deployments:read` | | Promote | `deployments:write` | ## See also - [Deployments](/docs/api-reference/deployments/) - parent resource - [Releases](/docs/api-reference/releases/) - artifact reference - [Versions](/docs/deploy/versions/) - conceptual model - [Rollback](/docs/deploy/rollback/) - uses promote internally --- # Webhooks API URL: https://miosa.ai/docs/api-reference/webhooks Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/webhooks Source: src/routes/docs/api-reference/webhooks/+page.md Description: Subscribe to platform lifecycle events via outgoing HTTP callbacks with HMAC signature verification. Webhooks fire outgoing HTTP POST requests to your endpoint when platform events occur - sandbox lifecycle changes, deployment completions, billing events, and more. Each delivery is signed with HMAC-SHA256 so you can verify authenticity. Base path: `/api/v1/webhooks` The webhook `secret` is returned **only once** - in the response to `POST /webhooks`. Store it immediately in a secure location. It cannot be retrieved later (only a masked preview is shown afterward). --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/webhooks` | List all webhooks | | `POST` | `/api/v1/webhooks` | Create a webhook | | `GET` | `/api/v1/webhooks/{id}` | Get a webhook | | `PATCH` | `/api/v1/webhooks/{id}` | Update a webhook | | `DELETE` | `/api/v1/webhooks/{id}` | Delete a webhook | | `POST` | `/api/v1/webhooks/{id}/test` | Send a test delivery | | `GET` | `/api/v1/webhooks/{id}/deliveries` | List delivery attempts | --- ## List Webhooks **`GET /api/v1/webhooks`** ### Auth ``` Authorization: Bearer msk_... ``` ### Response - `200 OK` ```json { "data": [ { "id": "wh_01hwxyz...", "name": "My Webhook", "url": "https://api.myapp.com/hooks/miosa", "events": ["sandbox.created", "sandbox.destroyed"], "secret_preview": "sk_live_••••••••abcd", "retry_count": 3, "created_at": "2026-05-01T00:00:00Z", "updated_at": "2026-05-01T00:00:00Z" } ] } ``` --- ## Create a Webhook **`POST /api/v1/webhooks`** ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `url` | string | Yes | HTTPS endpoint to POST events to | | `name` | string | No | Human-readable label | | `events` | string[] | No | Event type filter; empty = all events | | `headers` | object | No | Extra headers to include in deliveries | | `retry_count` | integer | No | Number of retries on failure (0-10, default `3`) | | `metadata` | object | No | Arbitrary key-value metadata | ```json { "url": "https://api.myapp.com/hooks/miosa", "name": "Sandbox lifecycle", "events": ["sandbox.created", "sandbox.ready", "sandbox.error", "sandbox.destroyed"], "retry_count": 3 } ``` ### Response - `201 Created` ```json { "data": { "id": "wh_01hwxyz...", "name": "Sandbox lifecycle", "url": "https://api.myapp.com/hooks/miosa", "events": ["sandbox.created", "sandbox.ready", "sandbox.error", "sandbox.destroyed"], "secret": "sk_live_abc123def456...", "secret_preview": "sk_live_••••••••3456", "retry_count": 3, "created_at": "2026-05-26T10:14:23Z", "updated_at": "2026-05-26T10:14:23Z" } } ``` The `secret` field appears only in this response. Save it securely. --- ## Update a Webhook **`PATCH /api/v1/webhooks/{id}`** Partial update - include only fields you want to change. ### Request Body Same fields as create, all optional. ### Response - `200 OK` Webhook object without the `secret` field. --- ## Delete a Webhook **`DELETE /api/v1/webhooks/{id}`** ### Response - `204 No Content` --- ## Send a Test Delivery **`POST /api/v1/webhooks/{id}/test`** Sends a synthetic `webhook.test` event to the configured URL to verify connectivity and signature verification. ### Response - `200 OK` ```json { "delivered": true, "status_code": 200, "duration_ms": 143 } ``` --- ## List Deliveries **`GET /api/v1/webhooks/{id}/deliveries`** Returns recent delivery attempts with status codes and timing. ### Response - `200 OK` ```json { "data": [ { "id": "del_01hwyza...", "event_type": "sandbox.ready", "status": "delivered", "status_code": 200, "attempt": 1, "duration_ms": 98, "delivered_at": "2026-05-26T10:14:24Z" }, { "id": "del_01hwyzb...", "event_type": "sandbox.created", "status": "failed", "status_code": 500, "attempt": 3, "duration_ms": 5012, "delivered_at": "2026-05-26T10:14:20Z" } ] } ``` --- ## Signature Verification Every delivery includes an `X-Miosa-Signature` header. Verify it on your server before trusting the payload: ``` X-Miosa-Signature: sha256= ``` Compute the expected signature: ``` hmac = HMAC-SHA256(secret, raw_request_body_bytes) expected = "sha256=" + hex(hmac) assert constant_time_equals(expected, request.headers["X-Miosa-Signature"]) ``` Always compare signatures using a constant-time function to prevent timing attacks. Never use `==` for string comparison. ### Example verification (Python) ```python def verify_signature(secret: str, body: bytes, header: str) -> bool: expected = "sha256=" + hmac.new( secret.encode(), body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, header) ``` ### Example verification (TypeScript) ```typescript function verifySignature(secret: string, body: Buffer, header: string): boolean { const expected = 'sha256=' + createHmac('sha256', secret).update(body).digest('hex'); return timingSafeEqual(Buffer.from(expected), Buffer.from(header)); } ``` --- ## Retry Policy Failed deliveries are retried with exponential backoff: | Attempt | Delay | |---------|-------| | 2 | 30 seconds | | 3 | 5 minutes | | 4+ | Up to `retry_count` × 10 minutes (capped at 6 hours) | A delivery is considered failed if your endpoint returns a non-2xx status code or does not respond within 30 seconds. Set `retry_count: 0` to disable retries. --- ## Subscribed Event Types | Event | Description | |-------|-------------| | `sandbox.created` | Sandbox created | | `sandbox.ready` | Sandbox reached `running` | | `sandbox.error` | Sandbox failed to start | | `sandbox.destroyed` | Sandbox deleted | | `computer.created` | Computer created | | `computer.running` | Computer reached `running` | | `computer.stopped` | Computer stopped | | `computer.deleted` | Computer deleted | | `deployment.succeeded` | Build succeeded and promoted | | `deployment.failed` | Build or promotion failed | | `deployment.rollback` | Rollback triggered | | `billing.credit_low` | Credit balance below threshold | | `billing.credit_exhausted` | Credit balance reached zero | | `webhook.test` | Synthetic test delivery | Subscribe to all events by omitting the `events` field. Subscribe to a subset by listing specific event types. --- ## Examples ```bash # Create curl -X POST https://api.miosa.ai/api/v1/webhooks \ -H "Authorization: Bearer msk_live_..." \ -H "Content-Type: application/json" \ -d '{ "url": "https://api.myapp.com/hooks/miosa", "events": ["sandbox.created", "sandbox.destroyed"] }' # Test curl -X POST https://api.miosa.ai/api/v1/webhooks/wh_01hwxyz/test \ -H "Authorization: Bearer msk_live_..." # Deliveries curl https://api.miosa.ai/api/v1/webhooks/wh_01hwxyz/deliveries \ -H "Authorization: Bearer msk_live_..." ``` ```python client = miosa.Miosa() hook = client.webhooks.create( url="https://api.myapp.com/hooks/miosa", events=["sandbox.created", "sandbox.destroyed"], retry_count=3, ) # Save hook.secret securely - only available now print(hook.secret) # Test result = client.webhooks.test(hook.id) print(result.delivered, result.status_code) ``` ```typescript const client = new Miosa(); const hook = await client.webhooks.create({ url: 'https://api.myapp.com/hooks/miosa', events: ['sandbox.created', 'sandbox.ready', 'sandbox.destroyed'], retryCount: 3, }); // Store hook.secret in your secrets manager - only returned once console.log(hook.secret); // Test connectivity const result = await client.webhooks.test(hook.id); console.log(result.delivered, result.statusCode); ``` --- # Workspaces API URL: https://miosa.ai/docs/api-reference/workspaces Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/workspaces Source: src/routes/docs/api-reference/workspaces/+page.md Description: API reference for creating and managing MIOSA workspaces - the customer/client layer inside an organization. Workspaces group resources inside an organization. In a white-label platform, a workspace usually represents one downstream customer or client, such as `Dr. Smith Clinic` inside the `ClinicIQ` organization. Projects live inside workspaces, and sandboxes, computers, deployments, databases, storage buckets, volumes, functions, jobs, and domains belong to projects. Base path: `/api/v1/workspaces` A **default** workspace is created automatically when you sign up. Resources are assigned to the default workspace and its default project unless you specify workspace/project ownership at creation time. --- ## Quick Start ```typescript const client = new Miosa(); // reads MIOSA_API_KEY // Create a workspace for a downstream customer const ws = await client.workspaces.create({ name: 'Dr. Smith Clinic', slug: 'dr-smith-clinic', externalWorkspaceId: 'clinic_123', }); // Create a computer inside a project in that workspace const computer = await client.computers.create({ name: 'clinic-builder', templateType: 'miosa-desktop', workspaceId: ws.id, projectSlug: 'lead-magnet', projectName: 'Lead Magnet', }); // List computers in the workspace const { data } = await client.workspaces.listComputers(ws.id); ``` ```bash # curl equivalent curl -X POST https://api.miosa.ai/api/v1/workspaces \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Dr. Smith Clinic", "slug": "dr-smith-clinic", "external_workspace_id": "clinic_123"}' ``` --- ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/workspaces` | List workspaces for the tenant | | `POST` | `/workspaces` | Create a workspace | | `GET` | `/workspaces/{id}` | Get a workspace | | `PATCH` | `/workspaces/{id}` | Update a workspace | | `PUT` | `/workspaces/{id}/settings` | Update workspace-level resource limits | | `GET` | `/workspaces/{id}/preview-domain` | Read the workspace preview domain | | `PUT` | `/workspaces/{id}/preview-domain` | Set the workspace preview domain | | `DELETE` | `/workspaces/{id}/preview-domain` | Clear the workspace preview domain | | `GET` | `/workspaces/{id}/preview-domain/verify` | Check DNS readiness | | `DELETE` | `/workspaces/{id}` | Delete a workspace | | `GET` | `/workspaces/{id}/computers` | List computers in a workspace | | `GET` | `/workspaces/{id}/stats` | Computer counts and resource totals | | `GET` | `/workspaces/{id}/usage` | Time-series credit and compute usage | | `GET` | `/workspaces/{id}/projects` | List projects in a workspace | --- ## List Workspaces **`GET /api/v1/workspaces`** Returns all workspaces belonging to the authenticated tenant. ### Response - `200 OK` ```json { "data": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "tenant_id": "...", "name": "default", "slug": "default", "external_workspace_id": null, "description": null, "metadata": {}, "is_default": true, "computer_count": 3, "created_at": "2026-04-11T00:00:00Z", "updated_at": "2026-04-11T00:00:00Z" } ], "total": 1 } ``` ```bash curl https://api.miosa.ai/api/v1/workspaces \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Create a Workspace **`POST /api/v1/workspaces`** ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | Human-readable name (1-120 chars) | | `slug` | string | No | URL-safe identifier. Auto-derived from `name` if omitted. Pattern: `[a-z0-9][a-z0-9-]{0,79}` | | `external_workspace_id` | string | No | Your customer/account/workspace ID. Globally unique inside this MIOSA organization. | | `description` | string | No | Optional description | | `metadata` | object | No | Caller metadata stored on the workspace. | ### Response - `201 Created` Full workspace object (same shape as list items above). ### Errors | Status | Error | Cause | |--------|-------|-------| | 422 | `has already been taken` | Slug already exists in tenant | | 422 | `must start with alphanumeric and contain only a-z 0-9 -` | Invalid slug format | ```bash curl -X POST https://api.miosa.ai/api/v1/workspaces \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Dr. Smith Clinic", "slug": "dr-smith-clinic", "external_workspace_id": "clinic_123", "description": "Client workspace for Dr. Smith Clinic" }' ``` --- ## Get a Workspace **`GET /api/v1/workspaces/{id}`** ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | `id` | UUID | Workspace ID | ### Response - `200 OK` Full workspace object. ### Errors | Status | Error | Cause | |--------|-------|-------| | 403 | `forbidden` | Workspace belongs to a different tenant | | 404 | `workspace not found` | Does not exist | ```bash curl https://api.miosa.ai/api/v1/workspaces/{id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Update a Workspace **`PATCH /api/v1/workspaces/{id}`** Update mutable workspace fields. Prefer treating the slug as stable once resources and URLs exist. ### Request Body | Field | Type | Description | |-------|------|-------------| | `name` | string | New display name | | `slug` | string | New URL-safe slug. Must remain unique inside the organization. | | `description` | string | New description (`null` to clear) | | `external_workspace_id` | string | Your customer/account/workspace ID | | `metadata` | object | Replacement metadata map | ### Response - `200 OK` Updated workspace object. ### Errors | Status | Error | Cause | |--------|-------|-------| | 403 | `forbidden` | Wrong tenant | | 404 | `workspace not found` | Does not exist | ```bash curl -X PATCH https://api.miosa.ai/api/v1/workspaces/{id} \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "production-us-east"}' ``` --- ## Delete a Workspace **`DELETE /api/v1/workspaces/{id}`** Deletes an empty workspace. Fails if the workspace contains computers or projects. ### Response - `200 OK` ```json { "deleted": true } ``` ### Errors | Status | Error | Cause | |--------|-------|-------| | 403 | `forbidden` | Wrong tenant | | 404 | `workspace not found` | Does not exist | | 409 | `workspace has computers` | Remove or move resources first | ```bash curl -X DELETE https://api.miosa.ai/api/v1/workspaces/{id} \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Update Workspace Settings **`PUT /api/v1/workspaces/{id}/settings`** Applies resource limits and default preferences for the workspace. These limits cap what any computer inside the workspace can do. Tenant-level limits still apply as a ceiling. ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `max_computers` | integer | No | Maximum number of computers allowed in this workspace. `null` = no workspace-level cap (tenant limit applies). | | `max_computer_size` | string | No | Maximum `size` value a computer in this workspace may be created with: `"small"`, `"medium"`, or `"large"`. | | `default_computer_size` | string | No | Default `size` applied when creating a computer without an explicit `size`. | | `default_auto_stop_seconds` | integer | No | Auto-stop idle timeout (seconds) applied to new computers by default. `0` = no auto-stop. | | `allowed_template_types` | string[] | No | Allowlist of template names that can be used in this workspace. Empty array = all templates allowed. | ### Response - `200 OK` ```json { "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "settings": { "max_computers": 10, "max_computer_size": "large", "default_computer_size": "small", "default_auto_stop_seconds": 3600, "allowed_template_types": [] }, "updated_at": "2026-05-17T10:00:00Z" } ``` --- ## Workspace Preview Domain **`PUT /api/v1/workspaces/{id}/preview-domain`** Sets a client/workspace-level base domain for generated URLs. This overrides the organization preview domain for resources in this workspace unless a project has its own preview domain. ```bash curl -X PUT https://api.miosa.ai/api/v1/workspaces/{id}/preview-domain \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"preview_domain":"drsmithclinic.com"}' ``` Response: ```json { "scope": "workspace", "id": "550e8400-e29b-41d4-a716-446655440000", "preview_domain": "drsmithclinic.com", "effective_domain": "drsmithclinic.com", "status": "pending_dns", "dns_status": "pending", "url_examples": { "default_preview": "https://.drsmithclinic.com", "port_preview": "https://3000-.sandbox.drsmithclinic.com", "deployment": "https://.drsmithclinic.com" } } ``` Required DNS records: | Record type | Name | Value | |---|---|---| | `CNAME` | `*` | `proxy.miosa.ai` | | `CNAME` | `*.sandbox` | `proxy.miosa.ai` | Verify: ```bash curl https://api.miosa.ai/api/v1/workspaces/{id}/preview-domain/verify \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` Clear and inherit from the organization/domain fallback: ```bash curl -X DELETE https://api.miosa.ai/api/v1/workspaces/{id}/preview-domain \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ### Errors | Status | Error | Cause | |--------|-------|-------| | 403 | `forbidden` | Wrong tenant | | 404 | `workspace not found` | Does not exist | | 422 | `VALIDATION_FAILED` | Unknown size value or invalid limit | ```bash curl -X PUT https://api.miosa.ai/api/v1/workspaces/{id}/settings \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "max_computers": 10, "max_computer_size": "large", "default_auto_stop_seconds": 3600 }' ``` --- ## List Computers in a Workspace **`GET /api/v1/workspaces/{id}/computers`** ### Response - `200 OK` ```json { "data": [ { "id": "...", "name": "runner-1", "status": "running", "size": "small", "template_type": "miosa-desktop", "created_at": "2026-04-11T00:00:00Z" } ], "total": 1 } ``` ```bash curl https://api.miosa.ai/api/v1/workspaces/{id}/computers \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## List Projects in a Workspace **`GET /api/v1/workspaces/{id}/projects`** Returns the projects owned by a workspace. Use this for client dashboards before listing the sandboxes, computers, deployments, and databases inside a project. ### Response - `200 OK` ```json { "data": [ { "id": "660e8400-e29b-41d4-a716-446655440001", "tenant_id": "...", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "external_workspace_id": "clinic_123", "external_project_id": "project_789", "name": "Lead Magnet", "slug": "lead-magnet", "description": null, "metadata": {}, "created_at": "2026-05-18T10:00:00Z", "updated_at": "2026-05-18T10:00:00Z" } ], "total": 1 } ``` ```bash curl https://api.miosa.ai/api/v1/workspaces/{id}/projects \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Get Workspace Stats **`GET /api/v1/workspaces/{id}/stats`** Returns a snapshot of computer counts grouped by status and the aggregate resource footprint for the workspace. ### Response - `200 OK` ```json { "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "computers": { "total": 5, "by_status": { "running": 3, "stopped": 2, "provisioning": 0, "error": 0 } }, "resources": { "total_vcpus": 7, "total_memory_mb": 28672, "total_disk_gb": 160 }, "settings": { "max_computers": 10, "max_computer_size": "large", "default_auto_stop_seconds": 3600 } } ``` ```bash curl https://api.miosa.ai/api/v1/workspaces/{id}/stats \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Get Workspace Usage **`GET /api/v1/workspaces/{id}/usage`** Returns time-series credit consumption and compute-hour totals for the workspace over a given window. Use this for per-workspace cost attribution dashboards. ### Query Parameters | Parameter | Type | Description | |-----------|------|-------------| | `window` | string | `"24h"` (default), `"7d"`, `"30d"` | | `granularity` | string | `"hour"` (default), `"day"` - bucket size for the time-series data | ### Response - `200 OK` ```json { "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "window": "7d", "granularity": "day", "total_credits_used": 1284, "total_compute_hours": 42.5, "series": [ { "ts": "2026-05-10T00:00:00Z", "credits_used": 198, "compute_hours": 6.6 }, { "ts": "2026-05-11T00:00:00Z", "credits_used": 211, "compute_hours": 7.0 } ] } ``` ### Errors | Status | Error | Cause | |--------|-------|-------| | 403 | `forbidden` | Wrong tenant | | 404 | `workspace not found` | Does not exist | | 422 | `VALIDATION_FAILED` | Unknown `window` or `granularity` value | ```bash curl "https://api.miosa.ai/api/v1/workspaces/{id}/usage?window=7d&granularity=day" \ -H "Authorization: Bearer $MIOSA_API_KEY" ``` --- ## Common Recipes ### Move all computers to a new workspace Computers do not yet support a `move` operation on workspace assignment via `PATCH /computers/{id}`. Use the `POST /computers/{id}/move` endpoint instead: ```bash curl -X POST https://api.miosa.ai/api/v1/computers/{computer_id}/move \ -H "Authorization: Bearer $MIOSA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"workspace_id": "target-workspace-id"}' ``` ### Enumerate all workspaces and their sizes ```typescript const { data: workspaces } = await client.workspaces.list(); for (const ws of workspaces) { console.log(`${ws.name}: ${ws.computerCount} computers`); } ``` ### Default workspace ID lookup If you need the default workspace ID programmatically: ```typescript const { data } = await client.workspaces.list(); const defaultWs = data.find(ws => ws.isDefault); console.log(defaultWs.id); ``` --- ## Common Errors | Status | Code | Cause | |--------|------|-------| | 403 | `forbidden` | Workspace belongs to a different tenant | | 404 | `workspace not found` | Does not exist | | 409 | `workspace has computers` | Cannot delete a workspace that contains computers | | 422 | `VALIDATION_FAILED` | Name or slug failed schema validation | --- ## See also - [Computers API](/docs/api-reference/computers/) - computers are scoped to workspaces - [Projects API](/docs/api-reference/projects/) - projects live inside workspaces - [Error Codes](/docs/api-reference/errors/) - `VALIDATION_FAILED` and general errors --- # Authentication URL: https://miosa.ai/docs/authentication Fallback URL: https://miosa.roberto-c49.workers.dev/docs/authentication Source: src/routes/docs/authentication/+page.md Description: How to authenticate with the MIOSA API using msk_ API keys or JWT tokens. # Authentication Every MIOSA API request carries a single `Authorization: Bearer <token>` header. The token is either an **API key** (prefix `msk_`) or a **JWT** from `POST /auth/login`. One key covers the entire platform - computers, desktop, files, exec, CUA agent, OSA, credits, and (with the right role) admin. No separate keys for AI, no per-product configuration. ## Credential hierarchy Diagram: graph TB MSK[msk_* workspace key] -->|server-side only| API[MIOSA API calls] MSK -->|mints via POST /api/v1/browser-tokens| BT[Browser token - scoped + short-lived] BT -->|browser only| EP[Specific sandbox endpoint] MSK -->|mints via POST /api/v1/auth/sse-ticket| ST[SSE ticket - single use] ST -->|EventSource| SS[SSE stream] MSK -->|mints via POST /api/v1/sandboxes/:id/preview-token| MP[mp_ Preview token] MP -->|iframe embed - no msk_ exposed| Preview[MiosaPreview component] MSK -->|mints via POST /api/v1/sandboxes/:id/shares| MS[ms_ Share token] MS -->|URL param - public read-only| ShareURL[Public share URL] ## API Keys ### Key format ``` msk__ ``` | Prefix | Role | Capabilities | |--------|----------|---------------------------------------------------| | `msk_u_...` | user | Compute, desktop, files, CUA, OSA, credits | | `msk_a_...` | admin | Everything above **+** `/api/v1/admin/*` | | `msk_p_...` | platform | Tenant-wide automation issued by the MIOSA platform | Role is **orthogonal** to `purpose`, which picks the backend the key can call: - `purpose: api` - compute, desktop, files, CUA, OSA, credits - `purpose: optimal` - AI/LLM proxy (`/v1/chat/completions`, `/v1/responses`) If you need both, create two keys. They share the `msk_` family so SDK configuration is identical. ### Creating an API key ```bash curl -X POST https://api.miosa.ai/api/v1/api-keys \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Production SDK Key", "key_type": "user", "purpose": "api" }' ``` ```json { "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "key": "msk_u_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", "key_prefix": "msk_u_a1b2c3", "name": "Production SDK Key", "key_type": "user", "key_purpose": "api", "rate_limit_rpm": 60, "status": "active", "created_at": "2026-04-17T00:00:00Z" } } ``` The raw key is **only returned once** at creation. Store it securely. If lost, revoke it and create a new one - the original cannot be recovered. User-role callers can only mint `user` keys. Admin and platform callers may mint any `key_type`. To issue admin keys programmatically, use the admin variant at `POST /admin/api-keys` (operator-internal - not part of the public docs). ### Using an API key Pass the key in the `Authorization` header: ```bash curl https://api.miosa.ai/api/v1/computers \ -H "Authorization: Bearer msk_u_a1b2c3d4e5f6..." ``` All five SDKs read `MIOSA_API_KEY` from the environment by default: ```python from miosa import Miosa # Explicit key client = Miosa(api_key="msk_u_...") # Or from environment: export MIOSA_API_KEY="msk_u_..." client = Miosa() ``` ```ts // Explicit key const miosa = new Miosa({ apiKey: 'msk_u_...' }); ``` ```go // Explicit key client := miosa.NewClient("msk_u_...") // Or from environment client := miosa.NewClient(os.Getenv("MIOSA_API_KEY")) ``` ```elixir # Explicit key client = Miosa.client("msk_u_...") # Or from environment client = Miosa.client(System.fetch_env!("MIOSA_API_KEY")) ``` ```java MiosaClient miosa = new MiosaClient("msk_u_..."); ``` ### Managing keys | Action | Endpoint | |--------|----------| | List your keys | `GET /api/v1/api-keys` | | Create a key | `POST /api/v1/api-keys` | | Revoke a key | `DELETE /api/v1/api-keys/{id}` | Revoking a key flips its status to `revoked`. The change is cached and enforced within seconds - subsequent requests return `401 Unauthorized`. Revocation is irreversible; create a new key if you need access restored. ### Admin keys `msk_a_*` and `msk_p_*` keys unlock everything under `/api/v1/admin/*` - user management, tenant operations, credit grants, fleet-wide computer control, hosted model routing. These are gated behind the same `RequireAdmin` plug as admin JWTs. User-role callers hitting an admin endpoint receive `403 Forbidden`. Admin endpoints are operator-internal and not documented publicly. ## Preview tokens Preview tokens (`mp_`) are short-lived signed tokens for embedding a sandbox preview in an ` ``` MIOSA handles pixel streaming and input forwarding inside the iframe. The user clicks or types in the iframe and those events go to the desktop. ## What's streamed - **Pixels** - encoded video tuned for desktop content. - **Cursor position** - overlay rendered client-side. - **Audio** - optional, off by default. Pass `?audio=1` if your app needs it. - **Clipboard** - bidirectional, gated by the iframe's `allow="clipboard-read; clipboard-write"`. ## Read-only mode For "user watches the agent" UI where the end-user shouldn't interact with the desktop: ```typescript const ticket = await computer.terminalTicket({ readOnly: true }) ``` Input events from the iframe are dropped at the stream gateway. Useful for compliance-sensitive flows where only the AI agent should touch the screen. ## Bandwidth Default codec / bitrate is tuned for typical desktop content (~500 Kbps - 2 Mbps). For high-motion content (video playback inside the VM), the codec adapts but expect higher bitrate. The stream gateway is geo-routed to the user's nearest region for lower latency. ## Lifetime Tickets are short-lived (default 1 hour, max 24 hours; configurable per plan). When a ticket expires the stream disconnects; your frontend should watch `expires_at` and re-mint before that. Multiple tickets per computer are fine. Multiple browsers can watch the same desktop simultaneously - they all see the same pixels. ## Custom domains / branded streaming White-label customers can configure `stream.<your-domain>` to front MIOSA's stream gateway, so end users see your brand in the iframe URL. Same DNS / TLS flow as [Deployment Domains](/docs/deploy/domains/), different target. Contact support to set up. ## CSP The stream gateway emits `Content-Security-Policy` allowing the stream to be embedded from approved origins: - Default: `*.miosa.app`, `*.miosa.ai`, `localhost:4000`. - For white-label: add your platform origin via tenant config. If your iframe doesn't render, check the parent page's CSP isn't blocking the iframe and that the stream gateway CSP includes your origin. ## Audit Each ticket issuance emits an audit event with: - The computer ID - The token prefix - Issuing API key - External attribution If a ticket is leaked, you can revoke all outstanding tickets for a computer: ```http DELETE /api/v1/computers/$CID/terminal-tickets ``` ## See also - [Overview](/docs/computers/overview/) - what you're embedding - [Desktop Control](/docs/computers/desktop/) - programmatic control of the same desktop - [Browser Tokens](/docs/platform/browser-tokens/) - analogous pattern for sandbox previews --- # Computers (Desktop) URL: https://miosa.ai/docs/computers/overview Fallback URL: https://miosa.roberto-c49.workers.dev/docs/computers/overview Source: src/routes/docs/computers/overview/+page.md Description: Full Linux desktops in the cloud - Xfce, Firefox, terminal - controlled via 28 Python SDK methods including screenshot, click, type, scroll, clipboard, windows, shell, and more. A **Computer** is a cloud desktop environment. Use it when you need a real GUI surface: browser automation, computer-use AI agents, RPA, or screenshot-driven testing. For code execution, dev servers, and most AI agent build loops, [Sandboxes](/docs/develop/sandboxes/) are cheaper and faster. Choose based on whether your workload needs a rendered desktop. ## When to use Computer vs Sandbox | Use case | Pick | |---|---| | Run untrusted code, dev server, `npm install` | **Sandbox** | | Computer-use agent, GUI automation | **Computer** | | Browser automation that needs a real rendered DOM | **Computer** | | AI-agent build loop (code-gen, hot reload, preview) | **Sandbox** | | Screenshot-based UI testing | **Computer** | | Headless Playwright / Selenium | **Sandbox** (cheaper) | | Controlling desktop apps (Figma, Slack, etc.) | **Computer** | ## What is in the box - **Xfce desktop** with Firefox and common Linux apps pre-installed. - **Terminal** - `bash`, `python`, standard CLI toolchain. - **Persistent storage** - files survive stop/start cycles via snapshot restore. - **VNC + WebSocket streaming** - embed the desktop live in a browser via MIOSA's pixel-stream protocol. - **OSA agent** - pre-installed, disabled by default. Activate it for autonomous desktop task execution. ## Quick example ```python client = miosa.Miosa(api_key=os.environ["MIOSA_API_KEY"]) # Create and start a desktop computer computer = client.computers.create(name="agent-1", template="miosa-desktop", size="small") computer.start() # Capture the screen, interact with it png_bytes = computer.screenshot() computer.left_click(640, 480) computer.double_click(100, 200) computer.type("hello world") computer.key("Return") computer.scroll("down", clicks=3) computer.hotkey("ctrl", "c") computer.bash("firefox &") ``` ```typescript const miosa = new Miosa({ apiKey: process.env.MIOSA_API_KEY! }) // Create and start a desktop computer const computer = await miosa.computers.create({ name: "agent-1", templateType: "miosa-desktop", size: "small", }) await computer.start() // Capture the screen, interact with it const png = await computer.screenshot() await computer.leftClick(640, 480) await computer.doubleClick(100, 200) await computer.type("hello world") await computer.key("Return") await computer.scroll({ direction: "down", clicks: 3 }) await computer.hotkey("ctrl", "c") await computer.bash("firefox &") ``` ## Desktop action reference All 28 methods available on a running computer: | Group | Method | Description | |---|---|---| | **Screen** | `screenshot()` | Capture full desktop as PNG bytes | | | `screenshot_base64()` | Same, base64-encoded - ready for LLM vision APIs | | **Click** | `click(x, y)` | Generic click (defaults to left button) | | | `left_click(x, y)` | Left-button click | | | `right_click(x, y)` | Right-button click (context menu) | | | `double_click(x, y)` | Double-click | | **Mouse** | `move_cursor(x, y)` | Move cursor without clicking | | | `mouse_down(x, y)` | Press and hold the mouse button | | | `mouse_up(x, y)` | Release a held mouse button | | | `drag(from_x, from_y, to_x, to_y)` | Click-drag between two coordinates | | **Keyboard** | `type(text)` | Type a string at the current focus | | | `key(key)` | Send a single key (e.g. `"Return"`, `"ctrl+a"`) | | | `hotkey(*keys)` | Simultaneous key combo (e.g. `"ctrl", "c"`) | | | `key_down(key)` | Press and hold a key | | | `key_up(key)` | Release a held key | | **Scroll** | `scroll(direction, clicks)` | Scroll `up`/`down`/`left`/`right` | | | `scroll_up/down/left/right(clicks)` | Convenience scroll methods | | **Clipboard** | `get_clipboard()` | Read clipboard text | | | `set_clipboard(text)` | Write text to clipboard | | **Screen info** | `get_screen_size()` | Desktop resolution `{width, height}` | | | `get_cursor_position()` | Current cursor `{x, y}` in normalized coords | | **Windows** | `windows()` | List open windows with IDs, titles, positions | | | `launch(app)` | Open an installed app by name | | | `focus_window(id)` | Bring a window to the foreground | | | `get/set_window_size(id, ...)` | Read or set window dimensions | | | `get/set_window_position(id, ...)` | Read or set window position | | | `maximize/minimize/close_window(id)` | Change window state | | **Environment** | `get_desktop_environment()` | DE name and version (e.g. `xfce4`) | | | `set_wallpaper(path)` | Set desktop background from a VM file path | | | `get_accessibility_tree()` | AT-SPI element tree for structured agent perception | | **Shell** | `bash(cmd)` | Execute a shell command inside the VM | | | `python(code)` | Execute a Python snippet inside the VM | | | `write_file(path, content)` | Write a file into the VM's filesystem | | | `read_file(path)` | Read a file from the VM's filesystem | See [Desktop Control](/docs/computers/desktop/) for full examples, parameter details, and coordinate system documentation. ## Workspace-scoped creation Computers can be created inside a named workspace so that resources are isolated and billed separately. Pass `external_workspace_id` to attribute the computer to a tenant in your platform. ```python client = miosa.Miosa(api_key=os.environ["MIOSA_API_KEY"]) computer = client.computers.create( name="agent-1", template="miosa-desktop", size="small", external_workspace_id="acme-corp", # your tenant identifier metadata={"agent_run": "run-2026-05-17"}, ) computer.start() ``` ```typescript const miosa = new Miosa({ apiKey: process.env.MIOSA_API_KEY! }) const computer = await miosa.computers.create({ name: "agent-1", templateType: "miosa-desktop", size: "small", externalWorkspaceId: "acme-corp", metadata: { agentRun: "run-2026-05-17" }, }) await computer.start() ``` `external_workspace_id` is a free-form string you control - use your own tenant/org identifier. Usage is tracked per workspace in the billing dashboard so you can attribute compute costs to individual customers. ## White-label desktops Computers support per-tenant desktop branding. After starting a computer, push a wallpaper and apply it with two SDK calls. The wallpaper persists across stop/start cycles because it is stored in the VM's snapshotted filesystem. ```python client = miosa.Miosa(api_key=os.environ["MIOSA_API_KEY"]) computer = client.computers.create( name="acme-agent", template="miosa-desktop", size="small", external_workspace_id="acme-corp", ) computer.start() # Push the tenant wallpaper from your server's filesystem computer.write_file("/tmp/acme-wallpaper.png", open("assets/acme-bg.png", "rb").read()) computer.set_wallpaper("/tmp/acme-wallpaper.png") # Optionally verify the DE de = computer.get_desktop_environment() print(de) # {"name": "xfce4", "version": "4.18"} ``` ```typescript const miosa = new Miosa({ apiKey: process.env.MIOSA_API_KEY! }) const computer = await miosa.computers.create({ name: "acme-agent", templateType: "miosa-desktop", size: "small", externalWorkspaceId: "acme-corp", }) await computer.start() // Push and apply the tenant wallpaper const bg = fs.readFileSync("assets/acme-bg.png") await computer.writeFile("/tmp/acme-wallpaper.png", bg) await computer.setWallpaper("/tmp/acme-wallpaper.png") ``` For production deployments, bake the wallpaper directly into a custom template snapshot so every new computer boots with the correct branding - no post-boot setup required. See the Templates documentation for details. ## How it fits together Diagram: graph LR SDK["Python / TypeScript SDK"] -->|REST| API["MIOSA API"] API --> Desktop["Desktop environment"] Desktop -->|input| Session["Desktop session"] Desktop -->|frame| Stream["Pixel stream"] Stream -->|WebSocket| Browser["Browser iframe"] Your code talks to the MIOSA API. MIOSA routes actions to an desktop command service. The command service drives the X11 session and returns screenshots or action confirmations. For embedded views, MIOSA streams the desktop over WebSocket to a browser iframe. ## Embed the desktop in a browser Mint a stream token from your backend, then pass the URL to a browser iframe: ```python token = computer.stream_token() # token["url"] → short-lived WebSocket stream URL # Pass this URL to your frontend; do not embed your API key. ``` ```typescript const token = await computer.streamToken() // token.url → short-lived WebSocket stream URL // ``` Never put `msk_*` in a `