# MIOSA Documentation - Full Text
Generated: 2026-05-18T15:03:14.334Z
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, and deploy AI-generated apps on managed compute — one workspace API key, your customers attributed.
# MIOSA Documentation
MIOSA is the AI compute platform — a unified surface for building, deploying, and operating AI-powered apps. Spin up isolated sandboxes for code-gen agents, publish versioned deployments with rollbacks, attach managed Postgres and storage, and embed the whole thing into your own product without surfacing the MIOSA brand to your users.
## Get started with MIOSA
Spin up your first sandbox in under two minutes. Connect the [Python](/docs/sdks/python/) or [TypeScript](/docs/sdks/typescript/) SDK, attribute resources to your downstream tenants with `external_workspace_id`, and publish to a stable production URL with one call.
The [MIOSA SDK ecosystem](/docs/sdks/python/) provides idiomatic clients for five languages — Python, TypeScript, Go, Java, and Elixir — all wrapping the same REST API.
Connect your [Git workflow](/docs/develop/sandboxes/) and your AI agent can write code directly into the sandbox, with previews proxied live to your browser before you publish.
See the [Quickstart](/docs/quickstart/) for a full walkthrough, or the [Concepts](/docs/concepts/) page for the resource model.
---
## Quick references
---
## Build your applications
Use one or more of the following primitives to build your application:
- **[Sandboxes](/docs/develop/sandboxes/)**: Mutable Firecracker microVMs that boot warm in ~150 ms. Where your AI agent writes code, runs builds, and streams output.
- **[Previews](/docs/develop/previews/)**: Live, share-tokened URLs proxied straight to a sandbox port. No build step, no preview-deploy round-trip.
- **[Templates](/docs/develop/templates/)**: Pre-configured sandbox images (Next.js, React, Python, Go, more) or your own BuildSpec for reusable team templates.
- **[Files and Exec](/docs/develop/files-and-exec/)**: Write, read, and list files; spawn processes; stream stdout/stderr — all over a single REST surface.
- **[AI Agent integration](/docs/guides/agent/)**: Hook your agent loop to the sandbox API. The agent decides what to do; MIOSA executes.
---
## Use MIOSA's infrastructure
Add AI-native compute to your platform:
- **[Computers](/docs/computers/overview/)** New: Full Linux desktop VMs with screenshot, click, and type APIs for computer-use agents.
- **[Snapshots](/docs/api-reference/snapshots/)**: Fork or rewind a sandbox state. Restore in under 200 ms.
- **[Sandbox Templates BuildSpec](/docs/develop/templates/)** New: Author and publish reusable sandbox images for your team or the public catalog.
- **[Events (SSE)](/docs/api-reference/events/)**: Subscribe to live build, publish, and runtime events with short-lived auth tickets.
---
## Embed MIOSA in your platform
Run MIOSA underneath your own product without exposing the MIOSA brand to your end users:
- **[Platform overview](/docs/platform/overview/)**: How MIOSA maps one workspace key to many downstream tenants.
- **[External Attribution](/docs/platform/attribution/)**: Tag every resource with `external_workspace_id`, `external_user_id`, `external_project_id`. Usage and billing roll up automatically.
- **[Browser Tokens](/docs/platform/browser-tokens/)**: Short-lived, scoped tokens that let browser code call MIOSA APIs without exposing your workspace key.
- **[Usage and Billing](/docs/platform/usage-and-billing/)**: Per-tenant usage reports, spend caps, and billing delegation.
---
## Deploy and scale
Ship code to production, manage versions, and operate at scale:
- **[Deployments](/docs/deploy/overview/)**: The runtime object — points at one immutable Version at a time, exposes a public URL.
- **[Publishing](/docs/deploy/publishing/)**: Turn a sandbox into a Version. Builder VMs, source snapshots, immutable releases, automatic promotion.
- **[Versions](/docs/deploy/versions/)** and **[Releases](/docs/deploy/releases/)**: How immutable build artifacts and the metadata pointing at them are modeled.
- **[Runtime Instances](/docs/deploy/runtime-instances/)**: Compute pods for dynamic deployments. Horizontal scale, health checks, autoscale-to-zero (planned).
- **[Custom Domains](/docs/deploy/domains/)**: DNS + TLS for production. Per-deployment or per-Version subdomains; CNAME or apex.
- **[Rollback](/docs/deploy/rollback/)**: Point a deployment at a previous Version in one call. Sub-second.
---
## Data services
Managed data attached to your sandbox as env vars at boot:
- **[Postgres](/docs/data/postgres/)**: Provision a database; `DATABASE_URL` injected. Branch and snapshot for staging environments (planned).
- **[Storage / Buckets](/docs/data/storage/)**: S3-compatible object storage. Pre-signed URLs for browser uploads.
- **[Redis / Cache](/docs/data/redis/)** Beta: Managed Redis for sessions, caches, and queues.
- **[Auth as a Service](/docs/data/auth/)** Coming soon: User auth (signup, signin, sessions, OAuth) wired into your generated apps.
- **[Volumes](/docs/data/volumes/)**: Persistent disk attached to a Runtime Instance.
---
## Explore guides and tutorials
Extend your knowledge with deep-dives:
- **[Concepts](/docs/concepts/)**: The full resource model — Sandbox, Preview, Deployment, Version, Runtime Instance, Domain.
- **[Fundamentals](/docs/fundamentals/)**: How authentication, tenancy, and the request lifecycle work.
- **[CLI](/docs/cli/)**: Scriptable access to every MIOSA surface from your terminal.
- **[Changelog](/docs/changelog/)**: What shipped, what's next.
- **[API Reference](/docs/api-reference/)**: Every public endpoint, request/response shape.
---
[View full sitemap](/docs/api-reference/)
---
# 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 — in-VM agent 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).
---
# 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 — full Firecracker microVMs with a Linux desktop, VNC streaming, and a resident envd agent that accepts exec, file, and desktop commands. 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.sandbox.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"
}
```
### 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 VNC Credentials
**`GET /api/v1/computers/{id}/vnc-credentials`**
Returns credentials for direct VNC desktop access.
### Response — `200 OK`
```json
{
"vnc_url": "https://my-computer.sandbox.miosa.ai/desktop/index.html",
"ws_url": "wss://my-computer.sandbox.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 VNC 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.sandbox.miosa.ai/desktop/index.html",
"vnc_url": "https://my-computer.sandbox.miosa.ai/desktop/index.html",
"terminal_url": "wss://my-computer.sandbox.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. This endpoint is proxied to the in-VM envd daemon.
### Response — `200 OK`
Proxied response from envd 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` | In-VM envd agent unreachable |
---
## 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 resident envd agent 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 in-VM agent (envd) |
---
## 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
---
# 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 in-VM envd agent 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 | In-VM envd agent 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 in-VM agent 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 /home/user", "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 /home/user && 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(\"/home/user\"):\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 `/home/user`, `/home/ubuntu`, and `/tmp`.
Base path: `/api/v1/computers/{id}/files`
The computer must be in `"running"` status. All operations go through the in-VM agent (envd).
---
## 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: `/home/user/` |
### Response — `201 Created`
```json
{
"success": true,
"file": {
"path": "/home/user/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=/home/user/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: `/home/user` |
### 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": "/home/user"
}
```
### 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=/home/user" \
-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=/home/user/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": "/home/user/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": "/home/user/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": "/home/user/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": "/home/user/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 `/home/user`, `/home/ubuntu`, or `/tmp`
3. **Rejection** — Paths outside these prefixes return `403 FORBIDDEN_PATH`
This prevents path traversal attacks. For example, `/home/user/../../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 in-VM envd daemon and restricted to allowed paths.
Base path: `/api/v1/computers/{id}/files`
**Allowed paths:** `/home/user`, `/home/ubuntu`, `/tmp`
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: '/home/user/hello.py',
content: 'print("Hello from MIOSA")',
});
// List the directory
const { files } = await client.files.list(computerId, '/home/user');
// Download it back
const content = await client.files.download(computerId, '/home/user/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: `/home/user/` |
### Response — `201 Created`
```json
{
"success": true,
"file": {
"path": "/home/user/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=/home/user/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": "/home/user/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": "/home/user/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: `/home/user` |
### 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": "/home/user"
}
```
```bash
curl "https://api.miosa.ai/api/v1/computers/{id}/files?path=/home/user" \
-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=/home/user/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": "/home/user/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": "/home/user/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": "/home/user/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": "/home/user/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": "/home/user/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=/home/user/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": "/home/user/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": "/home/user/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": "/home/user/old.txt", "destination": "/home/user/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": "/home/user/app.py", "destination": "/home/user/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": "/home/user/template.py", "destination": "/home/user/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": "/home/user/template.py", "destination": "/home/user/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": "/home/user/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": "/home/user/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 `/home/user`, `/home/ubuntu`, or `/tmp`
3. **Rejection** — Paths outside these prefixes return `403 FORBIDDEN_PATH`
`/home/user/../../etc/passwd` expands to `/etc/passwd` and is rejected.
---
## Common Recipes
### Scaffold a project directory
```typescript
// Create structure
await client.files.mkdir(computerId, { path: '/home/user/app/src', recursive: true });
await client.files.mkdir(computerId, { path: '/home/user/app/tests', recursive: true });
// Write files
await client.files.write(computerId, { path: '/home/user/app/src/main.py', content: mainPy });
await client.files.write(computerId, { path: '/home/user/app/requirements.txt', content: reqs });
// Make entrypoint executable
await client.files.chmod(computerId, { path: '/home/user/app/src/main.py', mode: '755' });
```
### Retrieve build artifacts
```python
# List the build output directory
files = client.files.list(computer_id, path="/home/user/app/dist")
for f in files["files"]:
if f["type"] == "file":
content = client.files.export(computer_id, path=f"/home/user/app/dist/{f['name']}")
save_artifact(f["name"], content["content_base64"])
```
---
# 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 Firecracker VM 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 /home/user/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": "/home/user/projects",
"entries": [
{
"name": "my-app",
"path": "/home/user/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
---
# Projects API
URL: https://miosa.ai/docs/api-reference/projects
Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/projects
Source: src/routes/docs/api-reference/projects/+page.md
Description: API reference for creating and managing projects inside MIOSA workspaces.
A **Project** is the app, website, lead magnet, document, workflow, or customer build inside a workspace. Projects own sandboxes, computers, deployments, databases, storage buckets, volumes, functions, jobs, auth configuration, integrations, and custom domains.
Base path: `/api/v1/projects`
If a resource create call includes `project_slug`, `project_name`, or `external_project_id`, MIOSA resolves or creates the project before creating the resource. You can also create projects explicitly with this API.
## Endpoints
| Method | Path | Description |
|---|---|---|
| `GET` | `/projects` | List projects for the organization |
| `POST` | `/projects` | Create a project inside a workspace |
| `GET` | `/projects/{id}` | Get one project |
| `PATCH` | `/projects/{id}` | Update a project |
| `GET` | `/projects/{id}/preview-domain` | Read the project preview domain |
| `PUT` | `/projects/{id}/preview-domain` | Set the project preview domain |
| `DELETE` | `/projects/{id}/preview-domain` | Clear the project preview domain |
| `GET` | `/projects/{id}/preview-domain/verify` | Check DNS readiness |
| `GET` | `/workspaces/{id}/projects` | List projects in one workspace |
---
## List Projects
**`GET /api/v1/projects`**
### Query Parameters
| Parameter | Type | Description |
|---|---|---|
| `workspace_id` | UUID | Limit results to one workspace |
### Response - `200 OK`
```json
{
"data": [
{
"id": "660e8400-e29b-41d4-a716-446655440001",
"tenant_id": "tnt_abc123",
"workspace_id": "550e8400-e29b-41d4-a716-446655440000",
"external_workspace_id": "clinic_123",
"external_project_id": "project_789",
"name": "Lead Magnet",
"slug": "lead-magnet",
"description": null,
"metadata": {},
"settings": {},
"created_at": "2026-05-18T10:00:00Z",
"updated_at": "2026-05-18T10:00:00Z"
}
],
"total": 1
}
```
```bash
curl "https://api.miosa.ai/api/v1/projects?workspace_id=550e8400-e29b-41d4-a716-446655440000" \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
## Create a Project
**`POST /api/v1/projects`**
A project must belong to a workspace. Supply one of `workspace_id`, `workspace_slug`, or `external_workspace_id`.
### Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| `workspace_id` | UUID | Conditional | Existing MIOSA workspace ID |
| `workspace_slug` | string | Conditional | Existing workspace slug |
| `external_workspace_id` | string | Conditional | Your external workspace/customer ID |
| `name` | string | Yes | Human-readable project name |
| `slug` | string | No | URL-safe identifier. Auto-derived from `name` if omitted. |
| `external_project_id` | string | No | Your project/app/document ID |
| `description` | string | No | Optional description |
| `metadata` | object | No | Caller metadata stored on the project |
### Example
```bash
curl -X POST https://api.miosa.ai/api/v1/projects \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workspace_slug": "dr-smith-clinic",
"name": "Lead Magnet",
"slug": "lead-magnet",
"external_workspace_id": "clinic_123",
"external_project_id": "project_789"
}'
```
### Response - `201 Created`
Full project object.
### Errors
| Status | Code | Cause |
|---|---|---|
| 400 | `WORKSPACE_REQUIRED` | No `workspace_id`, `workspace_slug`, or `external_workspace_id` was supplied |
| 404 | `WORKSPACE_NOT_FOUND` | Workspace selector did not resolve inside the organization |
| 422 | `VALIDATION_FAILED` | Name, slug, or external project ID failed validation |
---
## Get a Project
**`GET /api/v1/projects/{id}`**
Returns one project if it belongs to the authenticated organization.
```bash
curl https://api.miosa.ai/api/v1/projects/{id} \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
## Update a Project
**`PATCH /api/v1/projects/{id}`**
### Request Body
| Field | Type | Description |
|---|---|---|
| `name` | string | New display name |
| `slug` | string | New URL-safe slug, unique inside the workspace |
| `description` | string | New description (`null` to clear) |
| `external_project_id` | string | Your project/app/document ID |
| `external_workspace_id` | string | Your customer/workspace ID |
| `metadata` | object | Replacement metadata map |
| `settings` | object | Replacement settings map. Prefer the preview-domain endpoint for domain changes. |
```bash
curl -X PATCH https://api.miosa.ai/api/v1/projects/{id} \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Summer Lead Magnet"}'
```
---
## Use Projects on Resource Create
Most create endpoints accept the same ownership fields:
```json
{
"workspace_id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "660e8400-e29b-41d4-a716-446655440001",
"external_workspace_id": "clinic_123",
"external_project_id": "project_789"
}
```
You can use slugs instead of UUIDs:
```json
{
"workspace_slug": "dr-smith-clinic",
"workspace_name": "Dr. Smith Clinic",
"project_slug": "lead-magnet",
"project_name": "Lead Magnet"
}
```
This applies to sandboxes, computers, deployments, databases, storage buckets, volumes, edge functions, cron jobs, project auth, and integrations. Derived records inherit ownership automatically.
---
## Project Preview Domain
**`PUT /api/v1/projects/{id}/preview-domain`**
Sets a project-level base domain for generated URLs. This overrides the workspace preview domain and organization preview domain for resources in this project.
```bash
curl -X PUT https://api.miosa.ai/api/v1/projects/{id}/preview-domain \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"preview_domain":"program.drsmithclinic.com"}'
```
Response:
```json
{
"scope": "project",
"id": "660e8400-e29b-41d4-a716-446655440001",
"preview_domain": "program.drsmithclinic.com",
"effective_domain": "program.drsmithclinic.com",
"status": "pending_dns",
"dns_status": "pending",
"url_examples": {
"default_preview": "https://.program.drsmithclinic.com",
"port_preview": "https://3000-.sandbox.program.drsmithclinic.com",
"deployment": "https://.program.drsmithclinic.com"
}
}
```
Required DNS records:
| Record type | Name | Value |
|---|---|---|
| `CNAME` | `*` | `proxy.miosa.ai` |
| `CNAME` | `*.sandbox` | `proxy.miosa.ai` |
Verify:
```bash
curl https://api.miosa.ai/api/v1/projects/{id}/preview-domain/verify \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
Clear and inherit from the workspace/organization fallback:
```bash
curl -X DELETE https://api.miosa.ai/api/v1/projects/{id}/preview-domain \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
## See also
- [Workspaces API](/docs/api-reference/workspaces/) - parent resource
- [Ownership and Attribution](/docs/platform/attribution/) - how IDs flow to usage and events
- [Sandboxes API](/docs/api-reference/sandboxes/) - create resources inside a project
- [Deployments API](/docs/api-reference/deployments/) - publish projects to URLs
---
# Regions API
URL: https://miosa.ai/docs/api-reference/regions
Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/regions
Source: src/routes/docs/api-reference/regions/+page.md
Description: List available regions and pin Computers and Sandboxes to a specific region.
MIOSA Computers and Sandboxes run in geographic regions. Every Computer (and every Sandbox, since a Sandbox is a lightweight Computer flavor) is created in exactly one region and stays there for its lifetime.
Base path: `/api/v1/regions`
---
## Overview
- **3 regions are live today.** All have equal status — none are preview, none are restricted.
- **Default region** when you don't specify one: `us-west-la`.
- **All VM sizes** (XS, S, M, L, XL) are available in every region for both Computers and Sandboxes.
- **Latency** is roughly equidistant for US users — pick by data residency or proximity preference rather than performance.
- **No multi-region resources.** A Computer lives in exactly one region. To run workloads in multiple regions, create multiple Computers.
---
## Available Regions
| Slug | Location | City | Status |
|------------------|-------------------|------------------|--------|
| `us-west-la` | US West | Los Angeles, CA | Live |
| `us-east-ny` | US East | New York, NY | Live |
| `us-mia` | US East 2 | Miami | Live |
The region slug is what you pass on create and what comes back on read — every Computer and Sandbox response includes a `region` field, e.g. `"region": "us-west-la"`.
---
## Specify a region on create
Pass `region` in the request body when creating a Computer or Sandbox. If omitted, the default (`us-west-la`) is used.
```python
computer = client.computers.create(
name="ny-build-box",
size="M",
region="us-east-ny",
)
print(computer.region) # "us-east-ny"
```
```typescript
const computer = await client.computers.create({
name: 'ny-build-box',
size: 'M',
region: 'us-east-ny',
});
console.log(computer.region); // "us-east-ny"
```
```bash
curl -X POST https://api.miosa.ai/api/v1/computers \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "ny-build-box",
"size": "M",
"region": "us-east-ny"
}'
```
The same `region` field works on `POST /api/v1/sandboxes`.
### Errors
| Status | Error | Cause |
|--------|----------------------|------------------------------------------------|
| 400 | `invalid_region` | Slug not in the list above |
| 409 | `region_unavailable` | Transient capacity issue — retry or pick another |
---
## Default region behavior
If `region` is omitted from the create request:
- Computers default to **`us-west-la`**.
- Sandboxes default to **`us-west-la`**.
- The response still includes the resolved `region` so you always know where the resource lives.
You can change your tenant's default region in dashboard settings — that override applies to API calls that omit `region`.
---
## List regions
**`GET /api/v1/regions`**
Returns the regions available to your tenant. Useful for region pickers in UIs.
### Response — `200 OK`
```json
{
"regions": [
{
"slug": "us-west-la",
"name": "US West (Los Angeles)",
"city": "Los Angeles",
"country": "US",
"status": "live",
"default": true
},
{
"slug": "us-east-ny",
"name": "US East (New York)",
"city": "New York",
"country": "US",
"status": "live",
"default": false
},
{
"slug": "us-mia",
"name": "US East 2 (Miami)",
"city": "Miami",
"country": "US",
"status": "live",
"default": false
}
]
}
```
### Response Fields
| Field | Type | Description |
|------------|---------|-------------------------------------------------------|
| `slug` | string | Stable identifier — pass this as `region` on create |
| `name` | string | Human-readable label (safe to render in UIs) |
| `city` | string | City the region is hosted in |
| `country` | string | Two-letter ISO country code |
| `status` | string | Always `"live"` today — reserved for future states |
| `default` | boolean | `true` for exactly one region (the tenant default) |
```bash
curl https://api.miosa.ai/api/v1/regions \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
The list endpoint is unauthenticated-friendly — you can use a short-lived ticket or a public token to populate region pickers without exposing your `msk_*` key.
---
## FAQ
### Why these 3 regions?
We picked Los Angeles, New York, and Miami to cover the US west coast, east coast, and central corridor. The three sites are roughly equidistant for the median US user, and they each sit on independent network and power footprints — so a regional outage at one doesn't take the others down.
### How do I pick a region?
Pick by **data residency** first. If you have a customer or compliance requirement that pins data to a coast, that decides for you.
If you have no residency requirement, pick by **proximity to your users or your CI runners** — shorter physical distance means lower RTT for VNC streaming and PTY round-trips. Compute performance itself is identical across regions.
### Can a Computer span multiple regions?
No. A Computer (or Sandbox) lives in exactly one region for its lifetime. To run workloads in multiple regions:
- Create one Computer per region.
- Use OpenComputers federation if you want a single logical workspace across them (see [OpenComputers](/docs/api-reference/open-computers/)).
There's no live migration between regions today.
### Will more regions ship?
Yes — EU and APAC regions are on the roadmap. When they ship they'll appear in `GET /api/v1/regions` with the same `"status": "live"` shape. Watch the [changelog](/docs/changelog/) for announcements.
---
## See also
- [Computers API](/docs/api-reference/computers/) — pass `region` on create
- [Sandboxes API](/docs/api-reference/sandboxes/) — sandbox region support
- [OpenComputers](/docs/api-reference/open-computers/) — federate BYOC hosts across regions
---
# API Reference / Releases
URL: https://miosa.ai/docs/api-reference/releases
Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/releases
Source: src/routes/docs/api-reference/releases/+page.md
Description: The immutable build artifact — static tarball or dynamic rootfs / OCI image, sha256-keyed.
First-class `deployment_releases` records and their dedicated endpoints arrive in Phase 2B of the deployment refactor. Today, release content is referenced via the `artifact_uri` / `artifact_sha256` fields on `deployment_versions`. This page documents the steady-state target.
A **Release** is the physical artifact produced by a build — what a [Version](/docs/api-reference/versions/) references. See [Releases](/docs/deploy/releases/) for the conceptual model.
## Endpoints (Phase 2B)
```http
GET /api/v1/releases/:id
GET /api/v1/deployments/:id/versions/:version_id/release
```
Releases are not typically created directly — they're produced by [Publish](/docs/api-reference/deployments/#publish). The endpoints above are for inspection and audit.
## Release shape
```json
{
"id": "rel_...",
"deployment_version_id": "ver_...",
"service_id": "svc_...",
"tenant_id": "...",
"kind": "static",
"storage_uri": "s3://miosa-releases/dep_xyz/rel_abc.tar.zst",
"sha256": "a5e6f0c1...",
"size_bytes": 184320,
"start_command": null,
"port": null,
"metadata": {
"build_log_uri": "s3://...",
"build_duration_ms": 8420
},
"created_at": "2026-05-14T18:19:48Z"
}
```
For dynamic releases: `kind: "oci" | "rootfs"`, `start_command` and `port` populated.
## Immutability
Releases are content-addressed by sha256. A release with sha256 `abc...` is always the same bytes. This is what makes [Rollback](/docs/deploy/rollback/) trivial and horizontal scaling identical across instances.
A release is never mutated. To "change" it, publish a new version, which produces a new release.
## Garbage collection
Static release artifacts older than the retention window (default 30 days for archived versions) are eligible for GC. The version row remains for audit; the artifact itself may be removed from object storage.
If you attempt to rollback to a version whose artifact has been GC'd, MIOSA returns `410 Gone`. Active versions (any deployment's `active_version_id`) are never GC'd.
## Permissions
| Action | Scope |
|---|---|
| Get | `deployments:read` |
## See also
- [Versions](/docs/api-reference/versions/) — parent resource
- [Deployments](/docs/api-reference/deployments/) — top-level resource
- [Releases](/docs/deploy/releases/) — conceptual model
---
# Sandboxes API
URL: https://miosa.ai/docs/api-reference/sandboxes
Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/sandboxes
Source: src/routes/docs/api-reference/sandboxes/+page.md
Description: API reference for creating, managing, and executing code in MIOSA sandboxes — ephemeral Firecracker microVMs.
Sandboxes are ephemeral Firecracker microVMs that restore from a pre-seeded snapshot, accept exec and file operations, expose previews, and are billed by compute/runtime usage. They are designed for AI-agent code execution workloads, artifact generation, and app previews.
Base path: `/api/v1/sandboxes`
All endpoints require `Authorization: Bearer <api_key>`. Get a key from the [MIOSA dashboard](https://app.miosa.ai/settings/api-keys).
For higher-level access, use the official SDKs: [Python](/docs/sdks/python/), [TypeScript](/docs/sdks/typescript/), [Go](/docs/sdks/go/), [Java](/docs/sdks/java/), [Elixir](/docs/sdks/elixir/).
---
## List Sandbox Templates
**`GET /api/v1/sandbox-templates`**
Returns the platform-managed sandbox template catalog. Pass one of these IDs as `template_id` when creating a sandbox. The catalog describes what your app should run: workdir, install command, start command, preview port, readiness probe, and artifact paths.
```bash
curl https://api.miosa.ai/api/v1/sandbox-templates \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
### Response — `200 OK`
```json
{
"default_template_id": "miosa-sandbox",
"data": [
{
"id": "nextjs",
"name": "Next.js",
"image_id": "miosa-sandbox",
"category": "web",
"cpu_count": 2,
"memory_mb": 2048,
"disk_mb": 4096,
"workdir": "/workspace",
"preview_port": 3000,
"install_command": "npm install",
"start_command": "npm run dev -- --hostname 0.0.0.0 --port 3000",
"readiness_probe": {
"type": "http",
"url": "http://127.0.0.1:3000"
}
}
]
}
```
Use `GET /api/v1/sandbox-templates/{id}` to fetch one template. Add `?include_aliases=true` on the list endpoint to include compatibility aliases such as `python-3.12`, `react-vite`, and `static`.
---
## Sandbox Template BuildSpec
Phase 4 introduces the public BuildSpec contract for reusable sandbox templates.
The API validates and normalizes BuildSpecs, persists tenant-owned template
records, queues template build records, and runs a builder worker that turns
the normalized BuildSpec into a Firecracker rootfs artifact.
### Get the BuildSpec schema
**`GET /api/v1/sandbox-templates/build-spec`**
```bash
curl https://api.miosa.ai/api/v1/sandbox-templates/build-spec \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
### Validate a BuildSpec
**`POST /api/v1/sandbox-templates/validate`**
```bash
curl -X POST https://api.miosa.ai/api/v1/sandbox-templates/validate \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"build_spec": {
"from": "node:22-bookworm",
"vcpu": 2,
"memoryMib": 2048,
"diskMib": 8192,
"steps": [
{"run": "corepack enable"},
{"workdir": "/workspace"}
],
"env": {"NODE_ENV": "development"},
"workdir": "/workspace",
"user": "root",
"startCmd": "pnpm dev --host 0.0.0.0 --port 3000",
"readyCmd": "curl -f http://127.0.0.1:3000",
"previewPort": 3000,
"artifactPaths": ["/workspace"]
}
}'
```
Response:
```json
{
"valid": true,
"build_spec": {
"from": "node:22-bookworm",
"vcpu": 2,
"memoryMib": 2048,
"diskMib": 8192,
"steps": [{"run": "corepack enable"}, {"workdir": "/workspace"}],
"env": {"NODE_ENV": "development"},
"workdir": "/workspace",
"user": "root",
"startCmd": "pnpm dev --host 0.0.0.0 --port 3000",
"readyCmd": "curl -f http://127.0.0.1:3000",
"previewPort": 3000,
"artifactPaths": ["/workspace"]
}
}
```
Validation errors use stable codes:
| Code | Meaning |
|------|---------|
| `REQUIRED` | A required field such as `from` is missing. |
| `UNSUPPORTED_BASE_IMAGE` | Alpine/distroless bases are rejected for public agent templates. |
| `OUT_OF_RANGE` | Resource values are outside tenant-safe defaults. |
| `INVALID_INTEGER` | Numeric fields are not integers. |
| `INVALID_PORT` | Preview port is outside `1..65535`. |
| `INVALID_PATH` | Workdir or artifact path is not absolute. |
| `INVALID_STEP` | A build step is not a supported object. |
### Create a custom template
**`POST /api/v1/sandbox-templates`**
```bash
curl -X POST https://api.miosa.ai/api/v1/sandbox-templates \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Agent Web App",
"slug": "agent-web-app",
"description": "Reusable Node app sandbox",
"build_spec": {
"from": "node:22-bookworm",
"vcpu": 2,
"memoryMib": 2048,
"diskMib": 8192,
"startCmd": "pnpm dev --host 0.0.0.0 --port 3000",
"readyCmd": "curl -f http://127.0.0.1:3000",
"previewPort": 3000,
"artifactPaths": ["/workspace"]
}
}'
```
### Queue a template build
**`POST /api/v1/sandbox-templates/{template_id}/builds`**
```bash
curl -X POST https://api.miosa.ai/api/v1/sandbox-templates/tpl_123/builds \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
```
Build records start in `queued`. The builder worker moves them through
`building`, `snapshotting`, `certifying`, `distributing`, and then `ready` or `failed`.
`ready` means the rootfs exists, a Firecracker memory snapshot has been seeded,
certification passed, and the rootfs/snapshot artifacts have been published to
the fleet artifact source. Each compute host reconciles or fetches the artifacts
before it boots that custom template. After that, pass the template slug to
`POST /api/v1/sandboxes` as `template_id`.
If a custom template exists but is not ready, sandbox creation returns
`409 TEMPLATE_NOT_READY`. If snapshot seeding fails, the build fails with
`SNAPSHOT_FAILED`; if certification fails, it fails with
`CERTIFICATION_FAILED`.
### Read template builds
```bash
curl https://api.miosa.ai/api/v1/sandbox-templates/tpl_123/builds \
-H "Authorization: Bearer $MIOSA_API_KEY"
curl https://api.miosa.ai/api/v1/sandbox-template-builds/build_123 \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
### Observe and control template builds
```bash
curl https://api.miosa.ai/api/v1/sandbox-template-builds/build_123/logs \
-H "Authorization: Bearer $MIOSA_API_KEY"
# Browsers/EventSource cannot send Authorization headers. Issue a short-lived
# SSE ticket first, then pass it as ?ticket=...
curl -X POST https://api.miosa.ai/api/v1/auth/sse-ticket \
-H "Authorization: Bearer $MIOSA_API_KEY"
curl -N "https://api.miosa.ai/api/v1/sandbox-template-builds/build_123/logs/stream?ticket=$SSE_TICKET" \
-H "Accept: text/event-stream"
curl -X POST https://api.miosa.ai/api/v1/sandbox-template-builds/build_123/cancel \
-H "Authorization: Bearer $MIOSA_API_KEY"
curl -X POST https://api.miosa.ai/api/v1/sandbox-template-builds/build_123/retry \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
`/logs` returns persisted lifecycle events from build metadata. `/logs/stream`
opens an SSE stream that first replays persisted events, then emits live
`build_event` messages until the build reaches `ready`, `failed`, or
`cancelled`.
Example SSE payload:
```text
event: build_event
data: {"event":"snapshotting","state":"snapshotting","at":"2026-05-13T10:00:00Z"}
```
Retry is allowed for `failed` or `cancelled` builds. Cancel is accepted for
non-terminal builds and records `BUILD_CANCELLED`.
### BuildSpec fields
| Field | Type | Description |
|-------|------|-------------|
| `from` | string | OCI base image. Debian/Ubuntu/Bookworm bases are recommended. |
| `vcpu` | integer | Template-owned vCPU count. |
| `memoryMib` | integer | Template-owned memory in MiB. |
| `diskMib` | integer | Template-owned root disk in MiB. |
| `steps` | array | Ordered build steps. Each step may define `run`, `workdir`, or `env`. |
| `env` | object | Runtime-default environment variables. |
| `workdir` | string | Runtime working directory. Defaults to `/workspace`. |
| `user` | string | Runtime user. Defaults to `root`. |
| `startCmd` | string | Long-running command used by `/template/start`. |
| `readyCmd` | string | Readiness command for the started app. |
| `previewPort` | integer | Default preview port for `/expose` and template start. |
| `artifactPaths` | array | Paths returned by the artifact manifest. |
---
## Create a Sandbox
**`POST /api/v1/sandboxes`**
Spawns a new sandbox VM. The API returns the sandbox record immediately after the create request is accepted; poll `GET /api/v1/sandboxes/{id}` or subscribe to events until `state` is `running` and `ready` is `true`.
The current production alias, `miosa-sandbox`, points at `miosa-sandbox`. On the deployed six-host fleet, the snapshot path benchmarks at **48 ms p50 / 51 ms p95 runtime boot** and **156 ms p50 / 208 ms p95 create-to-ready**.
### Auth
Bearer token required.
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `template_id` | string | No | Boot template. Defaults to `miosa-sandbox`. See available templates below. |
| `cpu_count` | integer | No | vCPU count. Default `1`, default cap `4`. |
| `memory_mb` | integer | No | RAM in MB. Default `1024`, default cap `8192`. |
| `disk_mb` | integer | No | Disk in MB. Default `3072`, default cap `10240`. |
| `timeout_sec` | integer | No | Max wall-clock seconds before force-destroy. Day-long/always-on use is available when billing and policy allow it. |
| `env` | object | No | Key-value env vars injected at boot. |
| `metadata` | object | No | Arbitrary caller-supplied metadata stored on the record. |
| `auto_start` | boolean | No | If `true`, MIOSA starts the selected template after the sandbox reaches `running`. Generated-app platforms usually keep this false, write files first, then call `/template/start`. |
| `workspace_id` | UUID | No | Existing MIOSA workspace that owns the sandbox. Defaults to the organization default workspace. |
| `workspace_slug` | string | No | Existing or auto-created workspace slug. |
| `workspace_name` | string | No | Workspace display name if auto-created. |
| `project_id` | UUID | No | Existing MIOSA project that owns the sandbox. Defaults to the workspace default project. |
| `project_slug` | string | No | Existing or auto-created project slug inside the workspace. |
| `project_name` | string | No | Project display name if auto-created. |
| `external_workspace_id` | string | No | Your customer/account/workspace ID. |
| `external_user_id` | string | No | Your end-user ID. |
| `external_project_id` | string | No | Your project/app/document ID. |
**Available templates:**
| `template_id` | Description |
|---------------|-------------|
| `miosa-sandbox` | Stable production alias for the current sandbox image. |
| `nextjs` | Next.js app preview profile. |
| `vite-react` | Vite React app preview profile. |
| `python` | Python script/artifact generation profile. |
| `streamlit` | Streamlit data app preview profile. |
| `gradio` | Gradio ML/demo app preview profile. |
| `static-html` | Static HTML/CSS/JS preview profile. |
### Request Headers
| Header | Description |
|--------|-------------|
| `Idempotency-Key` | Client-generated key (UUID recommended). Same key within 24 h returns the existing sandbox instead of creating a new one. |
### Response — `201 Created`
```json
{
"id": "sbx_01j9xr2t4fk8me3n5q",
"tenant_id": "tnt_abc123",
"owner_id": "usr_def456",
"workspace_id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "660e8400-e29b-41d4-a716-446655440001",
"external_workspace_id": "clinic_123",
"external_user_id": "dr-smith-456",
"external_project_id": "project_789",
"template_id": "miosa-sandbox",
"image_id": "miosa-sandbox",
"state": "provisioning",
"ready": false,
"cpu_count": 1,
"memory_mb": 1024,
"disk_size_mb": 3072,
"boot_path": null,
"boot_ms": null,
"preview_url": "https://sbx01j9x.sandbox.miosa.app",
"timeout_sec": 300,
"total_runtime_sec": null,
"metadata": {},
"created_at": "2026-04-25T10:00:00Z",
"started_at": null,
"ready_at": null,
"destroyed_at": null
}
```
### Errors
| Status | Code | Cause |
|--------|------|-------|
| 400 | `INVALID_TEMPLATE` | `template_id` is not a recognized template. |
| 402 | `INSUFFICIENT_CREDITS` | Not enough credits to provision the VM. |
| 409 | `SANDBOX_LIMIT_EXCEEDED` | Tenant has reached the concurrent sandbox limit (default 10). |
| 422 | `VALIDATION_ERROR` | Invalid field values (for example, a resource request above the tenant's plan cap). |
---
## Start a Template App
**`POST /api/v1/sandboxes/{id}/template/start`**
After your platform writes generated files into `/workspace`, call this endpoint to run the selected template lifecycle. MIOSA runs the template install command, launches the start command in the background, stores PID/log paths, and returns the preview URL.
```bash
curl -X POST https://api.miosa.ai/api/v1/sandboxes/{id}/template/start \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"install":true}'
```
Response:
```json
{
"data": {
"status": "started",
"template_id": "nextjs",
"workdir": "/workspace",
"preview_port": 3000,
"preview_url": "https://abc12345.sandbox.miosa.app",
"logs_path": "/tmp/miosa-run/template.log",
"pid_path": "/tmp/miosa-run/template.pid",
"artifact_paths": ["/workspace"]
}
}
```
You can override `install_command`, `start_command`, `port`, or `workdir` in the request body when your generated project needs a custom command.
---
## Get Artifacts
**`GET /api/v1/sandboxes/{id}/artifacts`**
Returns the template artifact contract and current lifecycle metadata.
```bash
curl https://api.miosa.ai/api/v1/sandboxes/{id}/artifacts \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
The response includes `preview.url`, `preview.port`, artifact paths, and template lifecycle log/PID paths.
```bash
curl -X POST https://api.miosa.ai/api/v1/sandboxes \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"template_id": "miosa-sandbox",
"workspace_slug": "dr-smith-clinic",
"workspace_name": "Dr. Smith Clinic",
"project_slug": "lead-magnet",
"project_name": "Lead Magnet",
"external_workspace_id": "clinic_123",
"external_user_id": "dr-smith-456",
"external_project_id": "project_789",
"memory_mb": 1024,
"env": {"MY_VAR": "hello"}
}'
```
---
## List Sandboxes
**`GET /api/v1/sandboxes`**
Returns all sandboxes belonging to the authenticated tenant.
### Query Parameters
| Parameter | Type | Description |
|---|---|---|
| `workspace_id` | UUID | Filter to one MIOSA workspace |
| `project_id` | UUID | Filter to one MIOSA project |
| `external_workspace_id` | string | Filter by your customer/account ID |
| `external_user_id` | string | Filter by your end-user ID |
| `external_project_id` | string | Filter by your project/app/document ID |
| `state` | string | Filter by lifecycle state: `provisioning`, `running`, `paused`, `destroyed`, `error`. |
### Auth
Bearer token required.
### Response — `200 OK`
```json
{
"data": [
{
"id": "sbx_01j9xr2t4fk8me3n5q",
"template_id": "miosa-sandbox",
"state": "running",
"cpu_count": 1,
"memory_mb": 1024,
"created_at": "2026-04-25T10:00:00Z"
}
]
}
```
```bash
curl "https://api.miosa.ai/api/v1/sandboxes?state=running" \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
## Get a Sandbox
**`GET /api/v1/sandboxes/{id}`**
### Auth
Bearer token required.
### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | Sandbox ID. |
### Response — `200 OK`
Full sandbox object (same shape as the create response with current `state`).
### Errors
| Status | Code | Cause |
|--------|------|-------|
| 404 | `NOT_FOUND` | Sandbox does not exist or belongs to a different tenant. |
```bash
curl https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
## Destroy a Sandbox
**`DELETE /api/v1/sandboxes/{id}`**
Terminates the VM immediately and settles billing.
### Auth
Bearer token required.
### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | Sandbox ID. |
### Response — `200 OK`
```json
{
"id": "sbx_01j9xr2t4fk8me3n5q",
"state": "destroyed",
"total_runtime_sec": 42
}
```
### Errors
| Status | Code | Cause |
|--------|------|-------|
| 404 | `NOT_FOUND` | Sandbox does not exist or belongs to a different tenant. |
```bash
curl -X DELETE https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
## Execute a Command
**`POST /api/v1/sandboxes/{id}/exec`**
Runs a shell command inside the sandbox. Blocks until the process exits.
### Auth
Bearer token required.
### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | Sandbox ID. Must be in `running` state. |
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `command` | string | Yes | Shell command to execute. |
| `timeout` | integer | No | Per-command timeout in seconds. Default `30`, max `300`. |
| `working_dir` | string | No | Working directory inside the VM. Default `/root`. |
| `env` | object | No | Env vars for this invocation only. |
### Response — `200 OK`
```json
{
"data": {
"sandbox_id": "sbx_01j9xr2t4fk8me3n5q",
"stdout": "2\n",
"stderr": "",
"exit_code": 0
}
}
```
### Errors
| Status | Code | Cause |
|--------|------|-------|
| 409 | `SANDBOX_NOT_RUNNING` | Sandbox is not in `running` state. |
| 504 | — | Command timed out. |
```bash
curl -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/exec \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"command": "python3 -c \"print(1+1)\"", "timeout": 30}'
```
---
## Stream Command Output
**`POST /api/v1/sandboxes/{id}/exec/stream`**
Runs a command and streams stdout/stderr events as Server-Sent Events. Use this
for installer output, long-running agent tasks, and build logs that should show
progress before the command exits.
Request body is the same as `/exec`.
```bash
curl -N -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/exec/stream \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Accept: text/event-stream" \
-H "Content-Type: application/json" \
-d '{"command":"npm install && npm run build","working_dir":"/workspace","timeout":300}'
```
Example events:
```text
event: stdout
data: {"line":"added 342 packages"}
event: exit
data: {"exit_code":0}
```
---
## Open a Terminal Session
**`POST /api/v1/sandboxes/{id}/terminal`**
Creates a PTY session inside a running sandbox and returns a short-lived stream
token for browser WebSocket clients.
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `cols` | integer | No | Initial terminal width. Defaults to `80`. |
| `rows` | integer | No | Initial terminal height. Defaults to `24`. |
| `shell` | string | No | Shell command. Defaults to login bash when available. |
### Response — `201 Created`
```json
{
"session_id": "52d1df5a0a3a0018",
"ws_url": "wss://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/terminal/stream",
"stream_auth": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9...",
"stream_auth_expires_at": "2026-05-13T12:30:00Z"
}
```
Open the WebSocket with:
```text
wss://api.miosa.ai/api/v1/sandboxes/{id}/terminal/stream?session_id={session_id}&cols=120&rows=32&token={stream_auth}
```
Clients should send raw terminal input as binary frames. Resize events are sent
as JSON text frames:
```json
{"type":"resize","cols":120,"rows":32}
```
Delete the session when finished:
```bash
curl -X DELETE https://api.miosa.ai/api/v1/sandboxes/{id}/terminal/{session_id} \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
## Expose a Preview Port
**`POST /api/v1/sandboxes/{id}/expose`**
Returns a public, tenant-aware preview URL for a server running inside the sandbox. Use this when an agent generated a Vite/Next/FastAPI/Flask app and started it on a local port.
Inside the sandbox, bind dev servers to `0.0.0.0`, not `localhost`:
```bash
npm run dev -- --host 0.0.0.0 --port 5173
python -m http.server 8000 --bind 0.0.0.0
```
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `port` | integer | No | Port inside the sandbox to expose. If omitted, MIOSA uses the template lifecycle preview port when available. Must be `1` through `65535` when provided. |
### Response — `200 OK`
```json
{
"url": "https://5173-sbx01j9x.sandbox.miosa.app"
}
```
If the tenant has a white-label preview domain configured, the same endpoint returns that domain instead of the platform default. Default app ports such as `3000`, `5173`, `8080`, `8000`, and `80` may be returned as `https://{slug}.sandbox.{domain}`; non-default ports use `https://{port}-{slug}.sandbox.{domain}`.
### Errors
| Status | Code | Cause |
|--------|------|-------|
| 400 | `MISSING_PARAM` | `port` was omitted and the sandbox template has no default preview port. |
| 409 | `SANDBOX_NOT_RUNNING` | Sandbox is not in `running` state. |
| 422 | `INVALID_PORT` | Port is outside `1` through `65535`. |
```bash
curl -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/expose \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"port": 5173}'
```
For static artifacts such as PDFs, images, Markdown, CSV, or ZIP files, write them under `/workspace` and download them through the files API. For web apps, start a server and expose the port.
---
## Promote a Sandbox to a Deployment
**`POST /api/v1/sandboxes/{id}/deploy`**
Promotes a running sandbox into a persistent deployment URL. This is the publish step for generated-app platforms: the sandbox remains the runtime behind the deployment route, its auto-destroy timer is cancelled, and the deployment is marked `running` against the sandbox VM IP and preview port.
Use this after your generated app is serving successfully through `/template/start` or `/expose`.
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Human-readable deployment name. Used to create the deployment slug. |
| `port` | integer | No | Runtime port to route. Defaults to the template lifecycle preview port, then `80`. |
| `domain` | string | No | Custom domain to attach. |
| `custom_domain` | string | No | Alias for `domain`, used by frontend clients. |
### Response — `201 Created`
```json
{
"deployment_id": "dep_01j9xr2t4fk8me3n5q",
"url": "https://my-app-a1b2c3.acme.miosa.app",
"state": "running"
}
```
Managed deployment URLs are tenant-scoped: `https://{deployment-slug}.{tenant-slug}.miosa.app`. MIOSA persists the sandbox runtime target on the deployment and reconciles running routes, so a temporary proxy restart or admin API outage can be repaired from the database.
### Errors
| Status | Code | Cause |
|--------|------|-------|
| 400 | `MISSING_PARAM` | `name` was omitted. |
| 409 | `SANDBOX_NOT_RUNNING` | Sandbox is not in `running` state. |
| 409 | `SANDBOX_RUNTIME_UNAVAILABLE` | Sandbox is marked running but has no VM IP yet. |
| 422 | `INVALID_PORT` | Port is outside `1` through `65535`. |
| 422 | `VALIDATION_ERROR` | Deployment record validation failed. |
```bash
curl -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/deploy \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"my-app","port":8000}'
```
---
## Upload a File
**`POST /api/v1/sandboxes/{id}/files`**
Writes a file to the sandbox filesystem. Two request formats are accepted.
### Auth
Bearer token required.
### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | Sandbox ID. Must be in `running` state. |
### Request Body — JSON
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `path` | string | Yes | Absolute destination path inside the sandbox. |
| `content` | string | Yes | Base64-encoded file content. |
```json
{
"path": "/workspace/script.py",
"content": "cHJpbnQoJ2hlbGxvJyk="
}
```
### Request Body — Multipart
Alternatively, send `multipart/form-data` with fields:
| Field | Description |
|-------|-------------|
| `path` | Absolute destination path. |
| `file` | File part containing the raw content. |
### Response — `200 OK`
```json
{
"data": {
"sandbox_id": "sbx_01j9xr2t4fk8me3n5q",
"path": "/workspace/script.py",
"size": 16
}
}
```
### Errors
| Status | Code | Cause |
|--------|------|-------|
| 409 | `SANDBOX_NOT_RUNNING` | Sandbox is not in `running` state. |
| 413 | — | File exceeds the 100 MB upload limit. |
```bash
# JSON (base64-encoded content)
curl -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/files \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"path\": \"/workspace/script.py\", \"content\": \"$(base64 -w0 script.py)\"}"
# Multipart
curl -X POST https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/files \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-F "path=/workspace/script.py" \
-F "file=@script.py"
```
---
## Download a File
**`GET /api/v1/sandboxes/{id}/files/{path}`**
Downloads a file from the sandbox filesystem.
### Auth
Bearer token required.
### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | Sandbox ID. Must be in `running` state. |
| `path` | string | URL-encoded path to the file inside the sandbox (leading `/` stripped). |
### Response — `200 OK`
Raw file bytes with `Content-Type: application/octet-stream`.
### Errors
| Status | Code | Cause |
|--------|------|-------|
| 404 | `FILE_NOT_FOUND` | Path does not exist inside the sandbox. |
| 409 | `SANDBOX_NOT_RUNNING` | Sandbox is not in `running` state. |
```bash
curl -o output.json \
"https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/files/workspace%2Foutput.json" \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
## List Files
**`GET /api/v1/sandboxes/{id}/files?path=/workspace`**
Lists files and directories in a sandbox directory.
```bash
curl "https://api.miosa.ai/api/v1/sandboxes/{id}/files?path=/workspace" \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
Response:
```json
{
"data": {
"path": "/workspace",
"entries": [
{"name": "index.html", "path": "/workspace/index.html", "is_dir": false, "size": 128}
]
}
}
```
---
## Stat a File
**`POST /api/v1/sandboxes/{id}/files/stat`**
Returns metadata for one file or directory.
```bash
curl -X POST https://api.miosa.ai/api/v1/sandboxes/{id}/files/stat \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"path":"/workspace/index.html"}'
```
---
## Read Logs
**`GET /api/v1/sandboxes/{id}/logs`**
Returns recent sandbox/template lifecycle logs. Pass `lines` to control the
tail length.
```bash
curl "https://api.miosa.ai/api/v1/sandboxes/{id}/logs?lines=200" \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
Stream logs with SSE:
```bash
curl -N "https://api.miosa.ai/api/v1/sandboxes/{id}/logs/stream" \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Accept: text/event-stream"
```
---
## Snapshot Lifecycle
**`POST /api/v1/sandboxes/{id}/snapshots`**
Creates a named checkpoint for a running sandbox.
```bash
curl -X POST https://api.miosa.ai/api/v1/sandboxes/{id}/snapshots \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"comment":"before dependency upgrade"}'
```
List, inspect, restore, and delete snapshots:
```bash
curl https://api.miosa.ai/api/v1/sandboxes/{id}/snapshots \
-H "Authorization: Bearer $MIOSA_API_KEY"
curl https://api.miosa.ai/api/v1/sandboxes/{id}/snapshots/{snapshot_id} \
-H "Authorization: Bearer $MIOSA_API_KEY"
curl -X POST https://api.miosa.ai/api/v1/sandboxes/{id}/restore/{snapshot_id} \
-H "Authorization: Bearer $MIOSA_API_KEY"
curl -X DELETE https://api.miosa.ai/api/v1/sandboxes/{id}/snapshots/{snapshot_id} \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
## Pause and Resume
**`POST /api/v1/sandboxes/{id}/pause`**
Pauses a running sandbox using the Firecracker snapshot path. Paused sandboxes
keep their lifecycle record and can be resumed.
```bash
curl -X POST https://api.miosa.ai/api/v1/sandboxes/{id}/pause \
-H "Authorization: Bearer $MIOSA_API_KEY"
curl -X POST https://api.miosa.ai/api/v1/sandboxes/{id}/resume \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
## Subscribe to Events (SSE)
**`GET /api/v1/sandboxes/{id}/events`**
Opens a Server-Sent Events (SSE) stream that emits sandbox lifecycle events. The connection closes automatically when the sandbox reaches `destroyed` or `error`.
### Auth
Bearer token required (sent as `Authorization: Bearer <key>` header).
### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | Sandbox ID. |
### Response — `200 OK` (event stream)
`Content-Type: text/event-stream`
**Event: `state_changed`**
```
event: state_changed
data: {"state":"running","previous_state":"provisioning","timestamp":"2026-04-25T10:00:07Z"}
```
**Event: `exec_output`**
```
event: exec_output
data: {"exec_id":"exec_abc","stream":"stdout","data":"2\n","timestamp":"2026-04-25T10:00:10Z"}
```
**Event: `error`**
```
event: error
data: {"message":"VM terminated unexpectedly","timestamp":"2026-04-25T10:05:00Z"}
```
```bash
curl -N "https://api.miosa.ai/api/v1/sandboxes/sbx_01j9xr2t4fk8me3n5q/events" \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Accept: text/event-stream"
```
---
## Sandbox Object
The full sandbox object returned by create, get, and list endpoints:
| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Unique sandbox identifier. |
| `tenant_id` | string | Owning tenant ID. |
| `owner_id` | string | Creating user ID. |
| `template_id` | string | Boot template name. |
| `state` | string | Current lifecycle state. |
| `cpu_count` | integer | Allocated vCPUs. |
| `memory_mb` | integer | Allocated RAM in MB. |
| `disk_size_mb` | integer | Allocated root disk in MB. |
| `image_id` | string | Resolved rootfs image, for example `miosa-sandbox`. |
| `boot_path` | string/null | Boot mechanism used, for example `snapshot` or `cold`. |
| `boot_ms` | integer/null | Runtime boot time in milliseconds once known. |
| `ready` | boolean | Whether envd and readiness checks are accepting work. |
| `ready_at` | string/null | ISO timestamp when `ready` became true. |
| `preview_url` | string/null | Tenant-aware base preview URL for the sandbox slug. Use `/expose` for a specific port. |
| `timeout_sec` | integer | Max seconds before force-destroy. |
| `total_runtime_sec` | integer \| null | Billed seconds. Set only after destruction. |
| `metadata` | object | Caller-supplied metadata. |
| `created_at` | string | ISO-8601 creation timestamp. |
| `started_at` | string \| null | ISO-8601 timestamp when VM entered `running`. |
| `destroyed_at` | string \| null | ISO-8601 timestamp when VM was destroyed. |
---
## Lifecycle States
```
provisioning → running → destroyed
running → paused → running
* → error (terminal)
```
| State | Description |
|-------|-------------|
| `provisioning` | VM is booting. Exec and file operations are not yet available. |
| `running` | VM is reachable. All operations allowed. |
| `paused` | VM has a RAM snapshot on disk. Can be resumed. |
| `destroyed` | Terminal. VM is gone, billing is settled. |
| `error` | Terminal failure. Destroy and create a new sandbox. |
---
## Error Response Format
All error responses use this shape:
```json
{
"error": {
"code": "NOT_FOUND",
"message": "sandbox not found",
"details": null
}
}
```
The `x-request-id` response header is set on every request. Include it in support requests.
---
## Rate Limits
| Limit | Value |
|-------|-------|
| Requests per minute per workspace | 300 |
| Concurrent sandboxes per tenant | 10 (contact support to increase) |
| Max sandbox runtime | Plan/policy dependent; day-long and always-on sandboxes are supported when billing policy allows it |
| Default resource shape | 1 vCPU, 1 GB RAM, 3 GB disk |
| Default resource caps | 4 vCPU, 8 GB RAM, 10 GB disk |
| Max exec timeout | 300 s (5 minutes) |
| Max file upload size | 100 MB |
Rate-limited responses return HTTP `429` with a `Retry-After` header indicating seconds to wait.
---
## See also
- [Python SDK](/docs/sdks/python/) — `miosa`
- [TypeScript SDK](/docs/sdks/typescript/) — `@miosa/sdk`
- [Go SDK](/docs/sdks/go/) — `github.com/miosa-ai/miosa-go`
- [Java SDK](/docs/sdks/java/) — `ai.miosa:miosa-sdk`
- [Elixir SDK](/docs/sdks/elixir/) — `:miosa`
- [Events (SSE)](/docs/api-reference/events/) — SSE reference for other MIOSA resources
- [Error Codes](/docs/api-reference/errors/) — full error code catalog including sandbox-specific codes
---
# Services API
URL: https://miosa.ai/docs/api-reference/services
Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/services
Source: src/routes/docs/api-reference/services/+page.md
Description: API reference for managing long-running systemd services inside MIOSA computers.
Services are persistent processes managed by systemd inside a computer. Use them for web servers, background workers, databases, or any daemon that should survive across exec calls and restart automatically on failure.
Base path: `/api/v1/computers/{id}/services`
Services map directly to systemd unit files (`/etc/systemd/system/miosa-{name}.service`) managed by the in-VM envd daemon. The computer must be **running** to interact with services.
---
## Quick Start
```typescript
const client = new Miosa();
// Register and start a web server
const svc = await client.services.create(computerId, {
name: 'webserver',
command: 'python3 -m http.server 8080',
cwd: '/home/user/app',
restart: 'always',
});
console.log(svc.status); // "starting"
// Check status later
const updated = await client.services.get(computerId, svc.id);
console.log(updated.status); // "running"
```
```bash
curl -X POST https://api.miosa.ai/api/v1/computers/{id}/services \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "webserver",
"command": "python3 -m http.server 8080",
"cwd": "/home/user/app",
"restart": "always"
}'
```
---
## Endpoints
| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/computers/{id}/services` | Create and start a service |
| `GET` | `/computers/{id}/services` | List all services on a computer |
| `GET` | `/computers/{id}/services/{name}` | Get a service by name |
| `POST` | `/computers/{id}/services/{name}/start` | Start a stopped service |
| `POST` | `/computers/{id}/services/{name}/stop` | Stop a running service |
| `POST` | `/computers/{id}/services/{name}/restart` | Restart a service |
| `DELETE` | `/computers/{id}/services/{name}` | Remove a service |
---
## Create a Service
**`POST /api/v1/computers/{id}/services`**
Writes the systemd unit file and starts the service.
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Service name. Pattern: `[a-z][a-z0-9-]{0,62}` |
| `command` | string | Yes | Full command to run |
| `env` | object | No | Environment variables as `{ "KEY": "value" }` |
| `cwd` | string | No | Working directory (default: `/home/user`) |
| `restart` | string | No | `"always"`, `"on-failure"` (default), or `"no"` |
### Response — `201 Created`
```json
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"computer_id": "...",
"tenant_id": "...",
"name": "webserver",
"command": "python3 -m http.server 8080",
"env": {},
"cwd": "/home/user/app",
"restart": "always",
"status": "starting",
"pid": null,
"exit_code": null,
"last_started_at": null,
"last_exited_at": null,
"created_at": "2026-04-11T00:00:00Z",
"updated_at": "2026-04-11T00:00:00Z"
}
}
```
### Status Values
| Status | Description |
|--------|-------------|
| `stopped` | Registered but not started |
| `starting` | Start dispatched to systemd |
| `running` | systemd reports `ActiveState=active` |
| `failed` | systemd reports `ActiveState=failed` |
| `removed` | Unit file deleted; record kept for audit |
### Errors
| Status | Error | Cause |
|--------|-------|-------|
| 409 | `a service with this name already exists` | Name must be unique per computer |
| 422 | Validation error | Invalid name format or restart policy |
| 502 | `AGENT_UNAVAILABLE` | In-VM agent unreachable |
```bash
curl -X POST https://api.miosa.ai/api/v1/computers/{id}/services \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "api-server",
"command": "/home/user/app/server",
"env": {"PORT": "3000", "NODE_ENV": "production"},
"cwd": "/home/user/app",
"restart": "on-failure"
}'
```
---
## List Services
**`GET /api/v1/computers/{id}/services`**
### Response — `200 OK`
```json
{
"data": [
{
"id": "...",
"name": "webserver",
"status": "running",
"pid": 12345,
"restart": "always",
"created_at": "2026-04-11T00:00:00Z"
}
],
"total": 1
}
```
```bash
curl https://api.miosa.ai/api/v1/computers/{id}/services \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
## Get a Service
**`GET /api/v1/computers/{id}/services/{name}`**
### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | UUID | Computer ID |
| `name` | string | Service name |
### Response — `200 OK`
Full service object (same as create response).
### Errors
| Status | Error | Cause |
|--------|-------|-------|
| 404 | `service not found` | Does not exist on this computer |
---
## Start, Stop, Restart
**`POST /api/v1/computers/{id}/services/{name}/start`**
**`POST /api/v1/computers/{id}/services/{name}/stop`**
**`POST /api/v1/computers/{id}/services/{name}/restart`**
All return a `200 OK` with the updated service object.
### Errors
| Status | Error | Cause |
|--------|-------|-------|
| 409 | `service is already running` | Cannot start a running service |
| 409 | `service is not running` | Cannot stop a non-running service |
| 502 | `AGENT_UNAVAILABLE` | In-VM agent unreachable |
```bash
# Stop a service
curl -X POST https://api.miosa.ai/api/v1/computers/{id}/services/webserver/stop \
-H "Authorization: Bearer $MIOSA_API_KEY"
# Restart it
curl -X POST https://api.miosa.ai/api/v1/computers/{id}/services/webserver/restart \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
## Remove a Service
**`DELETE /api/v1/computers/{id}/services/{name}`**
Stops the service and removes the systemd unit file. The database record transitions to `"removed"` status.
### Response — `200 OK`
```json
{
"data": { "name": "webserver", "status": "removed" }
}
```
```bash
curl -X DELETE https://api.miosa.ai/api/v1/computers/{id}/services/webserver \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
## Common Recipes
### Deploy a Node.js app as a service
```typescript
// Upload app files first
await client.files.write(computerId, {
path: '/home/user/app/server.js',
content: serverJsContent,
});
// Register and start
const svc = await client.services.create(computerId, {
name: 'node-app',
command: 'node /home/user/app/server.js',
env: { PORT: '3000' },
restart: 'on-failure',
});
```
### Wait until a service is running
```python
svc = client.services.create(computer_id, name="worker", command="/usr/bin/worker")
while svc.status == "starting":
time.sleep(1)
svc = client.services.get(computer_id, svc.name)
if svc.status != "running":
raise RuntimeError(f"Service failed to start: {svc.status}")
```
### Check for failed services in a fleet
```typescript
const computers = await client.computers.list();
for (const computer of computers.data) {
if (computer.status !== 'running') continue;
const services = await client.services.list(computer.id);
const failed = services.data.filter(s => s.status === 'failed');
if (failed.length > 0) {
console.log(`${computer.name}: ${failed.length} failed services`);
}
}
```
---
# Snapshots API
URL: https://miosa.ai/docs/api-reference/snapshots
Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/snapshots
Source: src/routes/docs/api-reference/snapshots/+page.md
Description: API reference for creating, listing, and restoring Firecracker VM snapshots (checkpoints).
Snapshots capture the full in-memory state of a running computer — CPU state, RAM, and filesystem — and store it durably. Restore a snapshot to spin up an identical computer in seconds.
Base path: `/api/v1/computers/{id}/snapshots`
Snapshots require the computer to be **running**. Creating a snapshot does not stop the computer.
---
## Quick Start
```typescript
const client = new Miosa();
// Create a snapshot
const snap = await client.snapshots.create(computerId, {
comment: 'before-deploy-v2',
});
// Poll until ready
let status = snap.status;
while (status !== 'ready') {
await new Promise(r => setTimeout(r, 3000));
const updated = await client.snapshots.get(computerId, snap.id);
status = updated.status;
}
// Restore — creates a new computer from the snapshot
const restored = await client.snapshots.restore(computerId, snap.id);
console.log(`Restored computer: ${restored.id}`);
```
```bash
# Create a snapshot
curl -X POST https://api.miosa.ai/api/v1/computers/{id}/snapshots \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"comment": "before-deploy-v2"}'
```
---
## Endpoints
| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/computers/{id}/snapshots` | Create a snapshot |
| `GET` | `/computers/{id}/snapshots` | List snapshots for a computer |
| `GET` | `/computers/{id}/snapshots/{snap_id}` | Get a snapshot |
| `DELETE` | `/computers/{id}/snapshots/{snap_id}` | Delete a snapshot |
| `POST` | `/computers/{id}/restore/{snap_id}` | Restore from snapshot |
| `GET` | `/computers/{id}/snapshots/{snap_id}/events` | SSE progress stream |
---
## Create a Snapshot
**`POST /api/v1/computers/{id}/snapshots`**
Initiates an asynchronous snapshot. The returned status will be `"creating"`. Poll `GET /snapshots/{snap_id}` or subscribe to SSE events for progress.
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `comment` | string | No | Human-readable label (max 500 chars) |
### Response — `201 Created`
```json
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"computer_id": "...",
"tenant_id": "...",
"comment": "before-deploy-v2",
"status": "creating",
"state_size_bytes": null,
"memory_size_bytes": null,
"rootfs_size_bytes": null,
"compressed_size_bytes": null,
"s3_bucket": null,
"s3_prefix": null,
"error": null,
"created_at": "2026-04-11T00:00:00Z",
"updated_at": "2026-04-11T00:00:00Z"
}
}
```
### Status State Machine
```
creating → uploading → ready
any → failed
ready → deleted
```
### Errors
| Status | Error | Cause |
|--------|-------|-------|
| 404 | `computer not found` | Computer does not exist or wrong tenant |
| 409 | `computer is not running` | Snapshot requires running VM |
```bash
curl -X POST https://api.miosa.ai/api/v1/computers/{id}/snapshots \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"comment": "checkpoint-before-risky-op"}'
```
---
## List Snapshots
**`GET /api/v1/computers/{id}/snapshots`**
### Response — `200 OK`
```json
{
"data": [
{
"id": "...",
"comment": "before-deploy-v2",
"status": "ready",
"compressed_size_bytes": 524288000,
"created_at": "2026-04-11T00:00:00Z"
}
],
"total": 1
}
```
```bash
curl https://api.miosa.ai/api/v1/computers/{id}/snapshots \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
## Get a Snapshot
**`GET /api/v1/computers/{id}/snapshots/{snap_id}`**
### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | UUID | Computer ID |
| `snap_id` | UUID | Snapshot ID |
### Response — `200 OK`
Full snapshot object (same shape as create response).
### Errors
| Status | Error | Cause |
|--------|-------|-------|
| 403 | `forbidden` | Snapshot belongs to a different tenant |
| 404 | `snapshot not found` | Does not exist |
```bash
curl https://api.miosa.ai/api/v1/computers/{id}/snapshots/{snap_id} \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
## Delete a Snapshot
**`DELETE /api/v1/computers/{id}/snapshots/{snap_id}`**
Soft-deletes the snapshot (transitions to `"deleted"` status). S3 objects are cleaned up asynchronously.
### Response — `200 OK`
```json
{
"data": {
"id": "...",
"status": "deleted"
}
}
```
### Errors
| Status | Error | Cause |
|--------|-------|-------|
| 404 | `snapshot not found` | Does not exist |
| 409 | `snapshot is not in a deletable state` | Snapshot is being restored |
```bash
curl -X DELETE https://api.miosa.ai/api/v1/computers/{id}/snapshots/{snap_id} \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
## Restore from Snapshot
**`POST /api/v1/computers/{id}/restore/{snap_id}`**
Creates a new computer with the state from the snapshot. The original computer is unchanged.
### Response — `201 Created`
```json
{
"data": {
"id": "new-computer-uuid",
"name": "my-computer-restored",
"status": "provisioning",
"size": "small"
},
"snapshot": {
"id": "snap-uuid",
"status": "restoring"
}
}
```
### Errors
| Status | Error | Cause |
|--------|-------|-------|
| 404 | `snapshot not found` | Does not exist |
| 409 | `snapshot is not ready` | Snapshot must be in `ready` state |
```bash
curl -X POST https://api.miosa.ai/api/v1/computers/{id}/restore/{snap_id} \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
## SSE Progress Stream
**`GET /api/v1/computers/{id}/snapshots/{snap_id}/events`**
Subscribe to real-time snapshot progress. Requires a short-lived ticket rather than a Bearer token (browsers cannot set Authorization on EventSource connections).
### Event Types
| Event | Description |
|-------|-------------|
| `snapshot_creating` | Firecracker writing memory and VM state to disk |
| `snapshot_uploading` | Compressing and uploading to object storage |
| `snapshot_ready` | Upload complete; snapshot is usable |
| `snapshot_failed` | Unrecoverable error — see `error` field |
---
## Common Recipes
### Automated checkpoint before risky operations
```python
from miosa import Miosa
client = Miosa()
snap = client.snapshots.create(computer_id, comment="pre-migration")
while snap.status not in ("ready", "failed"):
time.sleep(2)
snap = client.snapshots.get(computer_id, snap.id)
if snap.status == "failed":
raise RuntimeError(f"Snapshot failed: {snap.error}")
# Proceed with risky operation knowing you can restore
do_migration(computer_id)
```
### Snapshot-based parallelism (clone a baseline)
```typescript
// Build a baseline environment once, clone it N times for parallel jobs
const baseline = await client.snapshots.create(setupComputerId, {
comment: 'baseline-env',
});
// Each restore creates an independent computer from the same state
const workers = await Promise.all(
Array.from({ length: 5 }, () =>
client.snapshots.restore(setupComputerId, baseline.id)
)
);
```
---
## See also
- [Computers API](/docs/api-reference/computers/) — computer lifecycle and clone endpoint
- [Events (SSE)](/docs/api-reference/events/) — stream snapshot progress events
- [Error Codes](/docs/api-reference/errors/) — snapshot-specific error codes
---
# Streaming Exec API
URL: https://miosa.ai/docs/api-reference/streaming-exec
Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/streaming-exec
Source: src/routes/docs/api-reference/streaming-exec/+page.md
Description: API reference for streaming command output in real-time from MIOSA computers via SSE.
Streaming exec runs a command on a computer and delivers output line-by-line via Server-Sent Events. Use it for long-running builds, installs, or any command where you want incremental output rather than waiting for completion.
Base path: `/api/v1/computers/{id}/exec/stream`
Standard `POST /computers/{id}/exec` waits for the command to finish before returning. Streaming exec is better for commands that run longer than a few seconds or that you want to monitor interactively.
---
## Quick Start
```typescript
const client = new Miosa();
// Stream a long-running build
for await (const event of client.exec.stream(computerId, {
command: 'pip install torch',
timeout: 300,
})) {
if (event.type === 'output') {
process.stdout.write(event.data.line);
} else if (event.type === 'exit') {
console.log(`Exit code: ${event.data.exitCode}`);
break;
}
}
```
```bash
# Obtain a streaming ticket first, then connect
TICKET=$(curl -s -X POST https://api.miosa.ai/api/v1/computers/{id}/exec/stream \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"command": "pip install torch", "timeout": 300}' | jq -r .ticket)
curl -N "https://api.miosa.ai/api/v1/computers/{id}/exec/stream/events?ticket=$TICKET" \
-H "Accept: text/event-stream"
```
---
## Endpoints
| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/computers/{id}/exec/stream` | Start a streaming exec and obtain a ticket |
| `GET` | `/computers/{id}/exec/stream/events` | SSE stream (requires ticket) |
| `POST` | `/computers/{id}/exec` | Non-streaming exec (waits for completion) |
| `POST` | `/computers/{id}/exec/python` | Non-streaming Python exec |
---
## Start Streaming Exec
**`POST /api/v1/computers/{id}/exec/stream`**
Starts the command and returns a short-lived ticket for the SSE stream.
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `command` | string | Yes | Shell command to execute |
| `timeout` | integer | No | Timeout in seconds (default: 30, max: 300) |
| `shell` | string | No | Shell to use (default: `/bin/bash`) |
### Response — `202 Accepted`
```json
{
"ticket": "exec_short_lived_token",
"expires_at": 1712700060,
"stream_url": "/api/v1/computers/{id}/exec/stream/events?ticket=exec_short_lived_token"
}
```
The ticket expires in 60 seconds — open the EventSource connection immediately.
### Errors
| Status | Error | Cause |
|--------|-------|-------|
| 409 | `COMPUTER_NOT_RUNNING` | Computer is not running |
| 502 | `AGENT_UNAVAILABLE` | In-VM agent unreachable |
```bash
curl -X POST https://api.miosa.ai/api/v1/computers/{id}/exec/stream \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"command": "npm install", "timeout": 120}'
```
---
## Connect to SSE Stream
**`GET /api/v1/computers/{id}/exec/stream/events?ticket={ticket}`**
### Event Types
| Event | Payload Fields | Description |
|-------|---------------|-------------|
| `output` | `line`, `stream` | A line of stdout or stderr |
| `exit` | `exit_code`, `duration_ms` | Command finished; stream closes after this |
| `timeout` | `timeout_seconds` | Command killed after timeout |
| `error` | `message` | Internal error before command started |
### Event Payload Examples
```
event: output
data: {"line": "Collecting torch\n", "stream": "stdout"}
event: output
data: {"line": " Downloading torch-2.3.0-...\n", "stream": "stdout"}
event: exit
data: {"exit_code": 0, "duration_ms": 45312}
```
### `stream` Values
| Value | Description |
|-------|-------------|
| `stdout` | Standard output |
| `stderr` | Standard error |
```javascript
// Browser
const es = new EventSource(`/api/v1/computers/${id}/exec/stream/events?ticket=${ticket}`);
es.addEventListener('output', e => {
const { line, stream } = JSON.parse(e.data);
console.log(`[${stream}] ${line}`);
});
es.addEventListener('exit', e => {
const { exit_code } = JSON.parse(e.data);
console.log(`Exited with code ${exit_code}`);
es.close();
});
es.addEventListener('error', () => {
// Connection dropped or ticket expired
es.close();
});
```
---
## Comparison: Streaming vs Non-Streaming
| Aspect | `POST /exec` | `POST /exec/stream` |
|--------|-------------|---------------------|
| Response timing | After command completes | Immediate (ticket) |
| Output delivery | All at once | Line by line via SSE |
| Max timeout | 300 s | 300 s |
| Best for | Quick commands | Long builds, installs |
| SDK method | `exec.run()` | `exec.stream()` |
---
## Common Recipes
### Monitor a build
```python
for event in client.exec.stream(computer_id, command="make build", timeout=300):
if event.type == "output":
print(event.data["line"], end="")
elif event.type == "exit":
if event.data["exit_code"] != 0:
raise RuntimeError(f"Build failed with exit code {event.data['exit_code']}")
print("Build succeeded")
break
```
### Capture both streams separately
```typescript
const stdout: string[] = [];
const stderr: string[] = [];
for await (const event of client.exec.stream(computerId, { command: 'myapp 2>&1 | tee /tmp/output.log' })) {
if (event.type === 'output') {
if (event.data.stream === 'stdout') stdout.push(event.data.line);
else stderr.push(event.data.line);
} else if (event.type === 'exit') {
break;
}
}
```
### Timeout handling
```python
try:
for event in client.exec.stream(computer_id, command="long-task", timeout=60):
if event.type == "timeout":
print(f"Command killed after {event.data['timeout_seconds']}s")
break
elif event.type == "exit":
break
except Exception as e:
print(f"Stream error: {e}")
```
---
# API Reference / Versions
URL: https://miosa.ai/docs/api-reference/versions
Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/versions
Source: src/routes/docs/api-reference/versions/+page.md
Description: Immutable deployment version sub-resource. List, get, promote, archive.
A **Deployment Version** is the immutable record of one publish. See [Versions](/docs/deploy/versions/) for the conceptual model.
## Endpoints
```http
GET /api/v1/deployments/:id/versions
GET /api/v1/deployments/:id/versions/:version_id
POST /api/v1/deployments/:id/versions/:version_id/promote
```
## List
```http
GET /api/v1/deployments/:id/versions
GET /api/v1/deployments/:id/versions?state=ready
GET /api/v1/deployments/:id/versions?workspace_id=550e8400-e29b-41d4-a716-446655440000
GET /api/v1/deployments/:id/versions?external_workspace_id=clinic_123
GET /api/v1/deployments/:id/versions?limit=50&cursor=...
```
Response:
```json
{
"data": [
{
"id": "ver_...",
"deployment_id": "dep_...",
"version_number": 17,
"kind": "static",
"state": "ready",
"artifact_sha256": "a5e6f0c1...",
"source_sha256": "f1a3b8c2...",
"promoted_at": "2026-05-14T18:20:00Z",
"workspace_id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "660e8400-e29b-41d4-a716-446655440001",
"external_workspace_id": "clinic_123",
"external_user_id": "dr-smith-456",
"created_at": "2026-05-14T18:19:48Z"
}
],
"next_cursor": "..."
}
```
## Get
```http
GET /api/v1/deployments/:id/versions/:version_id
```
Returns the full version row including `artifact_manifest`, `runtime_image`, `runtime_command`, `runtime_port`, `health_check_path`, `build_log_uri`.
## Promote
Make a specific ready version the active one for the deployment.
```http
POST /api/v1/deployments/:id/versions/:version_id/promote
```
Body: empty, or optionally `{ "environment": "production" }`.
Scopes: `deployments:write`. Requires `Idempotency-Key` for safety on retries.
Promotion is different from publish:
- **Publish** creates a new version from a sandbox source.
- **Promote** points an existing ready version as active in an environment.
Use promote for canary / staged rollouts: publish to a non-production environment first, validate, then promote to production.
## States
| State | Meaning |
|---|---|
| `created` | Row exists, build not started |
| `building` | Build in progress |
| `ready` | Build succeeded, artifact uploaded, promotable |
| `failed` | Build or health check failed |
| `archived` | Promoted-out-of; still bootable for rollback |
State transitions are one-way. Once `ready` or `failed`, a version stays that way.
## Permissions
| Action | Scope |
|---|---|
| List, Get | `deployments:read` |
| Promote | `deployments:write` |
## See also
- [Deployments](/docs/api-reference/deployments/) — parent resource
- [Releases](/docs/api-reference/releases/) — artifact reference
- [Versions](/docs/deploy/versions/) — conceptual model
- [Rollback](/docs/deploy/rollback/) — uses promote internally
---
# Workspaces API
URL: https://miosa.ai/docs/api-reference/workspaces
Fallback URL: https://miosa.roberto-c49.workers.dev/docs/api-reference/workspaces
Source: src/routes/docs/api-reference/workspaces/+page.md
Description: API reference for creating and managing MIOSA workspaces - the customer/client layer inside an organization.
Workspaces group resources inside an organization. In a white-label platform, a workspace usually represents one downstream customer or client, such as `Dr. Smith Clinic` inside the `ClinicIQ` organization. Projects live inside workspaces, and sandboxes, computers, deployments, databases, storage buckets, volumes, functions, jobs, and domains belong to projects.
Base path: `/api/v1/workspaces`
A **default** workspace is created automatically when you sign up. Resources are assigned to the default workspace and its default project unless you specify workspace/project ownership at creation time.
---
## Quick Start
```typescript
const client = new Miosa(); // reads MIOSA_API_KEY
// Create a workspace for a downstream customer
const ws = await client.workspaces.create({
name: 'Dr. Smith Clinic',
slug: 'dr-smith-clinic',
externalWorkspaceId: 'clinic_123',
});
// Create a computer inside a project in that workspace
const computer = await client.computers.create({
name: 'clinic-builder',
templateType: 'miosa-desktop',
workspaceId: ws.id,
projectSlug: 'lead-magnet',
projectName: 'Lead Magnet',
});
// List computers in the workspace
const { data } = await client.workspaces.listComputers(ws.id);
```
```bash
# curl equivalent
curl -X POST https://api.miosa.ai/api/v1/workspaces \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Dr. Smith Clinic", "slug": "dr-smith-clinic", "external_workspace_id": "clinic_123"}'
```
---
## Endpoints
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/workspaces` | List workspaces for the tenant |
| `POST` | `/workspaces` | Create a workspace |
| `GET` | `/workspaces/{id}` | Get a workspace |
| `PATCH` | `/workspaces/{id}` | Update a workspace |
| `PUT` | `/workspaces/{id}/settings` | Update workspace-level resource limits |
| `GET` | `/workspaces/{id}/preview-domain` | Read the workspace preview domain |
| `PUT` | `/workspaces/{id}/preview-domain` | Set the workspace preview domain |
| `DELETE` | `/workspaces/{id}/preview-domain` | Clear the workspace preview domain |
| `GET` | `/workspaces/{id}/preview-domain/verify` | Check DNS readiness |
| `DELETE` | `/workspaces/{id}` | Delete a workspace |
| `GET` | `/workspaces/{id}/computers` | List computers in a workspace |
| `GET` | `/workspaces/{id}/stats` | Computer counts and resource totals |
| `GET` | `/workspaces/{id}/usage` | Time-series credit and compute usage |
| `GET` | `/workspaces/{id}/projects` | List projects in a workspace |
---
## List Workspaces
**`GET /api/v1/workspaces`**
Returns all workspaces belonging to the authenticated tenant.
### Response — `200 OK`
```json
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"tenant_id": "...",
"name": "default",
"slug": "default",
"external_workspace_id": null,
"description": null,
"metadata": {},
"is_default": true,
"computer_count": 3,
"created_at": "2026-04-11T00:00:00Z",
"updated_at": "2026-04-11T00:00:00Z"
}
],
"total": 1
}
```
```bash
curl https://api.miosa.ai/api/v1/workspaces \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
## Create a Workspace
**`POST /api/v1/workspaces`**
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Human-readable name (1–120 chars) |
| `slug` | string | No | URL-safe identifier. Auto-derived from `name` if omitted. Pattern: `[a-z0-9][a-z0-9-]{0,79}` |
| `external_workspace_id` | string | No | Your customer/account/workspace ID. Globally unique inside this MIOSA organization. |
| `description` | string | No | Optional description |
| `metadata` | object | No | Caller metadata stored on the workspace. |
### Response — `201 Created`
Full workspace object (same shape as list items above).
### Errors
| Status | Error | Cause |
|--------|-------|-------|
| 422 | `has already been taken` | Slug already exists in tenant |
| 422 | `must start with alphanumeric and contain only a-z 0-9 -` | Invalid slug format |
```bash
curl -X POST https://api.miosa.ai/api/v1/workspaces \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Dr. Smith Clinic",
"slug": "dr-smith-clinic",
"external_workspace_id": "clinic_123",
"description": "Client workspace for Dr. Smith Clinic"
}'
```
---
## Get a Workspace
**`GET /api/v1/workspaces/{id}`**
### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | UUID | Workspace ID |
### Response — `200 OK`
Full workspace object.
### Errors
| Status | Error | Cause |
|--------|-------|-------|
| 403 | `forbidden` | Workspace belongs to a different tenant |
| 404 | `workspace not found` | Does not exist |
```bash
curl https://api.miosa.ai/api/v1/workspaces/{id} \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
## Update a Workspace
**`PATCH /api/v1/workspaces/{id}`**
Update mutable workspace fields. Prefer treating the slug as stable once resources and URLs exist.
### Request Body
| Field | Type | Description |
|-------|------|-------------|
| `name` | string | New display name |
| `slug` | string | New URL-safe slug. Must remain unique inside the organization. |
| `description` | string | New description (`null` to clear) |
| `external_workspace_id` | string | Your customer/account/workspace ID |
| `metadata` | object | Replacement metadata map |
### Response — `200 OK`
Updated workspace object.
### Errors
| Status | Error | Cause |
|--------|-------|-------|
| 403 | `forbidden` | Wrong tenant |
| 404 | `workspace not found` | Does not exist |
```bash
curl -X PATCH https://api.miosa.ai/api/v1/workspaces/{id} \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "production-us-east"}'
```
---
## Delete a Workspace
**`DELETE /api/v1/workspaces/{id}`**
Deletes an empty workspace. Fails if the workspace contains computers or projects.
### Response — `200 OK`
```json
{ "deleted": true }
```
### Errors
| Status | Error | Cause |
|--------|-------|-------|
| 403 | `forbidden` | Wrong tenant |
| 404 | `workspace not found` | Does not exist |
| 409 | `workspace has computers` | Remove or move resources first |
```bash
curl -X DELETE https://api.miosa.ai/api/v1/workspaces/{id} \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
## Update Workspace Settings
**`PUT /api/v1/workspaces/{id}/settings`**
Applies resource limits and default preferences for the workspace. These limits cap what any computer inside the workspace can do. Tenant-level limits still apply as a ceiling.
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `max_computers` | integer | No | Maximum number of computers allowed in this workspace. `null` = no workspace-level cap (tenant limit applies). |
| `max_computer_size` | string | No | Maximum `size` value a computer in this workspace may be created with: `"small"`, `"medium"`, or `"large"`. |
| `default_computer_size` | string | No | Default `size` applied when creating a computer without an explicit `size`. |
| `default_auto_stop_seconds` | integer | No | Auto-stop idle timeout (seconds) applied to new computers by default. `0` = no auto-stop. |
| `allowed_template_types` | string[] | No | Allowlist of template names that can be used in this workspace. Empty array = all templates allowed. |
### Response — `200 OK`
```json
{
"workspace_id": "550e8400-e29b-41d4-a716-446655440000",
"settings": {
"max_computers": 10,
"max_computer_size": "large",
"default_computer_size": "small",
"default_auto_stop_seconds": 3600,
"allowed_template_types": []
},
"updated_at": "2026-05-17T10:00:00Z"
}
```
---
## Workspace Preview Domain
**`PUT /api/v1/workspaces/{id}/preview-domain`**
Sets a client/workspace-level base domain for generated URLs. This overrides the organization preview domain for resources in this workspace unless a project has its own preview domain.
```bash
curl -X PUT https://api.miosa.ai/api/v1/workspaces/{id}/preview-domain \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"preview_domain":"drsmithclinic.com"}'
```
Response:
```json
{
"scope": "workspace",
"id": "550e8400-e29b-41d4-a716-446655440000",
"preview_domain": "drsmithclinic.com",
"effective_domain": "drsmithclinic.com",
"status": "pending_dns",
"dns_status": "pending",
"url_examples": {
"default_preview": "https://.drsmithclinic.com",
"port_preview": "https://3000-.sandbox.drsmithclinic.com",
"deployment": "https://.drsmithclinic.com"
}
}
```
Required DNS records:
| Record type | Name | Value |
|---|---|---|
| `CNAME` | `*` | `proxy.miosa.ai` |
| `CNAME` | `*.sandbox` | `proxy.miosa.ai` |
Verify:
```bash
curl https://api.miosa.ai/api/v1/workspaces/{id}/preview-domain/verify \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
Clear and inherit from the organization/domain fallback:
```bash
curl -X DELETE https://api.miosa.ai/api/v1/workspaces/{id}/preview-domain \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
### Errors
| Status | Error | Cause |
|--------|-------|-------|
| 403 | `forbidden` | Wrong tenant |
| 404 | `workspace not found` | Does not exist |
| 422 | `VALIDATION_FAILED` | Unknown size value or invalid limit |
```bash
curl -X PUT https://api.miosa.ai/api/v1/workspaces/{id}/settings \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"max_computers": 10,
"max_computer_size": "large",
"default_auto_stop_seconds": 3600
}'
```
---
## List Computers in a Workspace
**`GET /api/v1/workspaces/{id}/computers`**
### Response — `200 OK`
```json
{
"data": [
{
"id": "...",
"name": "runner-1",
"status": "running",
"size": "small",
"template_type": "miosa-desktop",
"created_at": "2026-04-11T00:00:00Z"
}
],
"total": 1
}
```
```bash
curl https://api.miosa.ai/api/v1/workspaces/{id}/computers \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
## List Projects in a Workspace
**`GET /api/v1/workspaces/{id}/projects`**
Returns the projects owned by a workspace. Use this for client dashboards before listing the sandboxes, computers, deployments, and databases inside a project.
### Response - `200 OK`
```json
{
"data": [
{
"id": "660e8400-e29b-41d4-a716-446655440001",
"tenant_id": "...",
"workspace_id": "550e8400-e29b-41d4-a716-446655440000",
"external_workspace_id": "clinic_123",
"external_project_id": "project_789",
"name": "Lead Magnet",
"slug": "lead-magnet",
"description": null,
"metadata": {},
"created_at": "2026-05-18T10:00:00Z",
"updated_at": "2026-05-18T10:00:00Z"
}
],
"total": 1
}
```
```bash
curl https://api.miosa.ai/api/v1/workspaces/{id}/projects \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
## Get Workspace Stats
**`GET /api/v1/workspaces/{id}/stats`**
Returns a snapshot of computer counts grouped by status and the aggregate resource footprint for the workspace.
### Response — `200 OK`
```json
{
"workspace_id": "550e8400-e29b-41d4-a716-446655440000",
"computers": {
"total": 5,
"by_status": {
"running": 3,
"stopped": 2,
"provisioning": 0,
"error": 0
}
},
"resources": {
"total_vcpus": 7,
"total_memory_mb": 28672,
"total_disk_gb": 160
},
"settings": {
"max_computers": 10,
"max_computer_size": "large",
"default_auto_stop_seconds": 3600
}
}
```
```bash
curl https://api.miosa.ai/api/v1/workspaces/{id}/stats \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
## Get Workspace Usage
**`GET /api/v1/workspaces/{id}/usage`**
Returns time-series credit consumption and compute-hour totals for the workspace over a given window. Use this for per-workspace cost attribution dashboards.
### Query Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `window` | string | `"24h"` (default), `"7d"`, `"30d"` |
| `granularity` | string | `"hour"` (default), `"day"` — bucket size for the time-series data |
### Response — `200 OK`
```json
{
"workspace_id": "550e8400-e29b-41d4-a716-446655440000",
"window": "7d",
"granularity": "day",
"total_credits_used": 1284,
"total_compute_hours": 42.5,
"series": [
{
"ts": "2026-05-10T00:00:00Z",
"credits_used": 198,
"compute_hours": 6.6
},
{
"ts": "2026-05-11T00:00:00Z",
"credits_used": 211,
"compute_hours": 7.0
}
]
}
```
### Errors
| Status | Error | Cause |
|--------|-------|-------|
| 403 | `forbidden` | Wrong tenant |
| 404 | `workspace not found` | Does not exist |
| 422 | `VALIDATION_FAILED` | Unknown `window` or `granularity` value |
```bash
curl "https://api.miosa.ai/api/v1/workspaces/{id}/usage?window=7d&granularity=day" \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
---
## Common Recipes
### Move all computers to a new workspace
Computers do not yet support a `move` operation on workspace assignment via `PATCH /computers/{id}`. Use the `POST /computers/{id}/move` endpoint instead:
```bash
curl -X POST https://api.miosa.ai/api/v1/computers/{computer_id}/move \
-H "Authorization: Bearer $MIOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"workspace_id": "target-workspace-id"}'
```
### Enumerate all workspaces and their sizes
```typescript
const { data: workspaces } = await client.workspaces.list();
for (const ws of workspaces) {
console.log(`${ws.name}: ${ws.computerCount} computers`);
}
```
### Default workspace ID lookup
If you need the default workspace ID programmatically:
```typescript
const { data } = await client.workspaces.list();
const defaultWs = data.find(ws => ws.isDefault);
console.log(defaultWs.id);
```
---
## Common Errors
| Status | Code | Cause |
|--------|------|-------|
| 403 | `forbidden` | Workspace belongs to a different tenant |
| 404 | `workspace not found` | Does not exist |
| 409 | `workspace has computers` | Cannot delete a workspace that contains computers |
| 422 | `VALIDATION_FAILED` | Name or slug failed schema validation |
---
## See also
- [Computers API](/docs/api-reference/computers/) — computers are scoped to workspaces
- [Projects API](/docs/api-reference/projects/) — projects live inside workspaces
- [Error Codes](/docs/api-reference/errors/) — `VALIDATION_FAILED` and general errors
---
# Authentication
URL: https://miosa.ai/docs/authentication
Fallback URL: https://miosa.roberto-c49.workers.dev/docs/authentication
Source: src/routes/docs/authentication/+page.md
Description: How to authenticate with the MIOSA API using msk_ API keys or JWT tokens.
# Authentication
Every MIOSA API request carries a single `Authorization: Bearer <token>` header.
The token is either an **API key** (prefix `msk_`) or a **JWT** from `POST /auth/login`.
One key covers the entire platform — computers, desktop, files, exec, CUA agent,
OSA, credits, and (with the right role) admin. No separate keys for AI, no
per-product configuration.
## Credential hierarchy
Diagram:
graph TB
MSK[msk_live_* workspace key] -->|server-side only| API[MIOSA API calls]
MSK -->|mints via POST /previews/:id/share| BT[Browser token - scoped + short-lived]
BT -->|browser only| EP[Specific sandbox endpoint]
MSK -->|mints via POST /auth/sse-ticket| ST[SSE ticket - single use]
ST -->|EventSource| SS[SSE stream]
## API Keys
### Key format
```
msk__
```
| Prefix | Role | Capabilities |
|--------|----------|---------------------------------------------------|
| `msk_u_...` | user | Compute, desktop, files, CUA, OSA, credits |
| `msk_a_...` | admin | Everything above **+** `/api/v1/admin/*` |
| `msk_p_...` | platform | Tenant-wide automation issued by the MIOSA platform |
Role is **orthogonal** to `purpose`, which picks the backend the key can call:
- `purpose: api` — compute, desktop, files, CUA, OSA, credits
- `purpose: optimal` — AI/LLM proxy (`/v1/chat/completions`, `/v1/responses`)
If you need both, create two keys. They share the `msk_` family so SDK
configuration is identical.
### Creating an API key
```bash
curl -X POST https://api.miosa.ai/api/v1/api-keys \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Production SDK Key",
"key_type": "user",
"purpose": "api"
}'
```
```json
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"key": "msk_u_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"key_prefix": "msk_u_a1b2c3",
"name": "Production SDK Key",
"key_type": "user",
"key_purpose": "api",
"rate_limit_rpm": 60,
"status": "active",
"created_at": "2026-04-17T00:00:00Z"
}
}
```
The raw key is **only returned once** at creation. Store it securely. If lost,
revoke it and create a new one — the original cannot be recovered.
User-role callers can only mint `user` keys. Admin and platform callers may
mint any `key_type`. To issue admin keys programmatically, use the admin
variant at `POST /admin/api-keys` (operator-internal — not part of the public docs).
### Using an API key
Pass the key in the `Authorization` header:
```bash
curl https://api.miosa.ai/api/v1/computers \
-H "Authorization: Bearer msk_u_a1b2c3d4e5f6..."
```
All five SDKs read `MIOSA_API_KEY` from the environment by default:
```python
from miosa import Miosa
# Explicit key
client = Miosa(api_key="msk_u_...")
# Or from environment: export MIOSA_API_KEY="msk_u_..."
client = Miosa()
```
```ts
// Explicit key
const miosa = new Miosa({ apiKey: 'msk_u_...' });
```
```go
// Explicit key
client := miosa.NewClient("msk_u_...")
// Or from environment
client := miosa.NewClient(os.Getenv("MIOSA_API_KEY"))
```
```elixir
# Explicit key
client = Miosa.client("msk_u_...")
# Or from environment
client = Miosa.client(System.fetch_env!("MIOSA_API_KEY"))
```
```java
MiosaClient miosa = new MiosaClient("msk_u_...");
```
### Managing keys
| Action | Endpoint |
|--------|----------|
| List your keys | `GET /api/v1/api-keys` |
| Create a key | `POST /api/v1/api-keys` |
| Revoke a key | `DELETE /api/v1/api-keys/{id}` |
Revoking a key flips its status to `revoked`. The change is cached and
enforced within seconds — subsequent requests return `401 Unauthorized`.
Revocation is irreversible; create a new key if you need access restored.
### Admin keys
`msk_a_*` and `msk_p_*` keys unlock everything under `/api/v1/admin/*` —
user management, tenant operations, credit grants, fleet-wide computer
control, Optimal model switching. These are gated behind the same
`RequireAdmin` plug as admin JWTs.
User-role callers hitting an admin endpoint receive `403 Forbidden`.
Admin endpoints are operator-internal and not documented publicly.
## JWT Tokens
JWT tokens are used by the MIOSA web UI and SSE ticket flows. They are issued
on login and carry user identity + tenant context.
### Obtaining a token
```bash
curl -X POST https://api.miosa.ai/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "your_password"}'
```
```json
{
"token": "eyJhbGciOiJIUzUxMiIs...",
"refresh_token": "eyJhbGciOiJIUzUxMiIs...",
"user": { "id": "...", "email": "user@example.com" }
}
```
### Token lifecycle
| Token | Lifetime | Refresh |
|-------|----------|---------|
| Access token | ~1 hour | Use refresh token |
| Refresh token | 7 days | Re-login required |
```bash
curl -X POST https://api.miosa.ai/api/v1/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token": "eyJhbGciOiJIUzUxMiIs..."}'
```
For programmatic access, use **API keys**. JWTs are designed for browser
sessions and expire frequently.
## SSE streams
Server-Sent Event endpoints (CUA session events) cannot use a standard
`Authorization` header because `EventSource` does not support custom headers.
Instead, mint a one-time ticket first:
```bash
# 1. Ask for a ticket (Bearer-authed — API key or JWT both work)
curl -X POST \
https://api.miosa.ai/api/v1/computers/{id}/cua/sessions/{session_id}/sse-ticket \
-H "Authorization: Bearer msk_u_..."
# {"ticket":"sset_abc123","expires_in":3600,"session_id":"..."}
# 2. Open the stream with the ticket as a query param
curl https://api.miosa.ai/api/v1/computers/{id}/cua/sessions/{session_id}/events?ticket=sset_abc123
```
The ticket is single-use and expires after one hour.
## Rate limits
| Scope | Limit | Window |
|-------|-------|--------|
| Developer API (`/api/v1/*`) | 300 requests | Per minute per key |
| Admin API (`/api/v1/admin/*`) | 300 requests | Per minute per key |
| Auth endpoints (`/auth/login`, `/auth/register`, `/auth/refresh`) | 60 requests | Per minute per IP |
When rate limited, the API returns `429 Too Many Requests` with a
`Retry-After` header.
Rate-limit status is surfaced in every response:
```
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 287
X-RateLimit-Reset: 1712700060
```
## Error codes
| Status | Code | Meaning |
|--------|------|---------|
| 401 | `UNAUTHORIZED` | Missing / invalid / revoked credential |
| 403 | `FORBIDDEN` | Valid credential but insufficient role |
| 429 | `RATE_LIMITED` | Too many requests |
## Security best practices
1. **Never commit API keys** to version control.
2. **Use environment variables** or a secrets manager.
3. **Rotate keys** on exposure — revoke and create a new one.
4. **Scope by role** — hand out `msk_u_*` keys to most callers; reserve
`msk_a_*` / `msk_p_*` for operational tooling.
5. **Separate keys for dev and prod** — easier to revoke blast radius.
6. **Restrict by IP** (admin keys) — pass `allowed_ips` when minting via
`POST /admin/api-keys` to reject traffic from unexpected sources.
---
# Changelog
URL: https://miosa.ai/docs/changelog
Fallback URL: https://miosa.roberto-c49.workers.dev/docs/changelog
Source: src/routes/docs/changelog/+page.md
Description: What's new in MIOSA — shipped features, SDK updates, and platform improvements.
What's new in MIOSA. Updates ship continuously; this page is a human-readable digest grouped by week.
The MIOSA SDKs will live in dedicated public repos for each language — `Miosa-osa/python-sdk`, `Miosa-osa/typescript-sdk`, etc. Mirrored from the monorepo via a CI subtree-split workflow. Direct installs remain unchanged.
---
## May 12 – 18, 2026
### MCP server for AI coding assistants
A native MCP server ships 22 computer-use tools — screenshot, click, type, scroll, drag, key chords, window management, clipboard, cursor, launch, and more — making any MCP-capable AI coding assistant a first-class driver for MIOSA Computers. See [Desktop Control →](/docs/computers/desktop/)
### Desktop double-click, scroll fix, and faster typing
Fixed a scroll timing regression that caused scroll-up actions to stall. Added double-click as a distinct desktop action. Improved keystroke throughput for `type` commands. The Go SDK now ships a full `Computer` type with all 13 desktop actions.
### Python and TypeScript Sandbox SDKs reach full parity
All Sandbox methods — create, exec, stream, snapshot, file I/O, suspend, resume — are now available in Python (sync + async) and TypeScript. The CLI routes sandbox commands through the native API rather than a secondary proxy. See [Sandbox SDK reference →](/docs/develop/sandboxes/)
### Fixed: SDK import errors and agent exports
Resolved broken module imports introduced during SDK consolidation. `ExecResult` field names are now consistent across Python and TypeScript. Agent stub exports are correctly registered in the package index.
---
## May 5 – 11, 2026
### Sandbox architecture: snapshot restore now live
Sandbox snapshot restore is live at ~166ms p50. The platform captures snapshots automatically on suspend and restores them on resume — no cold boot penalty on warm sandboxes. See [Sandboxes →](/docs/develop/sandboxes/)
### Regions and computer embedding APIs
New `GET /api/v1/regions` endpoint returns the current compute region list with capacity metadata. The embed API now returns a scoped, short-lived token for embedding a computer's desktop into your own UI — no iframe proxy required. See [Embedding →](/docs/computers/embedding/)
### Production deploy pipeline hardened
The sandbox-to-deployment pipeline (source snapshot → builder → release → runtime instance) is fully wired in production. Build logs stream in real time. Static sites land on the edge; dynamic apps boot as runtime instances. See [Deploy overview →](/docs/deploy/overview/)
### Unified SDKs replace standalone sandbox package
The standalone sandbox SDK package is deprecated. All sandbox functionality is available under the unified language SDKs (`miosa` for Python, `@miosa/sdk` for TypeScript, and equivalents in Go, Java, Elixir). Migration is a single import change; the API surface is identical.
### Removed
- The separate sandbox CLI path is removed; all CLI sandbox commands route through the main API.
- Legacy agent endpoints (`/computers/:id/cua/*`, `/computers/:id/agent/*`) now return `410 Gone`. Use desktop primitives directly or the MCP server.
---
## Apr 28 – May 4, 2026
### CLI: public install via Homebrew
The MIOSA CLI is now installable via Homebrew:
```sh
brew install miosa-dev/tap/msk
```
The CLI supports `msk computers`, `msk sandboxes`, `msk deploy`, and `msk auth`. A device-flow OAuth login ties the CLI to your workspace in one browser step. See [CLI reference →](/docs/cli/)
### Account security pages
New security section in account settings: active sessions, passkey management, two-factor setup, and connected OAuth providers. Sessions can be revoked individually.
### Admin: email campaign composer
Platform operators can now compose and send targeted email campaigns to workspace segments directly from the admin console.
### Improved: AI engine token tracking
Token usage from the AI engine is now tracked per-computer and surfaced in the usage API. Metered usage accumulates in real time against the workspace's billing period. See [Usage and Billing →](/docs/platform/usage-and-billing/)
### Improved: frontend workspace tabs and GPU pricing
Workspace settings tabs now persist the active tab across navigation. GPU tier pricing is correctly reflected on the computer create form and the pricing page.
### Improved: admin observability and intelligence gateway
Admin dashboard surfaces per-tenant active computer count, sandbox throughput, and deploy queue depth. The intelligence gateway proxy correctly handles streaming responses under load.
---
## Apr 21 – 27, 2026
### Computer tab scrolling fixed
Long-running computers with many open tabs (terminal, preview, files, etc.) now scroll the tab strip horizontally rather than overflowing off-screen.
### Computer create: services panel
The computer create flow now includes a services panel for attaching data services (Postgres, Redis, object storage) at creation time. Environment variables are injected automatically.
### Environment variable persistence
Environment variables set on a computer or sandbox are persisted across stop/start cycles. Variables set via API are visible in the in-computer terminal immediately.
### Warm pool admin controls
Platform operators can configure warm pool size, pre-warm image targets, and drain schedules from the admin console.
---
## Apr 14 – 20, 2026
### OpenComputers resources across all five SDKs
Hosts, jobs, files, tunnels, and inference cluster resources are now available in all five SDKs (Python, TypeScript, Go, Elixir, Java). Async variants included in Python. See [SDK references →](/docs/sdks/python/)
### SDK surface expansion
Ten new resource namespaces added: Sandboxes, Deployments, Versions, Releases, Runtime Instances, Domains, Data Services (Postgres, Redis, Storage), Network Policy, and Credits. Coverage tables are on each SDK page.
### envd: network tunnels
The in-computer daemon now supports exposing internal services as tunnels — useful for previewing dev servers running inside a computer from outside the VM boundary. See [Previews →](/docs/develop/previews/)
---
## Mar 31 – Apr 13, 2026
### Deployments API
The Deployments API is in production: `POST /api/v1/sandboxes/:id/deploy` publishes a sandbox, `GET /api/v1/deployments` lists deployment history, and `POST /api/v1/deployments/:id/rollback` re-points the active version. All operations are available in the SDK and dashboard. See [Deploy →](/docs/deploy/overview/)
### Sandbox, Deploy, and Intelligence dashboards
The dashboard ships three new top-level sections: Sandboxes (live usage, snapshot list), Deploy (deployment history, build logs, rollback), and Intelligence (AI engine usage, cost trends).
### API drift CI gate
A new CI check compares the server's declared API surface against all five SDK clients on every pull request. Contract mismatches — missing endpoints, changed field names, type drift — fail the build before they reach production.
### Docs site launched
The MIOSA docs site launched with the unified API key format (`msk_*`), Mermaid diagram rendering, six-language SDK tabs, and full-text search.
---
## Mar 2 – 29, 2026
### Compute platform in production
The compute engine and platform API are deployed to production. The provisioning flow is hardened with retry logic, crash guards, and correct error propagation. OAuth state mismatch now redirects to login rather than surfacing a raw error.
### Computer rename complete
"Instance" is renamed to "Computer" across the API, dashboard, SDKs, and documentation. Existing API clients using `/instances/*` continue to work via redirect; new code should use `/computers/*`. See [Computers →](/docs/computers/overview/)
### Computer: `/enter` endpoint
`POST /api/v1/computers/:id/enter` returns a scoped session token for direct desktop access. Used by the embed flow and the CLI `msk computers enter` command.
### 26 app templates with tier system
The computer create flow ships 26 starting templates (landing page, SaaS scaffold, data dashboard, agent workspace, and more) organized by tier: free, pro, and enterprise. The template picker is a split-panel modal with live previews.
### Security hardening
Sixteen security fixes: secret redaction from configure responses, XSS prevention on user-controlled fields, timer leak fixes, provisioning crash guard, and tenant slug collision prevention. Internal auth tokens are no longer included in API responses to unprivileged callers.
### Command Center
The Command Center UI is live — manage AI agent sessions across computers, track cost by session, and view real-time agent status. Backend scheduling and cost accounting routes are wired.
---
## Feb 2 – Mar 1, 2026
### Platform foundation shipped
Stripe billing, audit logging, email notifications, promo codes, and OAuth providers are in production. Redis session persistence ensures computer state survives backend restarts.
### Go backend fully retired
The last Go backend service is archived. The entire stack now runs on Elixir. Dead proxy code and legacy stubs are removed.
### Documentation architecture
Internal technical documentation organized across all five service packages (703 files), covering architecture decisions, API contracts, deployment procedures, and runbooks.
---
## Jan 5 – Feb 1, 2026
### Platform foundation
Auth, billing groundwork, and initial security audit complete. 81 known issues triaged; 27 legacy backend issues closed.
---
# CLI Reference
URL: https://miosa.ai/docs/cli
Fallback URL: https://miosa.roberto-c49.workers.dev/docs/cli
Source: src/routes/docs/cli/+page.md
Description: Complete reference for the MIOSA CLI — manage computers, sandboxes, deployments, data services, and AI agents from your terminal.
The `miosa` CLI (`@miosa/cli`) is the command-line entry point for MIOSA. It manages computers, sandboxes, deployments, data services, and AI agents — all from your terminal.
Commands marked **[planned]** are not yet shipped. All others are live in `@miosa/cli@0.2.0`.
## Install
```bash
npm install -g @miosa/cli
```
```bash
pnpm add -g @miosa/cli
```
```bash
brew install miosa-ai/tap/miosa
```
```bash
curl -fsSL https://install.miosa.ai | sh
```
Installs to `~/.local/bin` (non-root) or `/usr/local/bin` (root). Override with `INSTALL_DIR=/usr/local/bin`.
Verify the install:
```bash
miosa --version
```
## Configuration
Config is stored at `~/.miosa/config.toml`:
```toml
api_url = "https://api.miosa.ai/api/v1"
api_key = "msk_..."
```
**Precedence:** CLI flag `--api-key` → `MIOSA_API_KEY` env var → config file → interactive prompt.
| Environment variable | Description |
|---|---|
| `MIOSA_API_KEY` | API key (overrides config file) |
| `MIOSA_BASE_URL` | Custom API endpoint |
| `MIOSA_DEBUG=1` | Enable verbose debug output |
---
## Command Reference
### Getting Started
#### `miosa login [--api-key key]`
Authenticate with your MIOSA API key. Saves the key to `~/.miosa/config.toml`.
Get an API key from [miosa.ai/settings/api-keys](https://miosa.ai/settings/api-keys).
```bash
miosa login
miosa login --api-key msk_yourkey
echo "msk_yourkey" | miosa login # CI/non-TTY
```
#### `miosa logout`
Remove the stored API key from `~/.miosa/config.toml`.
#### `miosa whoami`
Print the currently authenticated user, tenant, and plan.
```bash
miosa whoami
```
#### `miosa status`
Show auth state, endpoint, tenant, plan, credit balance, and active resource counts.
```bash
miosa status
```
#### `miosa doctor`
Run environment diagnostics — checks auth, API reachability, CLI version, and shell integration.
```bash
miosa doctor
```
#### `miosa config [get|set|unset] [key] [value]`
Read or write values in `~/.miosa/config.toml`.
```bash
miosa config get api_url
miosa config set api_url https://your-instance.ai/api/v1
miosa config unset api_url
```
#### `miosa up`
Start the local development environment. Equivalent to running `miosa dev` with sensible defaults — watches for changes, forwards tunnels, and tails logs.
```bash
miosa up
miosa up --port 3000
```
---
### Computers
#### `miosa computer create [--image img] [--name name] [--size size]`
Provision a new cloud computer.
```bash
miosa computer create
miosa computer create --name dev-box --image ubuntu-22.04 --size medium
miosa computer create --image debian-12 --size large --json
```
| Flag | Description |
|---|---|
| `--name <name>` | Human-readable name |
| `--image <image>` | OS image slug (default: `ubuntu-22.04`) |
| `--size <size>` | `small`, `medium`, `large` (default: `medium`) |
| `--json` | Output as JSON |
#### `miosa computer list [--json]`
List all computers with their state, image, and last-seen time.
```bash
miosa computer list
miosa computer list --json | jq '.[].name'
```
#### `miosa computer get <name-or-id> [--json]`
Show details for a specific computer including live telemetry.
```bash
miosa computer get dev-box
miosa computer get abc12345 --json
```
#### `miosa computer delete <name-or-id> [-f]`
Delete a computer. Prompts for confirmation unless `-f`.
```bash
miosa computer delete dev-box
miosa computer delete abc12345 -f
```
#### `miosa computer start <name-or-id>`
Start a stopped computer.
```bash
miosa computer start dev-box
```
#### `miosa computer stop <name-or-id>`
Stop a running computer.
```bash
miosa computer stop dev-box
```
#### `miosa computer screenshot <name-or-id> [--out file]`
Capture a screenshot of the computer's desktop and save it locally.
```bash
miosa computer screenshot dev-box
miosa computer screenshot dev-box --out screen.png
```
#### `miosa computer apps <name-or-id>`
List running applications on the computer's desktop.
```bash
miosa computer apps dev-box
```
#### `miosa computer open <name-or-id>`
Open the computer's desktop stream in your browser.
```bash
miosa computer open dev-box
```
#### `miosa computer exec <name-or-id> <cmd> [args...]`
Run a command on the computer non-interactively. Streams stdout/stderr and exits with the remote exit code.
```bash
miosa computer exec dev-box npm test
miosa computer exec dev-box ls -- -la /tmp
miosa computer exec dev-box make build --cwd /project --timeout 10m
miosa computer exec dev-box server.js --env NODE_ENV=production --env PORT=8080
```
| Flag | Description |
|---|---|
| `--cwd <dir>` | Working directory on the computer |
| `--env KEY=VAL` | Environment variable (repeatable) |
| `--timeout <duration>` | e.g. `30s`, `2m`, `1h` (default: `5m`) |
#### `miosa computer files <name-or-id> <path>`
List files at a path on the computer.
```bash
miosa computer files dev-box /home/user
miosa computer files dev-box /tmp -la
```
---
### Interactive
#### `miosa shell <name-or-id> [--cmd "..."]`
Open an interactive PTY terminal on a computer. Exits with the remote shell's exit code.
```bash
miosa shell dev-box
miosa shell dev-box --cmd "htop"
```
#### `miosa watch <name-or-id>`
Stream live telemetry and events from a computer.
```bash
miosa watch dev-box
```
#### `miosa agent start <name-or-id> "<task>"`
Dispatch an AI agent task on a computer. Streams thoughts, tool calls, and results in real time.
```bash
miosa agent start dev-box "run the test suite and fix any failures"
miosa agent start dev-box "update all npm dependencies" --steps 20
miosa agent start dev-box "profile the API and find bottlenecks" --timeout 600000
```
| Flag | Description |
|---|---|
| `--model <model>` | Model override (e.g. `qwen3:32b`) |
| `--steps <n>` | Max agent steps (default: `10`) |
| `--timeout <ms>` | Timeout in milliseconds (default: `300000`) |
| `--tools <list>` | Comma-separated tool list (default: `exec,fs`) |
#### `miosa agent ls <name-or-id>`
List all agent sessions on a computer.
```bash
miosa agent ls dev-box
miosa agent ls dev-box --json
```
#### `miosa agent get <name-or-id> <session-id>`
Show details and current state for an agent session.
```bash
miosa agent get dev-box sess_abc123
```
#### `miosa agent task <name-or-id> <session-id> "<task>"`
Send an additional task to a running agent session.
```bash
miosa agent task dev-box sess_abc123 "also update the README"
```
#### `miosa agent stop <name-or-id> <session-id>`
Stop a running agent session.
```bash
miosa agent stop dev-box sess_abc123
```
#### `miosa agent history <name-or-id> <session-id>`
Print the full message history for an agent session.
```bash
miosa agent history dev-box sess_abc123
miosa agent history dev-box sess_abc123 --json
```
---
### Sandboxes
#### `miosa sandbox create [--image img] [--name name]`
Create a new sandbox.
```bash
miosa sandbox create
miosa sandbox create --name my-sandbox --image debian-12-sandbox-v8
```
| Flag | Description |
|---|---|
| `--name <name>` | Human-readable name |
| `--image <image>` | Sandbox image slug (default: `debian-12-sandbox-v8`) |
| `--json` | Output as JSON |
#### `miosa sandbox list [--json]`
List all sandboxes with their state and image.
```bash
miosa sandbox list
miosa sandbox list --json | jq '.[].id'
```
#### `miosa sandbox exec <id> <cmd> [args...]`
Execute a command inside a sandbox. Streams stdout/stderr and returns the exit code.
```bash
miosa sandbox exec sb_abc123 python main.py
miosa sandbox exec sb_abc123 npm test --timeout 5m
```
#### `miosa sandbox run <id> "<code>"`
Run a code snippet directly inside a sandbox.
```bash
miosa sandbox run sb_abc123 "print('hello')"
miosa sandbox run sb_abc123 "import sys; print(sys.version)"
```
#### `miosa sandbox write-file <id> <remote-path> <local-file>`
Write a local file into the sandbox filesystem.
```bash
miosa sandbox write-file sb_abc123 /app/main.py ./main.py
miosa sandbox write-file sb_abc123 /app/config.json ./config.json
```
#### `miosa sandbox read-file <id> <remote-path>`
Read a file from the sandbox filesystem and print it to stdout.
```bash
miosa sandbox read-file sb_abc123 /app/output.txt
miosa sandbox read-file sb_abc123 /var/log/app.log > local.log
```
#### `miosa sandbox upload <id> <local-path> <remote-path>`
Upload a file or directory to the sandbox.
```bash
miosa sandbox upload sb_abc123 ./dist /app/dist
miosa sandbox upload sb_abc123 ./data.csv /tmp/data.csv
```
#### `miosa sandbox download <id> <remote-path> [local-path]`
Download a file or directory from the sandbox.
```bash
miosa sandbox download sb_abc123 /app/output.json ./output.json
miosa sandbox download sb_abc123 /tmp/results ./results/
```
#### `miosa sandbox logs <id> [--follow]`
Stream logs from a sandbox.
```bash
miosa sandbox logs sb_abc123
miosa sandbox logs sb_abc123 --follow
```
---
### Deploy
#### `miosa deploy [--name name] [--env ENV=VAL]`
Deploy the current directory. Detects the framework automatically.
```bash
miosa deploy
miosa deploy --name my-app
miosa deploy --env NODE_ENV=production --env PORT=8080
miosa deploy --region us-east
```
| Flag | Description |
|---|---|
| `--name <name>` | Deployment name (defaults to directory name) |
| `--env KEY=VAL` | Environment variable (repeatable) |
| `--region <region>` | Target region |
| `--json` | Output as JSON |
#### `miosa deployments list [--json]`
List all deployments.
```bash
miosa deployments list
miosa deployments list --json | jq '.[].name'
```
#### `miosa deployments get <name-or-id>`
Show details for a deployment.
```bash
miosa deployments get my-app
```
#### `miosa deployments logs <name-or-id> [--follow]`
Stream logs for a deployment.
```bash
miosa deployments logs my-app
miosa deployments logs my-app --follow
```
#### `miosa deployments restart <name-or-id>`
Restart a deployment.
```bash
miosa deployments restart my-app
```
#### `miosa services list`
List all services attached to deployments.
```bash
miosa services list
```
#### `miosa domains list`
List all custom domains.
```bash
miosa domains list
```
#### `miosa domains add <deployment> <domain>`
Attach a custom domain to a deployment.
```bash
miosa domains add my-app app.example.com
```
#### `miosa domains remove <deployment> <domain>`
Remove a custom domain.
```bash
miosa domains remove my-app app.example.com
```
---
### Data
#### `miosa databases list`
List all managed databases.
```bash
miosa databases list
miosa databases list --json
```
#### `miosa databases create [--name name] [--engine engine]`
Provision a managed database.
```bash
miosa databases create --name app-db --engine postgres
miosa databases create --name cache --engine redis
```
| Flag | Description |
|---|---|
| `--name <name>` | Database name |
| `--engine <engine>` | `postgres` or `redis` |
#### `miosa databases get <name-or-id>`
Show connection details and status for a database.
```bash
miosa databases get app-db
```
#### `miosa storage list`
List all object storage buckets.
```bash
miosa storage list
```
#### `miosa storage create <name>`
Create a new storage bucket.
```bash
miosa storage create my-bucket
```
#### `miosa volumes list`
List all persistent volumes.
```bash
miosa volumes list
```
#### `miosa volumes create <name> [--size size]`
Create a persistent volume.
```bash
miosa volumes create app-data --size 10gb
```
---
### Platform
#### `miosa functions list`
List all serverless functions.
```bash
miosa functions list
```
#### `miosa functions deploy <path>`
Deploy a function from a local file or directory.
```bash
miosa functions deploy ./functions/handler.ts
```
#### `miosa cron list`
List all scheduled cron jobs.
```bash
miosa cron list
```
#### `miosa cron create "<schedule>" "<cmd>"`
Create a cron job using a standard cron expression.
```bash
miosa cron create "0 * * * *" "miosa sandbox exec sb_abc123 python report.py"
```
#### `miosa webhooks list`
List all registered webhooks.
```bash
miosa webhooks list
```
#### `miosa webhooks create --url <url> --events <events>`
Register a new webhook.
```bash
miosa webhooks create --url https://example.com/hook --events computer.started,computer.stopped
```
#### `miosa checkpoints list <name-or-id>`
List checkpoints (snapshots) for a computer.
```bash
miosa checkpoints list dev-box
```
#### `miosa snapshot <name-or-id> [--name name]`
Create a snapshot of a computer or sandbox.
```bash
miosa snapshot dev-box
miosa snapshot dev-box --name before-upgrade
```
#### `miosa network-policy list`
List network policies.
```bash
miosa network-policy list
```
#### `miosa network-policy create --name <name> --rule <rule>`
Create a network policy rule.
```bash
miosa network-policy create --name allow-http --rule "allow:80,443"
```
#### `miosa api-keys list`
List all API keys for the current tenant.
```bash
miosa api-keys list
miosa api-keys list --json
```
#### `miosa api-keys create [--name name] [--scopes scopes]`
Create a new API key.
```bash
miosa api-keys create --name ci-key --scopes computers:read,sandboxes:write
```
#### `miosa api-keys revoke <id>`
Revoke an API key.
```bash
miosa api-keys revoke key_abc123
```
---
### Workflow
#### `miosa dev [--port n]`
Start a local development session with hot reload, tunnel forwarding, and log tailing.
```bash
miosa dev
miosa dev --port 3000
```
#### `miosa run <script>`
Run a script defined in `miosa.json` or `package.json`.
```bash
miosa run build
miosa run test
```
#### `miosa link [name-or-id]`
Link the current directory to a MIOSA computer or deployment. Writes a `.miosa` config file.
```bash
miosa link
miosa link dev-box
```
#### `miosa pull <name-or-id> [path]`
Pull files from a computer or deployment into the current directory.
```bash
miosa pull dev-box /app/dist
miosa pull my-deploy .
```
#### `miosa tunnel open <name-or-id> --port <n>`
Expose a computer port publicly via a MIOSA tunnel.
```bash
miosa tunnel open dev-box --port 3000
miosa tunnel open dev-box --port 8080 --name my-app --watch
```
| Flag | Description |
|---|---|
| `--port <n>` | Port to expose (required) |
| `--name <slug>` | Optional tunnel name |
| `--watch` | Stay open and stream live events |
#### `miosa tunnel list <name-or-id>`
List active tunnels on a computer.
#### `miosa tunnel close <name-or-id> <slug>`
Revoke a tunnel.
#### `miosa mcp serve [--port n]`
Start a local MCP (Model Context Protocol) server that exposes MIOSA resources to AI tools.
```bash
miosa mcp serve
miosa mcp serve --port 8765
```
#### `miosa cp <src> <dst>`
Copy files between local and remote. Use `host:/path` notation for remote paths.
```bash
# Upload
miosa cp ./app.tar.gz dev-box:/tmp/
miosa cp -r ./dist dev-box:/var/www/html/
# Download
miosa cp dev-box:/var/log/app.log ./
miosa cp dev-box:/home/user/report.pdf ~/Downloads/
```
**Flags:** `-r` / `--recursive` for directories.
---
## Exit Codes
| Code | Meaning |
|---|---|
| `0` | Success |
| `1` | User error (bad arguments, resource not found, etc.) |
| `2` | Network error |
| `3` | Authentication / authorization error |
| `4` | Server error |
---
## Troubleshooting
**"No API key configured"** — Run `miosa login`.
**"Computer not found"** — Check `miosa computer list` for the correct name or ID.
**"Insufficient credits"** — Top up at [miosa.ai/billing](https://miosa.ai/billing).
**Network errors** — Check your connection. Enable debug output:
```bash
MIOSA_DEBUG=1 miosa computer list
```
**Custom endpoint (self-hosted)**:
```bash
MIOSA_BASE_URL=https://your-instance.ai miosa computer list
```
---
# BYOC / OpenComputers
URL: https://miosa.ai/docs/computers/byoc
Fallback URL: https://miosa.roberto-c49.workers.dev/docs/computers/byoc
Source: src/routes/docs/computers/byoc/+page.md
Description: Register your own hardware (Mac, Linux, Windows). One command, your machine becomes a MIOSA computer. Apache 2.0.
**OpenComputers** is MIOSA's bring-your-own-compute path. Install the agent on your own hardware — laptops, desktops, mini-PCs, even Raspberry Pi for light workloads — and it registers with MIOSA over an outbound WebSocket. Your machine becomes a MIOSA computer that you (or your AI agents) can drive through the same API as cloud computers.
Apache 2.0. Local-first scheduling — work runs on your hardware when there's capacity; cloud overflow when there isn't.
## Why BYOC
- **Already own the hardware.** No reason to pay for cloud capacity for workloads you have machines for.
- **Local LLMs, local models.** GPU on your machine, MIOSA orchestrates the work.
- **Data residency.** Work runs on your hardware, not in our datacenter.
- **Cloud overflow.** Scale beyond your local fleet when needed; MIOSA places overflow on its cloud.
- **Same API.** Your agent code doesn't care if a job runs on `miosa-host-01` (cloud) or `dad-mac-mini` (your home network).
## Install the agent (one command)
```bash
# On the host machine
curl -fsSL https://miosa.ai/install | sh
```
The installer:
1. Detects your OS (macOS / Linux / Windows; WSL2 supported).
2. Downloads the OSA agent binary.
3. Prompts for a registration token (from your MIOSA dashboard).
4. Starts the agent as a system service (`launchd` / `systemd` / `Windows Service`).
The agent maintains an outbound WebSocket to MIOSA — no inbound ports, no public IP. NAT-friendly.
## Three modes
| Mode | What's exposed | Use case |
|---|---|---|
| **Direct** | The host machine itself is one Computer | Use your laptop as a computer for a single agent |
| **VM dispatch** | The agent creates Firecracker microVMs on the host; each is a Computer | Run multiple parallel agents on one beefy machine |
| **Slicing** | One physical machine partitioned into N independent desktop sessions | Multi-tenant workstation; pre-K12 / kiosk fleet |
Most setups use VM dispatch — gives you process isolation, snapshots, and lifecycle the same way cloud computers work.
## Use a BYOC computer
```typescript
// List your hosts
const hosts = await miosa.openComputers.hosts.list()
// Create a computer on a specific host
const computer = await miosa.computers.create({
name: "local-agent",
templateType: "miosa-desktop",
hostId: hosts[0].id, // optional; scheduler picks if omitted
})
// From here, identical API as a cloud computer
await computer.desktop.screenshot()
```
The scheduler honors local-first placement by default — if you have available capacity on a BYOC host, jobs land there. Override via explicit `hostId` or set scheduling preferences per workspace.
## Tunnels
When you need direct browser access to a port running inside the host machine (a local dev server, a vendor admin UI, your home Home Assistant), open a MIOSA tunnel:
```typescript
const tunnel = await miosa.openComputers.tunnels.create(hostId, {
port: 8123,
name: "home-assistant",
})
console.log(tunnel.public_url)
// → https://home-assistant.your-workspace.tunnels.miosa.app
```
The tunnel is authenticated server-side (MIOSA workspace), proxied through MIOSA's edge, and emerges on your host via the outbound WebSocket. Same security properties as Preview — short-lived tokens for browser-facing access, no public IP needed on your host.
## Resource control
You control what your host shares with MIOSA:
```yaml
# ~/.miosa/agent.yaml
resources:
max_cpu_percent: 50 # cap MIOSA workloads at 50% of CPU
max_ram_mb: 8192 # 8 GB ceiling
max_disk_gb: 100
schedule:
- {days: [Mon-Fri], hours: "09:00-17:00", paused: true} # don't share during work hours
```
The agent reports available capacity to the scheduler in real time. Workloads are gracefully drained off when you go below limits.
## Health and reliability
Hosts emit heartbeats every 30s. After ~2 minutes of missed heartbeats, the scheduler marks the host offline and stops placing new work on it. In-flight work either completes (if the host comes back) or is rescheduled to other hosts.
For production-critical workloads, run multiple BYOC hosts and let MIOSA spread work across them.
## Security
- The agent runs as a regular user (recommended), not root. Computers are Firecracker microVMs the agent spawns; they cannot escape to the host machine.
- The outbound WebSocket is authenticated by a host-specific key tied to your tenant. Compromising one host doesn't expose other hosts in your fleet.
- The agent is open source (Apache 2.0) — auditable.
## Pricing
- **OpenComputers agent: free.** Apache 2.0, run as many hosts as you want.
- **MIOSA scheduling / control plane: per-host fee.** Plans range from free (a few personal hosts) to enterprise (hundreds of hosts, fleet management UI).
See [Pricing](https://miosa.ai/pricing) for details.
## See also
- [Computers / Overview](/docs/computers/overview/) — once registered, BYOC hosts run computers like cloud hosts
- [Desktop Control](/docs/computers/desktop/) — drive any computer, BYOC or cloud
- [API Reference: OpenComputers](/docs/api-reference/open-computers/) — register / list / manage hosts
- [GitHub](https://github.com/Miosa-osa/miosa) — agent source
---
# Desktop Control
URL: https://miosa.ai/docs/computers/desktop
Fallback URL: https://miosa.roberto-c49.workers.dev/docs/computers/desktop
Source: src/routes/docs/computers/desktop/+page.md
Description: 28 methods for driving a Computer's desktop — screenshot, click, type, scroll, drag, keyboard, clipboard, windows, shell, and more.
Once you have a [Computer](/docs/computers/overview/), drive its desktop with these methods. Suitable for AI agents (computer-use models, custom RPA loops) and for direct programmatic control.
## All 28 methods
| Group | Method | Purpose |
|---|---|---|
| **Screen** | `screenshot()` | Capture full desktop as PNG bytes |
| | `screenshot_base64()` | Same, base64-encoded — ready to pass to an LLM vision API |
| **Click** | `click(x, y)` | Generic click — defaults to left button |
| | `left_click(x, y)` | Left-button click at coordinates |
| | `right_click(x, y)` | Right-button click (context menu) |
| | `double_click(x, y)` | Double-click at coordinates |
| **Mouse** | `move_cursor(x, y)` | Move cursor without clicking |
| | `mouse_down(x, y)` | Press and hold the mouse button |
| | `mouse_up(x, y)` | Release a held mouse button |
| | `drag(from_x, from_y, to_x, to_y)` | Click-drag between two coordinates |
| **Keyboard** | `type(text)` | Type a string at the current focus |
| | `key(key)` | Press a single key (e.g. `"Return"`, `"Escape"`) |
| | `hotkey(*keys)` | Simultaneous key combo (e.g. `"ctrl", "c"`) |
| | `key_down(key)` | Press and hold a key |
| | `key_up(key)` | Release a held key |
| **Scroll** | `scroll(direction, clicks)` | Scroll `up`/`down`/`left`/`right` by N notches |
| | `scroll_up(clicks)` | Convenience — scroll up |
| | `scroll_down(clicks)` | Convenience — scroll down |
| | `scroll_left(clicks)` | Convenience — scroll left |
| | `scroll_right(clicks)` | Convenience — scroll right |
| **Clipboard** | `get_clipboard()` | Read the current clipboard text |
| | `set_clipboard(text)` | Write text to the clipboard |
| **Screen info** | `get_screen_size()` | Desktop resolution `{width, height}` |
| | `get_cursor_position()` | Current cursor `{x, y}` in normalized coords |
| **Windows** | `windows()` | List open windows with IDs, titles, positions |
| | `launch(app)` | Open an installed application by name |
| | `focus_window(id)` | Bring a window to the foreground |
| | `get_window_size(id)` | Read a window's `{width, height}` |
| | `set_window_size(id, w, h)` | Resize a window |
| | `get_window_position(id)` | Read a window's `{x, y}` |
| | `set_window_position(id, x, y)` | Move a window |
| | `maximize_window(id)` | Maximize a window |
| | `minimize_window(id)` | Minimize a window to the taskbar |
| | `close_window(id)` | Close a window |
| **Environment** | `get_desktop_environment()` | Detect DE name and version (e.g. `xfce4`) |
| | `set_wallpaper(path)` | Set the desktop background from a file path |
| | `get_accessibility_tree()` | AT-SPI element tree for structured agent perception |
| **Shell** | `bash(cmd)` | Execute a shell command inside the VM |
| | `python(code)` | Execute a Python snippet inside the VM |
| | `write_file(path, content)` | Write content to a file path inside the VM |
| | `read_file(path)` | Read a file path inside the VM |
## Coordinate system
Computers accept and report coordinates in **normalized 0-1000 space**. (0, 0) is top-left, (1000, 1000) is bottom-right, regardless of the actual display resolution.
```python
# Click the visual center of the screen on any display size
computer.left_click(500, 500)
```
When you capture a screenshot, MIOSA scales it to a fixed 1024-wide thumbnail by default (`smartResize`). Your agent reasons about coordinates in that normalized space; MIOSA translates to actual display pixels before sending the event to the VM.
## The control loop
Diagram:
sequenceDiagram
participant A as Agent / SDK
participant API as Computer API
participant envd as envd (port 49983)
participant X as X11 desktop
A->>API: computer.screenshot()
API->>envd: capture frame
envd->>X: XGetImage
X-->>envd: pixel data
envd-->>API: PNG bytes
API-->>A: bytes
A->>API: computer.left_click(500, 300)
API->>envd: inject click event
envd->>X: XSendEvent (ButtonPress)
X-->>envd: ack
envd-->>API: ok
API-->>A: None
---
## Screen
### `screenshot()`
Capture the full desktop as PNG bytes.
```python
png_bytes = computer.screenshot()
# Write to disk
with open("screen.png", "wb") as f:
f.write(png_bytes)
```
Screenshots are PNG. Average 150–300 KB for a 1024-wide thumbnail.
### `screenshot_base64()`
Same as `screenshot()`, but returns a base64-encoded string. Use this when feeding the image directly to an LLM vision API.
```python
b64 = computer.screenshot_base64()
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {"type": "base64", "media_type": "image/png", "data": b64},
},
{"type": "text", "text": "What is on screen? Where should I click to open the browser?"},
],
}],
)
```
---
## Click
### `click(x, y)`
Generic click at coordinates. Defaults to left button.
```python
computer.click(640, 480)
```
### `left_click(x, y)`
Explicit left-button click.
```python
computer.left_click(640, 480)
```
### `right_click(x, y)`
Right-button click — opens context menus.
```python
computer.right_click(640, 480)
```
### `double_click(x, y)`
Double-click — opens files, selects words.
```python
computer.double_click(100, 200)
```
---
## Mouse
### `move_cursor(x, y)`
Reposition the cursor without triggering a click. Useful before `mouse_down` / `mouse_up` sequences or for hover interactions.
```python
computer.move_cursor(500, 400)
```
### `mouse_down(x, y)` / `mouse_up(x, y)`
Low-level press and release. Use these when `drag` is not granular enough — for example, to hover mid-drag or to implement a long-press interaction.
```python
computer.mouse_down(200, 200)
computer.move_cursor(400, 400) # move while held
computer.mouse_up(400, 400)
```
### `drag(from_x, from_y, to_x, to_y)`
Click-drag between two points with smooth interpolation. Use for sliders, drag-to-select, re-ordering list items.
```python
computer.drag(200, 200, 600, 400)
```
---
## Keyboard
### `type(text)`
Type a string as if at a keyboard. Does NOT interpret special key names — use `key()` for those.
```python
computer.type("hello, world")
```
### `key(key)`
Press a single key or chord.
```python
computer.key("Return")
computer.key("Escape")
computer.key("ctrl+a") # select all
computer.key("alt+F4") # close window
```
Chords use `+` as the separator. Key names follow the X11 keysym convention: `Return`, `Tab`, `BackSpace`, `Delete`, `Home`, `End`, `Page_Up`, `Page_Down`, `Up`, `Down`, `Left`, `Right`, `F1`–`F12`, `super`, `ctrl`, `alt`, `shift`.
### `hotkey(*keys)`
Send multiple keys simultaneously — pass each key as a separate argument.
```python
computer.hotkey("ctrl", "c") # copy
computer.hotkey("ctrl", "v") # paste
computer.hotkey("ctrl", "z") # undo
computer.hotkey("ctrl", "shift", "t") # reopen tab
```
### `key_down(key)` / `key_up(key)`
Low-level key press and release. Use when you need to hold a modifier while performing other actions.
```python
computer.key_down("shift")
computer.left_click(800, 200) # shift-click to extend selection
computer.key_up("shift")
```
---
## Scroll
### `scroll(direction, clicks)`
Scroll the mouse wheel. `direction` is one of `"up"`, `"down"`, `"left"`, `"right"`. `clicks` is the number of notches.
```python
computer.scroll("down", clicks=3)
computer.scroll("up", clicks=5)
computer.scroll("right", clicks=2)
```
### Convenience scrolls
```python
computer.scroll_up(clicks=3)
computer.scroll_down(clicks=3)
computer.scroll_left(clicks=2)
computer.scroll_right(clicks=2)
```
`clicks` defaults to `1` for all convenience methods.
---
## Clipboard
### `get_clipboard()`
Read the current clipboard text content.
```python
text = computer.get_clipboard()
print(text)
```
### `set_clipboard(text)`
Write text to the clipboard. After this call, `Ctrl+V` inside the VM will paste the text.
```python
computer.set_clipboard("some text to paste")
computer.hotkey("ctrl", "v")
```
---
## Screen info
### `get_screen_size()`
Returns the desktop resolution as a dict.
```python
size = computer.get_screen_size()
print(size) # {"width": 1920, "height": 1080}
```
### `get_cursor_position()`
Returns the current cursor position in normalized 0-1000 coordinates.
```python
pos = computer.get_cursor_position()
print(pos) # {"x": 512, "y": 300}
```
---
## Windows
### `windows()`
List all open windows. Returns a list of dicts with `id`, `title`, `x`, `y`, `width`, `height`, and `focused`.
```python
wins = computer.windows()
for w in wins:
print(w["id"], w["title"], w["focused"])
```
### `launch(app)`
Open an installed application by name. Available apps depend on the template; `miosa-desktop` ships with Firefox, xterm, Thunar (file manager), and Mousepad (text editor).
```python
computer.launch("firefox")
computer.launch("xterm")
computer.launch("thunar") # file manager
```
### `focus_window(id)` / `get/set_window_size(id, ...)` / `get/set_window_position(id, ...)` / `maximize_window(id)` / `minimize_window(id)` / `close_window(id)`
Full window management via window ID returned by `windows()`.
```python
wins = computer.windows()
w = wins[0]
# Bring to foreground
computer.focus_window(w["id"])
# Read geometry
size = computer.get_window_size(w["id"])
pos = computer.get_window_position(w["id"])
# Set geometry
computer.set_window_size(w["id"], 1280, 800)
computer.set_window_position(w["id"], 100, 50)
# State changes
computer.maximize_window(w["id"])
computer.minimize_window(w["id"])
computer.close_window(w["id"])
```
---
## Environment
### `get_desktop_environment()`
Returns the active desktop environment name and version.
```python
de = computer.get_desktop_environment()
print(de) # {"name": "xfce4", "version": "4.18"}
```
### `set_wallpaper(path)`
Set the desktop background image from a path inside the VM. Combine with `write_file` to push a custom image first.
```python
# Push wallpaper then apply it
computer.write_file("/tmp/bg.png", open("bg.png", "rb").read())
computer.set_wallpaper("/tmp/bg.png")
```
Use `set_wallpaper` together with `write_file` to apply per-tenant branding automatically after computer creation. The wallpaper is stored in the VM's filesystem and persists across stop/start cycles.
### `get_accessibility_tree()`
Returns the AT-SPI accessibility tree as a structured dict. Use this for agent perception when coordinates alone are insufficient — for example, to extract button labels, form field names, or reading order without OCR.
```python
tree = computer.get_accessibility_tree()
# tree is a nested dict of accessible elements
# {"role": "frame", "name": "Firefox", "children": [...]}
```
---
## Shell
These methods execute code inside the VM directly — no GUI interaction required.
### `bash(cmd)`
Execute a shell command. Returns stdout as a string.
```python
output = computer.bash("ls /home/user/Desktop")
print(output)
# Install a package
computer.bash("sudo apt-get install -y curl")
# Launch Firefox in background (so the call returns immediately)
computer.bash("firefox &")
```
### `python(code)`
Execute a Python snippet inside the VM. Returns stdout.
```python
result = computer.python("print(1 + 1)")
print(result) # "2"
# Multi-line
code = """
data = {"status": "ok"}
print(json.dumps(data))
"""
output = computer.python(code)
```
### `write_file(path, content)`
Write a file into the VM's filesystem. `content` can be a string or bytes.
```python
computer.write_file("/home/user/script.py", "print('hello')")
computer.bash("python3 /home/user/script.py")
```
### `read_file(path)`
Read a file from the VM's filesystem. Returns the content as a string.
```python
content = computer.read_file("/home/user/.bashrc")
print(content)
```
---
## Agent reasoning loop
At the application level, an agent drives the computer through a perception-action cycle:
Diagram:
sequenceDiagram
participant AG as Agent
participant CM as Computer
AG->>CM: screenshot_base64()
CM-->>AG: PNG (base64)
AG->>AG: reason — what to do next?
AG->>CM: left_click(x, y)
CM-->>AG: ok
AG->>CM: type(text)
CM-->>AG: ok
AG->>CM: screenshot_base64()
CM-->>AG: PNG (base64)
AG->>AG: task complete?
## Full agent loop example
```python
from miosa import Miosa
miosa_client = Miosa(api_key=os.environ["MIOSA_API_KEY"])
claude_client = anthropic.Anthropic()
computer = miosa_client.computers.create(
name="browser-agent",
template="miosa-desktop",
size="small",
)
computer.start()
# Open the browser
computer.launch("firefox")
# Agent loop
for _ in range(10):
b64 = computer.screenshot_base64()
response = claude_client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": b64}},
{"type": "text", "text": "Navigate to miosa.ai and take a screenshot of the homepage."},
],
}],
)
# Parse action from response and dispatch
text = response.content[0].text
if "left_click" in text:
# extract x, y from model output and act
computer.left_click(500, 500)
elif "type" in text:
computer.type("https://miosa.ai\n")
computer.stop()
```
---
## Latency reference
Desktop methods run synchronously over authenticated HTTP RPC. Typical round-trip from MIOSA edge:
| Method type | Typical latency |
|---|---|
| Click, key, scroll, type | 30–80 ms |
| Screenshot | 100–300 ms (compression-bound) |
| Window list | 50–100 ms |
| `bash()` / `python()` | 50 ms + command execution time |
For high-frequency agent loops, prefer `bash()` for multi-step operations over many individual GUI calls.
---
## Authentication
All desktop methods require the same workspace API key as every other MIOSA resource. Call them server-side. For browser-side direct control (computer-use models running in the browser), mint a scoped token — see [Browser Tokens](/docs/platform/browser-tokens/).
---
## See also
Pick a template, size, and workspace. Create and start a computer.
[Open →](/docs/computers/overview/)
Show the live desktop in a browser iframe using short-lived stream tokens.
[Open →](/docs/computers/embedding/)
Wire format for all 28 methods — request/response shapes, error codes, rate limits.
[Open →](/docs/api-reference/desktop/)
---
# Embedding & Streaming
URL: https://miosa.ai/docs/computers/embedding
Fallback URL: https://miosa.roberto-c49.workers.dev/docs/computers/embedding
Source: src/routes/docs/computers/embedding/+page.md
Description: Embed a live Computer desktop in your own UI via KasmVNC. Pixel streaming, input forwarding.
To show a live desktop session in your end-user's browser — for "watch the AI work" UI, manual takeover, or kiosk-style apps — embed MIOSA's KasmVNC stream.
## The flow
```
1. Your backend asks MIOSA for a terminal ticket for a running computer
2. MIOSA returns a short-lived signed URL pointing at the stream gateway
3. Your frontend renders an iframe with that URL
4. The user sees the live desktop; keyboard/mouse input is forwarded into the VM
```
## Mint a ticket
```typescript
const ticket = await computer.terminalTicket()
console.log(ticket.url) // signed stream URL
console.log(ticket.expires_at) // ISO timestamp
```
```bash
curl -X POST https://api.miosa.ai/api/v1/computers/$CID/terminal-ticket \
-H "Authorization: Bearer $MIOSA_API_KEY"
```
Response:
```json
{
"token": "miosa_term_...",
"url": "https://stream.miosa.app/computer/cmp_xxx?ticket=miosa_term_...",
"expires_at": "2026-05-14T19:30:00Z"
}
```
`url` is what you embed. `token` is also returned in case you need to construct your own URL (custom stream proxy, etc.).
## Embed
```jsx
```
```html
```
KasmVNC handles pixel streaming + input forwarding inside the iframe. The user clicks / types in the iframe and those events go to the desktop.
## What's streamed
- **Pixels** — encoded video; KasmVNC defaults to a lossy codec tuned for desktop content.
- **Cursor position** — overlay rendered client-side.
- **Audio** — optional, off by default. Pass `?audio=1` if your app needs it.
- **Clipboard** — bidirectional, gated by the iframe's `allow="clipboard-read; clipboard-write"`.
## Read-only mode
For "user watches the agent" UI where the end-user shouldn't interact with the desktop:
```typescript
const ticket = await computer.terminalTicket({ readOnly: true })
```
Input events from the iframe are dropped at the stream gateway. Useful for compliance-sensitive flows where only the AI agent should touch the screen.
## Bandwidth
Default codec / bitrate is tuned for typical desktop content (~500 Kbps - 2 Mbps). For high-motion content (video playback inside the VM), the codec adapts but expect higher bitrate. The stream gateway is geo-routed to the user's nearest region for lower latency.
## Lifetime
Tickets are short-lived (default 1 hour, max 24 hours; configurable per plan). When a ticket expires the stream disconnects; your frontend should watch `expires_at` and re-mint before that.
Multiple tickets per computer are fine. Multiple browsers can watch the same desktop simultaneously — they all see the same pixels.
## Custom domains / branded streaming
White-label customers can configure `stream.<your-domain>` to front MIOSA's stream gateway, so end users see your brand in the iframe URL. Same DNS / TLS flow as [Deployment Domains](/docs/deploy/domains/), different target. Contact support to set up.
## CSP
The stream gateway emits `Content-Security-Policy` allowing the stream to be embedded from approved origins:
- Default: `*.miosa.app`, `*.miosa.ai`, `localhost:4000`.
- For white-label: add your platform origin via tenant config.
If your iframe doesn't render, check the parent page's CSP isn't blocking the iframe and that the stream gateway CSP includes your origin.
## Audit
Each ticket issuance emits an audit event with:
- The computer ID
- The token prefix
- Issuing API key
- External attribution
If a ticket is leaked, you can revoke all outstanding tickets for a computer:
```http
DELETE /api/v1/computers/$CID/terminal-tickets
```
## See also
- [Overview](/docs/computers/overview/) — what you're embedding
- [Desktop Control](/docs/computers/desktop/) — programmatic control of the same desktop
- [Browser Tokens](/docs/platform/browser-tokens/) — analogous pattern for sandbox previews
---
# Computers (Desktop)
URL: https://miosa.ai/docs/computers/overview
Fallback URL: https://miosa.roberto-c49.workers.dev/docs/computers/overview
Source: src/routes/docs/computers/overview/+page.md
Description: Full Linux desktops in the cloud — Xfce, Firefox, terminal — controlled via 28 Python SDK methods including screenshot, click, type, scroll, clipboard, windows, shell, and more.
A **Computer** is a Firecracker microVM running a full Linux desktop (Xfce). Use it when you need a real GUI environment: browser automation, computer-use AI agents, RPA, or screenshot-driven testing.
For code execution, dev servers, and most AI agent build loops, [Sandboxes](/docs/develop/sandboxes/) are cheaper and faster. Choose based on whether your workload needs a rendered desktop.
## When to use Computer vs Sandbox
| Use case | Pick |
|---|---|
| Run untrusted code, dev server, `npm install` | **Sandbox** |
| Computer-use agent, GUI automation | **Computer** |
| Browser automation that needs a real rendered DOM | **Computer** |
| AI-agent build loop (code-gen, hot reload, preview) | **Sandbox** |
| Screenshot-based UI testing | **Computer** |
| Headless Playwright / Selenium | **Sandbox** (cheaper) |
| Controlling desktop apps (Figma, Slack, etc.) | **Computer** |
## What is in the box
- **Xfce desktop** with Firefox and common Linux apps pre-installed.
- **Terminal** — `bash`, `python`, standard CLI toolchain.
- **Persistent storage** — files survive stop/start cycles via snapshot restore.
- **VNC + WebSocket streaming** — embed the desktop live in a browser via MIOSA's pixel-stream protocol.
- **OSA agent** — pre-installed, disabled by default. Activate it for autonomous in-VM AI task execution.
## Quick example
```python
client = miosa.Miosa(api_key=os.environ["MIOSA_API_KEY"])
# Create and start a desktop computer
computer = client.computers.create(name="agent-1", template="miosa-desktop", size="small")
computer.start()
# Capture the screen, interact with it
png_bytes = computer.screenshot()
computer.left_click(640, 480)
computer.double_click(100, 200)
computer.type("hello world")
computer.key("Return")
computer.scroll("down", clicks=3)
computer.hotkey("ctrl", "c")
computer.bash("firefox &")
```
```typescript
const miosa = new Miosa({ apiKey: process.env.MIOSA_API_KEY! })
// Create and start a desktop computer
const computer = await miosa.computers.create({
name: "agent-1",
templateType: "miosa-desktop",
size: "small",
})
await computer.start()
// Capture the screen, interact with it
const png = await computer.screenshot()
await computer.leftClick(640, 480)
await computer.doubleClick(100, 200)
await computer.type("hello world")
await computer.key("Return")
await computer.scroll({ direction: "down", clicks: 3 })
await computer.hotkey("ctrl", "c")
await computer.bash("firefox &")
```
## Desktop action reference
All 28 methods available on a running computer:
| Group | Method | Description |
|---|---|---|
| **Screen** | `screenshot()` | Capture full desktop as PNG bytes |
| | `screenshot_base64()` | Same, base64-encoded — ready for LLM vision APIs |
| **Click** | `click(x, y)` | Generic click (defaults to left button) |
| | `left_click(x, y)` | Left-button click |
| | `right_click(x, y)` | Right-button click (context menu) |
| | `double_click(x, y)` | Double-click |
| **Mouse** | `move_cursor(x, y)` | Move cursor without clicking |
| | `mouse_down(x, y)` | Press and hold the mouse button |
| | `mouse_up(x, y)` | Release a held mouse button |
| | `drag(from_x, from_y, to_x, to_y)` | Click-drag between two coordinates |
| **Keyboard** | `type(text)` | Type a string at the current focus |
| | `key(key)` | Send a single key (e.g. `"Return"`, `"ctrl+a"`) |
| | `hotkey(*keys)` | Simultaneous key combo (e.g. `"ctrl", "c"`) |
| | `key_down(key)` | Press and hold a key |
| | `key_up(key)` | Release a held key |
| **Scroll** | `scroll(direction, clicks)` | Scroll `up`/`down`/`left`/`right` |
| | `scroll_up/down/left/right(clicks)` | Convenience scroll methods |
| **Clipboard** | `get_clipboard()` | Read clipboard text |
| | `set_clipboard(text)` | Write text to clipboard |
| **Screen info** | `get_screen_size()` | Desktop resolution `{width, height}` |
| | `get_cursor_position()` | Current cursor `{x, y}` in normalized coords |
| **Windows** | `windows()` | List open windows with IDs, titles, positions |
| | `launch(app)` | Open an installed app by name |
| | `focus_window(id)` | Bring a window to the foreground |
| | `get/set_window_size(id, ...)` | Read or set window dimensions |
| | `get/set_window_position(id, ...)` | Read or set window position |
| | `maximize/minimize/close_window(id)` | Change window state |
| **Environment** | `get_desktop_environment()` | DE name and version (e.g. `xfce4`) |
| | `set_wallpaper(path)` | Set desktop background from a VM file path |
| | `get_accessibility_tree()` | AT-SPI element tree for structured agent perception |
| **Shell** | `bash(cmd)` | Execute a shell command inside the VM |
| | `python(code)` | Execute a Python snippet inside the VM |
| | `write_file(path, content)` | Write a file into the VM's filesystem |
| | `read_file(path)` | Read a file from the VM's filesystem |
See [Desktop Control](/docs/computers/desktop/) for full examples, parameter details, and coordinate system documentation.
## Workspace-scoped creation
Computers can be created inside a named workspace so that resources are isolated and billed separately. Pass `external_workspace_id` to attribute the computer to a tenant in your platform.
```python
client = miosa.Miosa(api_key=os.environ["MIOSA_API_KEY"])
computer = client.computers.create(
name="agent-1",
template="miosa-desktop",
size="small",
external_workspace_id="acme-corp", # your tenant identifier
metadata={"agent_run": "run-2026-05-17"},
)
computer.start()
```
```typescript
const miosa = new Miosa({ apiKey: process.env.MIOSA_API_KEY! })
const computer = await miosa.computers.create({
name: "agent-1",
templateType: "miosa-desktop",
size: "small",
externalWorkspaceId: "acme-corp",
metadata: { agentRun: "run-2026-05-17" },
})
await computer.start()
```
`external_workspace_id` is a free-form string you control — use your own tenant/org identifier. Usage is tracked per workspace in the billing dashboard so you can attribute compute costs to individual customers.
## White-label desktops
Computers support per-tenant desktop branding. After starting a computer, push a wallpaper and apply it with two SDK calls. The wallpaper persists across stop/start cycles because it is stored in the VM's snapshotted filesystem.
```python
client = miosa.Miosa(api_key=os.environ["MIOSA_API_KEY"])
computer = client.computers.create(
name="acme-agent",
template="miosa-desktop",
size="small",
external_workspace_id="acme-corp",
)
computer.start()
# Push the tenant wallpaper from your server's filesystem
computer.write_file("/tmp/acme-wallpaper.png", open("assets/acme-bg.png", "rb").read())
computer.set_wallpaper("/tmp/acme-wallpaper.png")
# Optionally verify the DE
de = computer.get_desktop_environment()
print(de) # {"name": "xfce4", "version": "4.18"}
```
```typescript
const miosa = new Miosa({ apiKey: process.env.MIOSA_API_KEY! })
const computer = await miosa.computers.create({
name: "acme-agent",
templateType: "miosa-desktop",
size: "small",
externalWorkspaceId: "acme-corp",
})
await computer.start()
// Push and apply the tenant wallpaper
const bg = fs.readFileSync("assets/acme-bg.png")
await computer.writeFile("/tmp/acme-wallpaper.png", bg)
await computer.setWallpaper("/tmp/acme-wallpaper.png")
```
For production deployments, bake the wallpaper directly into a custom template snapshot so every new computer boots with the correct branding — no post-boot setup required. See the Templates documentation for details.
## How it fits together
Diagram:
graph LR
SDK["Python / TypeScript SDK"] -->|REST| API["MIOSA API"]
API --> VM["Firecracker VM\n(Xfce desktop)"]
VM --> envd["envd agent\n(port 49983)"]
envd -->|X11 input| Desktop["Desktop session"]
envd -->|PNG frame| Stream["Pixel stream"]
Stream -->|WebSocket| Browser["Browser iframe"]
Your code talks to the MIOSA API. MIOSA routes actions to `envd` running inside the VM. `envd` drives the X11 session and returns screenshots or action confirmations. For embedded views, MIOSA streams the desktop over WebSocket to a browser iframe.
## Embed the desktop in a browser
Mint a stream token from your backend, then pass the URL to a browser iframe:
```python
token = computer.stream_token()
# token["url"] → short-lived WebSocket stream URL
# Pass this URL to your frontend; do not embed your API key.
```
```typescript
const token = await computer.streamToken()
// token.url → short-lived WebSocket stream URL
//
```
See [Embedding & Streaming](/docs/computers/embedding/) for the full pattern including authentication, iframe policy, and expiry handling.
## Lifecycle
```
create → provisioning → running ⇆ stopped → deleted
```
| State | Description |
|---|---|
| `provisioning` | Firecracker is booting the rootfs |
| `running` | Desktop is up, accepting API actions |
| `stopped` | Snapshotted; file state preserved; restartable |
| `deleted` | Permanent — not reversible |
Stopping a computer pauses billing (or charges at the stopped rate, depending on your plan). Restarting resumes from the snapshot.
## Sizing
| Size | vCPU | RAM | Disk | Typical use |
|---|---|---|---|---|
| `small` | 2 | 4 GB | 20 GB | Single agent, basic browsing |
| `medium` | 4 | 8 GB | 50 GB | Heavier desktop apps, faster page loads |
| `large` | 8 | 16 GB | 100 GB | Multi-app, large-context AI workloads |
GPU support for accelerated desktop and GPU-aware in-VM agents is plan-dependent.
Computers ship with the OSA agent installed but disabled by default. Activate it to give your computer an in-VM AI assistant that can run multi-step desktop tasks autonomously. OSA runs locally inside the computer — see the OSA documentation for details.
## Next
Full action reference: screenshot, click, type, key, scroll, drag, hotkey, windows, accessibility tree, and more.
[Open →](/docs/computers/desktop/)
Embed the live pixel stream in your own UI using short-lived stream tokens.
[Open →](/docs/computers/embedding/)
Bring your own hardware (Mac, Linux, Windows). One command registers your machine as a MIOSA computer.
[Open →](/docs/computers/byoc/)
---
# Core Concepts
URL: https://miosa.ai/docs/concepts
Fallback URL: https://miosa.roberto-c49.workers.dev/docs/concepts
Source: src/routes/docs/concepts/+page.md
Description: The canonical architecture reference for MIOSA — six concrete primitives, how they compose into deployments, data services, desktop Computers, and the white-label tenancy model.
# Core Concepts
MIOSA is a cloud platform for building, previewing, and deploying applications — primarily by AI agents, but equally usable by developers directly. Every operation maps to one of the six primitive resource types below. Everything else is composition.
If you read only one sentence: **Sandbox → Builder → Release → Version → Deployment → Domain.** That is the full publish pipeline.
---
## Physical base
MIOSA runs on bare-metal compute hosts. Each host runs [Firecracker](https://firecracker-microvm.github.io/), a hypervisor that starts microVMs in approximately 150 ms. Every workload — sandbox, builder, runtime instance, computer — is one of these microVMs. The product names below describe what the VM does, not how it is implemented.
You never interact with Firecracker directly.
---
## The six concrete things
### Sandbox
A **Sandbox** is a mutable Firecracker microVM where an agent or developer builds an app. It boots from a MIOSA sandbox image (for example, `miosa-sandbox`) and ships with a full development environment: Node, Python, git, package managers, and a terminal.
Key properties:
- **Mutable and stateful.** Files in `/workspace` persist across exec calls until the sandbox is destroyed or auto-suspended.
- **Exec surface.** Call `exec` to run any shell command — `npm install`, `python app.py`, `git clone` — and get stdout/stderr back.
- **File I/O.** Read and write files at arbitrary paths. Agents use this to generate code, configs, and assets.
- **Preview tunnels.** Open any port inside the sandbox to the public internet via a Preview URL (see below).
- **Snapshots.** Freeze the full VM state, fork from it, or restore it. Enables fast branching and rollback during development.
- **Auto-suspend.** Idle sandboxes suspend after a configurable timeout to save compute. Resume is near-instant from snapshot.
A Sandbox is **not production**. Production traffic never routes to a sandbox. The sandbox is where you build the thing that will become production.
[Sandboxes reference →](/docs/develop/sandboxes/)
### Preview
A **Preview** is a temporary public URL that tunnels to a port inside a running sandbox.
```
https://abc12345.preview.miosa.app
```
When you create a preview, you specify the sandbox port (typically 3000 for a dev server). MIOSA's edge proxy maps that hostname to the sandbox's internal IP and port. As files change inside the sandbox and the dev server hot-reloads, the preview updates live.
Key properties:
- **Mutable.** The preview reflects the live state of the sandbox at every request.
- **Ephemeral.** The preview URL dies when the sandbox is destroyed or suspended.
- **Share tokens.** For embedding previews in your own product's UI, your backend mints a short-lived share token. The token is safe to put in a browser `<iframe>` — it grants time-limited preview access without exposing the workspace API key.
[Previews reference →](/docs/develop/previews/)
### Builder
A **Builder** is a Firecracker microVM that runs for exactly one publish job. It boots from the dedicated `miosa-builder` template, pulls an immutable source snapshot from the sandbox at publish time, runs the build pipeline, uploads the result, and exits.
Three build modes:
| Mode | Trigger | Output |
|------|---------|--------|
| Static packager | `/workspace` has only static files | Tarball served from MIOSA's edge |
| Railpack auto-detect | Node/Python/Ruby/Go project detected | Dynamic rootfs booted as Runtime Instances |
| Dockerfile | `Dockerfile` present in `/workspace` | Dynamic rootfs booted as Runtime Instances |
The Builder is internal. You never address it directly. It exists so production builds are reproducible (source is frozen at publish time, not the live mutable sandbox) and so your sandbox keeps running while the build executes.
### Release
A **Release** is the immutable artifact produced by a Builder run. It is sha256-keyed and stored in object storage. The same release can be deployed to any number of regions, booted N times, and referenced by multiple versions — the content never changes.
Two shapes:
- **Static release** — a tarball of files (`index.html`, `app.js`, assets). MIOSA's edge serves them directly. No Runtime Instances required.
- **Dynamic release** — a rootfs / OCI image. Booted as Runtime Instances in production.
[Releases reference →](/docs/deploy/releases/)
### Runtime Instance
A **Runtime Instance** is a Firecracker microVM running a dynamic release in production. It boots from the release rootfs (not from the sandbox image), serves real traffic, and is managed by a scheduler and reconciler.
Key properties:
- **Scheduler placement.** Instances are placed on fleet hosts by a scheduler that watches CPU, RAM, and region capacity.
- **Horizontal scaling.** Multiple instances can serve a single version simultaneously.
- **Health checks.** The reconciler replaces instances that fail health checks without manual intervention.
- **Isolation.** Each instance runs in its own microVM, fully isolated from other tenants.
Static deployments have no Runtime Instances — files are served from the edge directly.
[Runtime Instances reference →](/docs/deploy/runtime-instances/)
### Domain
A **Domain** is a DNS hostname routed to a deployment's active version. Three tiers:
- `my-app.miosa.app` — MIOSA-managed subdomain, instant, no DNS change needed.
- `app.yourcustomer.com` — custom domain your customer brings; MIOSA auto-provisions a TLS certificate via Let's Encrypt.
- Internal routing domains for multi-region or canary setups.
Changing the domain mapping does not rebuild the app — it updates routing only. This is how rollback works: repoint the domain at an older ready version.
[Domains reference →](/docs/deploy/domains/)
---
## Deployments, Versions, and the publish pipeline
A **Deployment** is the stable product object — the thing the world thinks of as "the live app." It holds an `active_version_id`. Publishing creates a new **Version**, which references a **Release**. Promoting a version updates `active_version_id`; rollback resets it.
Diagram:
flowchart LR
S[Sandbox] -->|publish| B[Builder]
B -->|builds| R[Release]
R -->|referenced by| V[Version]
V -->|active for| D[Deployment]
Dom[Domain] -->|routes to| D
D -->|spawns| RI[Runtime Instances]
Full object hierarchy:
Diagram:
graph TD
Proj["Project"]
Dep["Deployment smile-dental"]
V17["Version 17 (active)"]
V16["Version 16 (archived)"]
V15["Version 15 (archived)"]
Rel["Release sha:abc"]
RI["Runtime Instances"]
D1["smiledental.miosa.app"]
D2["smiledental.test"]
Proj --> Dep
Dep -->|active_version_id| V17
Dep --> V16
Dep --> V15
V17 --> Rel
Rel --> RI
Dep --> D1
Dep --> D2
Rollback is Version 16 → `active_version_id`. Runtime Instances for Version 17 drain and exit; Version 16 instances start. Domain routing never changes — the domain still points at the same Deployment.
[Deploy overview →](/docs/deploy/overview/)
---
## Data services
Sandboxes and Runtime Instances are ephemeral. **Data Services** are not. MIOSA provisions and operates them outside the version lifecycle — destroying a sandbox or publishing a new version does not affect them.
| Service | What it is |
|---------|-----------|
| Managed Postgres | Fully managed PostgreSQL database. Schema migrations are yours to run; MIOSA provides the connection string. |
| Managed Redis | Persistent Redis cluster for caching, queues, pub/sub. |
| Object Storage | S3-compatible bucket. Useful for uploads, assets, generated files. |
| Volumes | Block storage mounted into Runtime Instances. Persists across restarts. |
| Auth | JWT-based signup/login as a service. One env var gives your project a full auth system. |
Both the sandbox and runtime instances receive credentials as environment variables at boot:
```
DATABASE_URL=postgresql://user:pass@host/dbname
REDIS_URL=redis://host:6379
STORAGE_BUCKET=miosa-ws123-uploads
AUTH_URL=https://auth.miosa.app/proj_abc
AUTH_JWT_SECRET=...
```
Destroying a sandbox does not destroy its database. Publishing a new version does not run migrations — MIOSA gives you the connection string; you run `db:migrate` in your deploy hook.
Managed Postgres and Auth are part of the platform Phase 6–8 rollout. The doc surface is published now so you can plan integration; some endpoints are still being wired in production.
[Data overview →](/docs/data/overview/)
---
## Computers (desktop product)
**Computers** are full Linux desktop VMs running in the cloud with screenshot, click, type, and keyboard APIs. They are a separate product surface from Sandboxes — different lifecycle, different use cases.
| | Sandbox | Computer |
|---|---|---|
| Primary use | Code execution, file I/O, dev servers, agent tool-use | Desktop GUI, browser automation, computer-use agents |
| Display | None (headless) | X11 + VNC |
| Accessibility tree | No | Yes |
| Lifecycle | Short-to-medium lived, auto-suspend | Session-oriented |
| Templates | `miosa-sandbox`, `miosa-node`, etc. | `miosa-desktop` |
Use Sandboxes for building apps. Use Computers when your agent or workflow needs a real screen.
You can also bring your own hardware (Mac, Linux, Windows) and register it as a Computer host via BYOC.
[Computers overview →](/docs/computers/overview/)
---
## White-label tenancy
MIOSA is designed for platforms — you build a product on top of MIOSA, and your customers never know MIOSA exists.
The tenancy model:
- **One MIOSA organization per customer platform.** A platform account has one organization slug and server-side `msk_*` keys.
- **Workspaces and projects for downstream customers.** Use MIOSA workspaces for clients and projects for the apps, sites, documents, and workflows they create.
- **External attribution for your IDs.** Pass `external_workspace_id`, `external_user_id`, and `external_project_id` when you need your database IDs on usage, audit, and list filters.
- **Server-side key, never in the browser.** For browser-side access (embedding a preview, opening a terminal), your backend mints a short-lived scoped token. The `msk_*` key stays server-side.
Diagram:
graph TD
Plat["Your Platform"]
Org["MIOSA Organization one slug · one bill"]
WS["Workspace Dr. Smith Clinic"]
Proj["Project Lead Magnet"]
Sbx["Sandbox"]
Dep["Deployment"]
Plat --> Org
Org --> WS
WS --> Proj
Proj --> Sbx
Proj --> Dep
Filters are always organization-scoped server-side. You cannot cross organizations by passing different workspace, project, or external IDs.
[Platform / White-label overview →](/docs/platform/overview/)
---
## Three naming layers
One thing trips up every new reader: "Computer" has a specific meaning in MIOSA.
| Layer | Term | Meaning |
|-------|------|---------|
| Brand | **MIOSA** | The wordmark and platform. |
| Resource | **Computer** | The desktop GUI product. Not a generic synonym for "VM." |
| AI agent | **OSA / Optimal** | The in-VM AI agent runtime. Optional; installable inside a Sandbox or Computer. |
Sandboxes are not Computers. Runtime Instances are not Computers. Computers are Computers. When you encounter older docs or code that uses "Computer" generically, substitute "Sandbox" or "Runtime Instance" based on context.
---
## What's next
Five-minute hands-on tour. Create a sandbox, write code, publish to a live URL.
[Start →](/docs/quickstart/)
Deep-dive on Sandboxes, file I/O, exec, previews, and the dev loop.
[Develop →](/docs/develop/sandboxes/)
Builder, Release, Runtime Instance, Domains — the full publish pipeline.
[Deploy →](/docs/deploy/overview/)
White-label tenancy, browser tokens, attribution, and API keys.
[Platform →](/docs/platform/overview/)
---
# Cookbook
URL: https://miosa.ai/docs/cookbook
Fallback URL: https://miosa.roberto-c49.workers.dev/docs/cookbook
Source: src/routes/docs/cookbook/+page.md
Description: End-to-end recipes — copy-paste-runnable walkthroughs that showcase the MIOSA platform.
# Cookbook
Practical, end-to-end walkthroughs. Each recipe starts from a blank terminal and ends with something running. Pick the one closest to your use case and adapt from there.
---
Your AI agent receives a prompt, writes code into a sandbox, runs the build, exposes a preview URL, then publishes to production. End deliverable: a deployed Next.js app from a single prompt.
Run a long Python script in a sandbox and stream stdout to your UI in real time via SSE. Includes backpressure handling. End deliverable: a code-execution console-style UI snippet.
Embed MIOSA into your SaaS. Tag every sandbox with your own workspace and user IDs, query usage per tenant, and issue browser tokens so your customers interact directly with their sandbox without seeing your API key.
Checkpoint a long-running task, fork into multiple branches, compare outcomes, and restore to a known-good state on failure.
Boot a Computer (full Linux desktop), screenshot a website, click a button, fill a form, take another screenshot. Implements the screenshot → action → screenshot loop with your own LLM.
---
## Before you start
All recipes share the same prerequisites:
- A MIOSA workspace API key (`msk_live_*`) — see [API Keys](/docs/platform/api-keys/)
- Node 22+ **or** Python 3.11+ installed locally
- The MIOSA SDK installed for your language:
```bash
# Python
pip install miosa
# TypeScript / Node
npm install @miosa/sdk
```
Set the key in your environment once — every recipe reads it from there:
```bash
export MIOSA_API_KEY="msk_live_..."
```
---
# Agent builds an app
URL: https://miosa.ai/docs/cookbook/agent-builds-app
Fallback URL: https://miosa.roberto-c49.workers.dev/docs/cookbook/agent-builds-app
Source: src/routes/docs/cookbook/agent-builds-app/+page.md
Description: Your AI agent receives a prompt, writes code into a sandbox, builds it, and publishes a deployed Next.js app — all from a single prompt.
# Agent builds an app
~10 min Python TypeScript
> **Goal**: Wire an LLM agent to MIOSA so it can write code, build it inside a sandbox, open a live preview, then publish to a production deployment — all from a single user prompt.
>
> **What you'll use**: Sandboxes, Previews, Deployments
## What you'll build
A function `build_and_deploy(prompt)` that hands a prompt to an LLM, streams generated code into a sandbox, runs `npm run build`, surfaces a preview URL, then publishes an immutable production release. The user sees a live URL in under 60 seconds.
The pattern is the same whether you use Anthropic, OpenAI, or any other model — MIOSA is the execution layer, your agent loop is the thinking layer.
## Prerequisites
- A MIOSA workspace API key (`msk_live_*`) — see [API Keys](/docs/platform/api-keys/)
- An Anthropic API key (or substitute your preferred provider)
- Node 22+ or Python 3.11+
## Step 1 — Install and configure
```bash
pip install miosa anthropic
export MIOSA_API_KEY="msk_live_..."
export ANTHROPIC_API_KEY="sk-ant-..."
```
```bash
npm install @miosa/sdk @anthropic-ai/sdk
export MIOSA_API_KEY="msk_live_..."
export ANTHROPIC_API_KEY="sk-ant-..."
```
## Step 2 — Boot a sandbox
Create a Next.js sandbox. The template already has Node, pnpm, and a Next.js scaffold — no `npx create-next-app` needed.
```python
from miosa import Miosa
client = Miosa(api_key=os.environ["MIOSA_API_KEY"])
sbx = client.sandboxes.create(
template_id="nextjs",
timeout_sec=600,
)
print(f"Sandbox ready: {sbx.id}")
```
```typescript
const client = new Miosa({ apiKey: process.env.MIOSA_API_KEY! });
const sbx = await client.sandboxes.create({
templateId: 'nextjs',
timeoutSec: 600,
});
console.log('Sandbox ready:', sbx.id);
```
## Step 3 — Ask the LLM to generate code
Call the model with a system prompt that instructs it to produce a complete `app/page.tsx` file. Keep it deterministic by setting `temperature=0`.
```python
ai = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
def generate_page(prompt: str) -> str:
msg = ai.messages.create(
model="claude-opus-4-5",
max_tokens=4096,
system=(
"You are a Next.js developer. "
"Respond with ONLY the complete content of app/page.tsx. "
"No explanation, no markdown fences, just valid TypeScript."
),
messages=[{"role": "user", "content": prompt}],
)
return msg.content[0].text
user_prompt = "Build a landing page for a dog-walking app with a hero, feature list, and CTA."
page_code = generate_page(user_prompt)
```
```typescript
const ai = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });
async function generatePage(prompt: string): Promise {
const msg = await ai.messages.create({
model: 'claude-opus-4-5',
max_tokens: 4096,
system:
'You are a Next.js developer. ' +
'Respond with ONLY the complete content of app/page.tsx. ' +
'No explanation, no markdown fences, just valid TypeScript.',
messages: [{ role: 'user', content: prompt }],
});
const block = msg.content[0];
return block.type === 'text' ? block.text : '';
}
const userPrompt = 'Build a landing page for a dog-walking app with a hero, feature list, and CTA.';
const pageCode = await generatePage(userPrompt);
```
## Step 4 — Write the file and build
Write the generated code into the sandbox filesystem, install any new dependencies, and run the build. Stream exec output so you can surface progress to the user in real time.
```python
# Write the agent-generated file
sbx.files.write("/workspace/app/page.tsx", page_code)
# Install dependencies (idempotent — next.js template already ran pnpm install)
install = sbx.exec("pnpm install", cwd="/workspace")
if install.exit_code != 0:
raise RuntimeError(f"pnpm install failed:\n{install.stderr}")
# Build
build = sbx.exec("pnpm run build", cwd="/workspace")
if build.exit_code != 0:
# Feed the error back to the agent for a self-correction turn
correction_prompt = (
f"The build failed with this error:\n{build.stderr}\n\n"
"Return a corrected app/page.tsx that fixes the error."
)
page_code = generate_page(correction_prompt)
sbx.files.write("/workspace/app/page.tsx", page_code)
build = sbx.exec("pnpm run build", cwd="/workspace")
if build.exit_code != 0:
raise RuntimeError("Build failed after self-correction attempt.")
print("Build succeeded.")
```
```typescript
// Write the agent-generated file
await sbx.files.write('/workspace/app/page.tsx', pageCode);
// Install dependencies
const install = await sbx.exec('pnpm install', { cwd: '/workspace' });
if (install.exitCode !== 0) {
throw new Error(`pnpm install failed:\n${install.stderr}`);
}
// Build
let build = await sbx.exec('pnpm run build', { cwd: '/workspace' });
if (build.exitCode !== 0) {
// Self-correction turn
const correctionPrompt =
`The build failed with this error:\n${build.stderr}\n\n` +
'Return a corrected app/page.tsx that fixes the error.';
pageCode = await generatePage(correctionPrompt);
await sbx.files.write('/workspace/app/page.tsx', pageCode);
build = await sbx.exec('pnpm run build', { cwd: '/workspace' });
if (build.exitCode !== 0) {
throw new Error('Build failed after self-correction attempt.');
}
}
console.log('Build succeeded.');
```
The self-correction pattern above is a one-turn fix loop. Production agents typically run 3–5 correction turns before surfacing an error to the user.
## Step 5 — Open a preview
Start the Next.js dev server, then expose port 3000 as a public URL your user can open immediately.
```python
# Start dev server (detached — runs in background inside the sandbox)
sbx.exec("pnpm run start --port 3000", cwd="/workspace", background=True)
# Expose port 3000
preview = sbx.previews.create(port=3000)
print(f"Live preview: {preview['url']}")
```
```typescript
// Start the production server (detached)
await sbx.exec('pnpm run start --port 3000', { cwd: '/workspace', background: true });
// Expose port 3000
const preview = await sbx.previews.create({ port: 3000 });
console.log('Live preview:', preview.url);
```
## Step 6 — Publish to production
When the user confirms, publish the sandbox to a permanent production deployment. The deployment is immutable — the sandbox can be destroyed after this.
```python
deployment = client.deployments.publish(
source_sandbox_id=sbx.id,
name="dogwalk-landing",
)
# Wait for the deployment to become live
while deployment.status not in ("live", "failed"):
time.sleep(2)
deployment = client.deployments.get(deployment.id)
if deployment.status == "failed":
raise RuntimeError(f"Deployment failed: {deployment.error}")
print(f"Production URL: {deployment.url}")
# Clean up the sandbox — no longer needed
sbx.delete()
```
```typescript
let deployment = await client.deployments.publish({
sourceSandboxId: sbx.id,
name: 'dogwalk-landing',
});
// Wait for live
while (deployment.status !== 'live' && deployment.status !== 'failed') {
await new Promise(r => setTimeout(r, 2000));
deployment = await client.deployments.get(deployment.id);
}
if (deployment.status === 'failed') {
throw new Error(`Deployment failed: ${deployment.error}`);
}
console.log('Production URL:', deployment.url);
// Clean up
await sbx.delete();
```
## Full script
```python
#!/usr/bin/env python3
"""agent_builds_app.py — full end-to-end recipe"""
from miosa import Miosa
client = Miosa(api_key=os.environ["MIOSA_API_KEY"])
ai = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
def generate_page(prompt: str) -> str:
msg = ai.messages.create(
model="claude-opus-4-5",
max_tokens=4096,
system=(
"You are a Next.js developer. Respond with ONLY the complete "
"content of app/page.tsx. No explanation, no markdown fences."
),
messages=[{"role": "user", "content": prompt}],
)
return msg.content[0].text
def build_and_deploy(user_prompt: str) -> str:
sbx = client.sandboxes.create(template_id="nextjs", timeout_sec=600)
page_code = generate_page(user_prompt)
sbx.files.write("/workspace/app/page.tsx", page_code)
sbx.exec("pnpm install", cwd="/workspace")
build = sbx.exec("pnpm run build", cwd="/workspace")
if build.exit_code != 0:
page_code = generate_page(
f"Fix this Next.js build error and return corrected app/page.tsx:\n{build.stderr}"
)
sbx.files.write("/workspace/app/page.tsx", page_code)
build = sbx.exec("pnpm run build", cwd="/workspace")
if build.exit_code != 0:
sbx.delete()
raise RuntimeError("Build failed after correction.")
sbx.exec("pnpm run start --port 3000", cwd="/workspace", background=True)
preview = sbx.previews.create(port=3000)
print(f"Preview: {preview['url']}")
deployment = client.deployments.publish(
source_sandbox_id=sbx.id,
name="agent-built-app",
)
while deployment.status not in ("live", "failed"):
time.sleep(2)
deployment = client.deployments.get(deployment.id)
sbx.delete()
if deployment.status == "failed":
raise RuntimeError(f"Deployment failed: {deployment.error}")
return deployment.url
if __name__ == "__main__":
url = build_and_deploy(
"Build a landing page for a dog-walking app with a hero, feature list, and CTA."
)
print(f"\nProduction URL: {url}")
```
```typescript
// agent-builds-app.ts — full end-to-end recipe
const client = new Miosa({ apiKey: process.env.MIOSA_API_KEY! });
const ai = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });
async function generatePage(prompt: string): Promise {
const msg = await ai.messages.create({
model: 'claude-opus-4-5',
max_tokens: 4096,
system:
'You are a Next.js developer. Respond with ONLY the complete content ' +
'of app/page.tsx. No explanation, no markdown fences.',
messages: [{ role: 'user', content: prompt }],
});
const block = msg.content[0];
return block.type === 'text' ? block.text : '';
}
async function buildAndDeploy(userPrompt: string): Promise {
const sbx = await client.sandboxes.create({ templateId: 'nextjs', timeoutSec: 600 });
let pageCode = await generatePage(userPrompt);
await sbx.files.write('/workspace/app/page.tsx', pageCode);
await sbx.exec('pnpm install', { cwd: '/workspace' });
let build = await sbx.exec('pnpm run build', { cwd: '/workspace' });
if (build.exitCode !== 0) {
pageCode = await generatePage(
`Fix this Next.js build error and return corrected app/page.tsx:\n${build.stderr}`,
);
await sbx.files.write('/workspace/app/page.tsx', pageCode);
build = await sbx.exec('pnpm run build', { cwd: '/workspace' });
if (build.exitCode !== 0) {
await sbx.delete();
throw new Error('Build failed after correction.');
}
}
await sbx.exec('pnpm run start --port 3000', { cwd: '/workspace', background: true });
const preview = await sbx.previews.create({ port: 3000 });
console.log('Preview:', preview.url);
let deployment = await client.deployments.publish({
sourceSandboxId: sbx.id,
name: 'agent-built-app',
});
while (deployment.status !== 'live' && deployment.status !== 'failed') {
await new Promise(r => setTimeout(r, 2000));
deployment = await client.deployments.get(deployment.id);
}
await sbx.delete();
if (deployment.status === 'failed') {
throw new Error(`Deployment failed: ${deployment.error}`);
}
return deployment.url;
}
const url = await buildAndDeploy(
'Build a landing page for a dog-walking app with a hero, feature list, and CTA.',
);
console.log('\nProduction URL:', url);
```
## What you learned
- Sandboxes boot in ~150 ms from a warm template — fast enough to create one per user request.
- The LLM and the sandbox are completely decoupled. Swap any model without touching the execution logic.
- A single self-correction turn catches most build errors automatically.
- `deployments.publish` produces an immutable release; the sandbox can be destroyed immediately after.
- `background: true` on exec keeps long-running processes alive without blocking your driver script.
## Next
Stream exec stdout to your UI in real time instead of waiting for a result.
Checkpoint before risky steps and fork into parallel branches to compare outcomes.
Full guide on wiring agents to MIOSA, including multi-turn state and approval workflows.
---
# Browser agent
URL: https://miosa.ai/docs/cookbook/browser-agent
Fallback URL: https://miosa.roberto-c49.workers.dev/docs/cookbook/browser-agent
Source: src/routes/docs/cookbook/browser-agent/+page.md
Description: Boot a full Linux desktop Computer, screenshot a website, click a button, fill a form, take another screenshot — the complete screenshot → action → screenshot loop with your own LLM.
# Browser agent
~12 min Python TypeScript
> **Goal**: Boot a MIOSA Computer (full Linux desktop VM), open a browser inside it, and run an LLM-driven action loop that screenshots the screen, decides what to do next, and executes the action — all from your own code.
>
> **What you'll use**: Computers, Desktop API (screenshot / click / type / key)
## What you'll build
A `BrowserAgent` class that:
1. Boots a Computer with a full desktop.
2. Opens a URL in Chromium.
3. Enters a screenshot → LLM → action loop.
4. Stops when the LLM signals completion.
The LLM is your choice — the code below uses Anthropic Claude's vision API but the pattern is model-agnostic.
## Prerequisites
- A MIOSA workspace API key (`msk_live_*`) — see [API Keys](/docs/platform/api-keys/)
- An Anthropic API key (or substitute your preferred vision-capable model)
- Node 22+ or Python 3.11+
## Step 1 — Install and configure
```bash
pip install miosa anthropic
export MIOSA_API_KEY="msk_live_..."
export ANTHROPIC_API_KEY="sk-ant-..."
```
```bash
npm install @miosa/sdk @anthropic-ai/sdk
export MIOSA_API_KEY="msk_live_..."
export ANTHROPIC_API_KEY="sk-ant-..."
```
## Step 2 — Boot a Computer
Computers take ~10 seconds to boot (full desktop VM, not a microVM). Create one, start it, then wait for it to reach `running`.
```python
from miosa import Miosa
client = Miosa(api_key=os.environ["MIOSA_API_KEY"])
print("Booting Computer...")
computer = client.computers.create(template="ubuntu-desktop")
computer.start()
# Wait for running
while computer.status != "running":
if computer.status == "error":
raise RuntimeError(f"Computer failed to boot: {computer.metadata.get('last_error')}")
time.sleep(2)
computer = client.computers.get(computer.id)
print(f"Computer running: {computer.id}")
```
```typescript
const client = new Miosa({ apiKey: process.env.MIOSA_API_KEY! });
console.log('Booting Computer...');
let computer = await client.computers.create({ template: 'ubuntu-desktop' });
await computer.start();
// Wait for running
while (computer.status !== 'running') {
if (computer.status === 'error') {
throw new Error(`Computer failed to boot: ${computer.metadata?.lastError}`);
}
await new Promise(r => setTimeout(r, 2000));
computer = await client.computers.get(computer.id);
}
console.log('Computer running:', computer.id);
```
## Step 3 — Take a screenshot
The desktop starts at the Linux login screen or a default desktop. Take the first screenshot to see what you are working with.
```python
# screenshot() returns a PNG as bytes
png = computer.screenshot()
# Save for inspection (optional)
with open("/tmp/screenshot_0.png", "wb") as f:
f.write(png)
# Encode for the LLM
png_b64 = base64.b64encode(png).decode()
print(f"Screenshot captured ({len(png)} bytes)")
```
```typescript
// screenshot() returns a Buffer (PNG)
const png = await computer.screenshot();
// Save for inspection (optional)
writeFileSync('/tmp/screenshot_0.png', png);
// Encode for the LLM
const pngB64 = png.toString('base64');
console.log(`Screenshot captured (${png.length} bytes)`);
```
## Step 4 — Open a URL in the browser
Use `exec` to launch Chromium with the target URL. The desktop VM has Chromium pre-installed.
```python
# Launch Chromium pointing to the target site
computer.exec(
"chromium-browser --no-sandbox --start-maximized https://example.com",
background=True,
)
# Wait for the browser to load
time.sleep(3)
# Take a screenshot to confirm the page loaded
png = computer.screenshot()
with open("/tmp/screenshot_1.png", "wb") as f:
f.write(png)
png_b64 = base64.b64encode(png).decode()
```
```typescript
// Launch Chromium
await computer.exec(
'chromium-browser --no-sandbox --start-maximized https://example.com',
{ background: true },
);
// Wait for browser to load
await new Promise(r => setTimeout(r, 3000));
// Screenshot the loaded page
const png = await computer.screenshot();
writeFileSync('/tmp/screenshot_1.png', png);
const pngB64 = png.toString('base64');
```
## Step 5 — Build the action loop
Ask the LLM what to do next, then execute the returned action on the computer. Repeat until the model signals `done`.
The system prompt below instructs the model to return a JSON action object — a minimal tool-call format that does not require function calling support.
```python
ai = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
SYSTEM_PROMPT = """\
You control a desktop browser. You receive a screenshot and a task.
Respond with ONLY a JSON object — no explanation, no markdown fences.
Action format:
{"type": "click", "x": , "y": }
{"type": "type", "text": ""}
{"type": "key", "combo": ""} e.g. "ctrl+a", "enter", "tab"
{"type": "scroll", "x": , "y": , "direction": "up"|"down"}
{"type": "screenshot"} take a fresh screenshot without acting
{"type": "done", "result": ""} task complete
Coordinates are 0-1000 on both axes regardless of screen resolution.
"""
def next_action(task: str, png_b64: str, history: list) -> dict:
messages = history + [
{
"role": "user",
"content": [
{
"type": "image",
"source": {"type": "base64", "media_type": "image/png", "data": png_b64},
},
{"type": "text", "text": f"Task: {task}"},
],
}
]
msg = ai.messages.create(
model="claude-opus-4-5",
max_tokens=256,
system=SYSTEM_PROMPT,
messages=messages,
)
text = msg.content[0].text.strip()
return json.loads(text)
```
```typescript
const ai = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });
const SYSTEM_PROMPT = `
You control a desktop browser. You receive a screenshot and a task.
Respond with ONLY a JSON object — no explanation, no markdown fences.
Action format:
{"type": "click", "x": , "y": }
{"type": "type", "text": ""}
{"type": "key", "combo": ""}
{"type": "scroll", "x": , "y": , "direction": "up"|"down"}
{"type": "screenshot"}
{"type": "done", "result": ""}
Coordinates are 0-1000 on both axes regardless of screen resolution.
`.trim();
type AgentHistory = Anthropic.MessageParam[];
async function nextAction(
task: string,
pngB64: string,
history: AgentHistory,
): Promise> {
const messages: AgentHistory = [
...history,
{
role: 'user',
content: [
{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: pngB64 } },
{ type: 'text', text: `Task: ${task}` },
],
},
];
const msg = await ai.messages.create({
model: 'claude-opus-4-5',
max_tokens: 256,
system: SYSTEM_PROMPT,
messages,
});
const block = msg.content[0];
const text = block.type === 'text' ? block.text.trim() : '{}';
return JSON.parse(text);
}
```
## Step 6 — Execute actions on the Computer
Translate the LLM's JSON output into Computer API calls. MIOSA coordinates are 0–1000 on both axes regardless of the actual screen resolution.
```python
def execute_action(computer, action: dict) -> bytes | None:
"""Execute an action and return the next screenshot (or None for done)."""
t = action["type"]
if t == "click":
computer.left_click(action["x"], action["y"])
elif t == "type":
computer.type_text(action["text"])
elif t == "key":
computer.key(action["combo"])
elif t == "scroll":
computer.scroll(action["x"], action["y"], action["direction"])
elif t == "screenshot":
pass # fall through to capture below
elif t == "done":
return None
time.sleep(0.5) # let the UI settle
return computer.screenshot()
```
```typescript
async function executeAction(
computer: Computer,
action: Record,
): Promise {
const { type: t } = action;
if (t === 'click') {
await computer.leftClick(action.x as number, action.y as number);
} else if (t === 'type') {
await computer.typeText(action.text as string);
} else if (t === 'key') {
await computer.key(action.combo as string);
} else if (t === 'scroll') {
await computer.scroll(
action.x as number,
action.y as number,
action.direction as 'up' | 'down',
);
} else if (t === 'screenshot') {
// fall through to screenshot below
} else if (t === 'done') {
return null;
}
await new Promise(r => setTimeout(r, 500)); // let UI settle
return computer.screenshot();
}
```
## Step 7 — Run the full agent loop
Combine the pieces into a loop capped at `max_turns` to prevent runaway execution.
```python
TASK = "Find the 'More information...' link on example.com, click it, then report the URL you land on."
MAX_TURNS = 20
history = []
current_png = png # screenshot from Step 4
current_png_b64 = png_b64
for turn in range(1, MAX_TURNS + 1):
print(f"\n--- Turn {turn} ---")
action = next_action(TASK, current_png_b64, history)
print(f"Action: {action}")
# Append to history so the model sees its own prior decisions
history.append({
"role": "assistant",
"content": [{"type": "text", "text": json.dumps(action)}],
})
if action["type"] == "done":
print(f"\nDone: {action.get('result')}")
break
next_png = execute_action(computer, action)
if next_png is None:
break
current_png = next_png
current_png_b64 = base64.b64encode(current_png).decode()
# Save each turn for debugging
with open(f"/tmp/screenshot_{turn}.png", "wb") as f:
f.write(current_png)
else:
print(f"Reached max turns ({MAX_TURNS}) without completion.")
# Clean up
computer.stop()
```
```typescript
const TASK =
"Find the 'More information...' link on example.com, click it, then report the URL you land on.";
const MAX_TURNS = 20;
const history: AgentHistory = [];
let currentPng = png; // from Step 4
let currentPngB64 = pngB64;
for (let turn = 1; turn <= MAX_TURNS; turn++) {
console.log(`\n--- Turn ${turn} ---`);
const action = await nextAction(TASK, currentPngB64, history);
console.log('Action:', action);
history.push({
role: 'assistant',
content: [{ type: 'text', text: JSON.stringify(action) }],
});
if (action.type === 'done') {
console.log('\nDone:', action.result);
break;
}
const nextPng = await executeAction(computer, action);
if (nextPng === null) break;
currentPng = nextPng;
currentPngB64 = currentPng.toString('base64');
writeFileSync(`/tmp/screenshot_${turn}.png`, currentPng);
if (turn === MAX_TURNS) {
console.log(`Reached max turns (${MAX_TURNS}) without completion.`);
}
}
await computer.stop();
```
## Full script (Python)
```python
#!/usr/bin/env python3
"""browser_agent.py — complete screenshot → action → screenshot loop"""
from miosa import Miosa
client = Miosa(api_key=os.environ["MIOSA_API_KEY"])
ai = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
SYSTEM_PROMPT = """\
You control a desktop browser. You receive a screenshot and a task.
Respond with ONLY a JSON object — no explanation, no markdown fences.
Actions: click(x,y) | type(text) | key(combo) | scroll(x,y,direction) | screenshot | done(result)
Coordinates are 0-1000 on both axes.
"""
def next_action(task, png_b64, history):
messages = history + [{
"role": "user",
"content": [
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": png_b64}},
{"type": "text", "text": f"Task: {task}"},
],
}]
msg = ai.messages.create(model="claude-opus-4-5", max_tokens=256, system=SYSTEM_PROMPT, messages=messages)
return json.loads(msg.content[0].text.strip())
def execute(computer, action):
t = action["type"]
if t == "click": computer.left_click(action["x"], action["y"])
elif t == "type": computer.type_text(action["text"])
elif t == "key": computer.key(action["combo"])
elif t == "scroll": computer.scroll(action["x"], action["y"], action["direction"])
elif t == "done": return None
time.sleep(0.5)
return computer.screenshot()
def run(task: str, url: str, max_turns: int = 20) -> str:
computer = client.computers.create(template="ubuntu-desktop")
computer.start()
while computer.status != "running":
time.sleep(2)
computer = client.computers.get(computer.id)
computer.exec(f"chromium-browser --no-sandbox --start-maximized {url}", background=True)
time.sleep(3)
png = computer.screenshot()
png_b64 = base64.b64encode(png).decode()
history = []
try:
for turn in range(1, max_turns + 1):
action = next_action(task, png_b64, history)
print(f"Turn {turn}: {action}")
history.append({"role": "assistant", "content": [{"type": "text", "text": json.dumps(action)}]})
if action["type"] == "done":
return action.get("result", "")
nxt = execute(computer, action)
if nxt is None:
return ""
png_b64 = base64.b64encode(nxt).decode()
return f"Stopped after {max_turns} turns."
finally:
computer.stop()
if __name__ == "__main__":
result = run(
task="Find the 'More information...' link, click it, report the landing URL.",
url="https://example.com",
)
print(f"\nResult: {result}")
```
Coordinates are always 0–1000 regardless of actual screen resolution. MIOSA's desktop API normalizes all input to this coordinate space. Tell the LLM the same constraint in the system prompt so it does not produce pixel-perfect coordinates from the screenshot dimensions.
## Tips for reliable browser agents
- **Add `time.sleep(0.5)` after every action.** Browsers animate and reflow. Acting on a screenshot taken 0 ms after a click often shows the pre-click state.
- **Save every screenshot.** The debug loop is: screenshot N shows the state the model reasoned about to produce action N. When an agent gets stuck, inspect the saved screenshots.
- **Cap `max_turns` tightly (10–25).** A runaway agent loop can exhaust credits fast. Set the ceiling and surface the incomplete-task status cleanly.
- **Use `computer.key("ctrl+r")` to reload.** If the page looks broken, a reload is often cheaper than restarting the Computer.
- **Snapshot after login.** If the task requires authenticating, snapshot immediately after the login succeeds. Restore from the snapshot for subsequent tasks to skip the login every time.
## What you learned
- `client.computers.create` + `computer.start()` boots a full Linux desktop VM.
- `computer.screenshot()` returns raw PNG bytes at any point.
- `computer.left_click(x, y)`, `computer.type_text(text)`, `computer.key(combo)`, and `computer.scroll(x, y, direction)` are the four action primitives.
- Coordinates are 0–1000 normalized — tell your LLM the same range.
- The agent loop is yours: MIOSA executes actions, your code decides what to do next.
## Next
Full Computer lifecycle, sizing, templates, and the difference between Computers and Sandboxes.
Complete method signatures for screenshot, click, type, key, and scroll.
Multi-turn state management, approval workflows, and best practices for agent loops.
---
# Multi-tenant SaaS
URL: https://miosa.ai/docs/cookbook/multi-tenant-saas
Fallback URL: https://miosa.roberto-c49.workers.dev/docs/cookbook/multi-tenant-saas
Source: src/routes/docs/cookbook/multi-tenant-saas/+page.md
Description: Embed MIOSA into your SaaS — tag sandboxes per tenant, query usage per workspace, and issue browser tokens so customers interact directly with their sandboxes.
# Multi-tenant SaaS
~12 min Python TypeScript
> **Goal**: Embed MIOSA into a multi-tenant product. Every sandbox is tagged with your own workspace and user IDs. Usage rolls up per tenant for billing. End-users interact with their sandboxes directly from the browser without ever seeing your API key.
>
> **What you'll use**: Sandboxes, External Attribution, Browser Tokens, Usage API
## What you'll build
An embedded sandbox experience inside your SaaS:
1. Your backend creates sandboxes tagged to the right customer.
2. Each tenant's usage is queryable for chargeback.
3. Your customer's browser gets a short-lived token to embed a live preview — no `msk_*` key anywhere client-side.
## Prerequisites
- A MIOSA workspace API key (`msk_live_*`) — see [API Keys](/docs/platform/api-keys/)
- Node 22+ or Python 3.11+
## Step 1 — Install and configure
```bash
pip install miosa
export MIOSA_API_KEY="msk_live_..."
```
```bash
npm install @miosa/sdk
export MIOSA_API_KEY="msk_live_..."
```
## Step 2 — Tag every sandbox with your IDs
Pass `external_workspace_id`, `external_user_id`, and `external_project_id` on every `sandboxes.create` call. MIOSA stores these on the resource row and propagates them to every usage record, audit event, and derived resource (previews, deployments) automatically.
```python
from miosa import Miosa
client = Miosa(api_key=os.environ["MIOSA_API_KEY"])
def create_sandbox_for_user(workspace_id: str, user_id: str, project_id: str):
"""Called from your request handler when a tenant starts a new sandbox."""
sbx = client.sandboxes.create(
template_id="miosa-sandbox",
timeout_sec=3600,
external_workspace_id=workspace_id,
external_user_id=user_id,
external_project_id=project_id,
)
return sbx
# Example: workspace "acme-corp", user "jane-doe", project "q3-dashboard"
sbx = create_sandbox_for_user("acme-corp", "jane-doe", "q3-dashboard")
print(f"Created sandbox {sbx.id} for acme-corp / jane-doe")
```
```typescript
const client = new Miosa({ apiKey: process.env.MIOSA_API_KEY! });
async function createSandboxForUser(
workspaceId: string,
userId: string,
projectId: string,
) {
const sbx = await client.sandboxes.create({
templateId: 'miosa-sandbox',
timeoutSec: 3600,
externalWorkspaceId: workspaceId,
externalUserId: userId,
externalProjectId: projectId,
});
return sbx;
}
// Example
const sbx = await createSandboxForUser('acme-corp', 'jane-doe', 'q3-dashboard');
console.log(`Created sandbox ${sbx.id} for acme-corp / jane-doe`);
```
Use whatever ID format your platform already has — UUIDs, slugs, integer strings. MIOSA stores them as text and never validates the format beyond a 255-character length limit.
## Step 3 — List sandboxes per tenant
Filter the sandbox list by your own IDs to show a customer only their resources, or to enumerate all sandboxes for an account before billing.
```python
# All sandboxes for a workspace
acme_sandboxes = client.sandboxes.list(external_workspace_id="acme-corp")
for sbx in acme_sandboxes:
print(f" {sbx.id} user={sbx.external_user_id} status={sbx.status}")
# Narrow to a single user
jane_sandboxes = client.sandboxes.list(
external_workspace_id="acme-corp",
external_user_id="jane-doe",
)
```
```typescript
// All sandboxes for a workspace
const acmeSandboxes = await client.sandboxes.list({
externalWorkspaceId: 'acme-corp',
});
for (const sbx of acmeSandboxes) {
console.log(` ${sbx.id} user=${sbx.externalUserId} status=${sbx.status}`);
}
// Narrow to a single user
const janeSandboxes = await client.sandboxes.list({
externalWorkspaceId: 'acme-corp',
externalUserId: 'jane-doe',
});
```
## Step 4 — Query usage per tenant
Pull a usage report grouped by `external_workspace_id` at the end of each billing period. Use this to generate invoices or display a cost dashboard inside your product.
```python
# Monthly rollup — grouped by workspace
usage = client.usage.report(
period={"start": "2026-05-01", "end": "2026-06-01"},
group_by=["external_workspace_id"],
)
for row in usage:
print(f" {row['external_workspace_id']}: ${row['total_usd']:.4f}")
# Drill into a single workspace by user
acme_by_user = client.usage.report(
period={"start": "2026-05-01", "end": "2026-06-01"},
external_workspace_id="acme-corp",
group_by=["external_user_id"],
)
for row in acme_by_user:
print(f" user={row['external_user_id']}: ${row['total_usd']:.4f}")
```
```typescript
// Monthly rollup — grouped by workspace
const usage = await client.usage.report({
period: { start: '2026-05-01', end: '2026-06-01' },
groupBy: ['external_workspace_id'],
});
for (const row of usage) {
console.log(` ${row.externalWorkspaceId}: $${row.totalUsd.toFixed(4)}`);
}
// Drill into a single workspace by user
const acmeByUser = await client.usage.report({
period: { start: '2026-05-01', end: '2026-06-01' },
externalWorkspaceId: 'acme-corp',
groupBy: ['external_user_id'],
});
for (const row of acmeByUser) {
console.log(` user=${row.externalUserId}: $${row.totalUsd.toFixed(4)}`);
}
```
## Step 5 — Issue browser tokens for direct access
Your customer's browser needs to embed a live preview. You cannot put `msk_*` in the frontend. Instead, your backend mints a short-lived share token and returns it to the client.
**Backend — mint the token:**
```python
from fastapi import FastAPI, Depends, HTTPException
from miosa import Miosa
app = FastAPI()
client = Miosa(api_key=os.environ["MIOSA_API_KEY"])
def get_current_user(request) -> dict:
# Your existing auth logic — returns { workspace_id, user_id }
...
@app.get("/api/sandboxes/{sandbox_id}/preview-token")
async def get_preview_token(sandbox_id: str, user=Depends(get_current_user)):
# Verify the sandbox belongs to this user's workspace
sbx = client.sandboxes.get(sandbox_id)
if sbx.external_workspace_id != user["workspace_id"]:
raise HTTPException(403, "Forbidden")
# Ensure a preview is running
previews = client.sandboxes.previews.list(sandbox_id)
if not previews:
preview = client.sandboxes.previews.create(sandbox_id, port=3000)
else:
preview = previews[0]
# Mint a 1-hour share token
share = client.sandboxes.previews.share(
sandbox_id,
preview.id,
expires_in_sec=3600,
)
return {"share_url": share.share_url, "expires_at": share.data["share_token_expires_at"]}
```
```typescript
const app = express();
const client = new Miosa({ apiKey: process.env.MIOSA_API_KEY! });
// Assumes req.user is populated by your auth middleware
app.get('/api/sandboxes/:sandboxId/preview-token', async (req, res) => {
const { sandboxId } = req.params;
const { workspaceId } = (req as any).user;
// Ownership check
const sbx = await client.sandboxes.get(sandboxId);
if (sbx.externalWorkspaceId !== workspaceId) {
res.status(403).json({ error: 'Forbidden' });
return;
}
// Ensure a preview exists
const previews = await client.sandboxes.previews.list(sandboxId);
const preview =
previews.length > 0
? previews[0]
: await client.sandboxes.previews.create(sandboxId, { port: 3000 });
// Mint a 1-hour share token
const share = await client.sandboxes.previews.share(sandboxId, preview.id, {
expiresInSec: 3600,
});
res.json({ shareUrl: share.shareUrl, expiresAt: share.data.shareTokenExpiresAt });
});
```
**Frontend — embed the preview:**
```html
```
Never put `msk_*` in a `