# 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.
Last updated: June 9, 2026
---
## 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 `