On this page

The official Python SDK for MIOSA. Supports Python 3.9+ and provides both synchronous (Miosa) and asynchronous (AsyncMiosa) clients.

Install

What’s new in 1.1.0

AreaWhat shipped
Sandbox lifecyclecreate_sandbox with slug, external_user_id, external_project_id; sandbox.update(); sandbox.preview_token()
Tenant preview domainclient.tenant.preview_domain.get/set/verify/delete()
Tenant brandingclient.tenant.branding.get/set/delete()
Webhooksclient.webhooks.create/list/get/delete/test()
Files advancedsandbox.files.tree(), sandbox.files.write_files(), sandbox.files.watch_dir()
Sandbox envsandbox.env.list/set/delete()
Processessandbox.processes.start/list/stop/logs()
Templates + forkclient.sandbox_templates.*, client.sandboxes.fork()
Usage / quotasclient.usage.get(), client.quotas.get/set/delete()
Audit logclient.audit_log.list()
Tenant events (SSE)client.events.stream(tenant_wide=True)
Sharingsandbox.share.create/list/revoke()

First request

from miosa import Miosa

client = Miosa(api_key="msk_live_...")

# Create a sandbox, run a command, destroy it
sandbox = client.sandboxes.create(image="miosa-sandbox")
result = sandbox.run("python3 -c 'print(1 + 1)'")
print(result.stdout)   # "2\n"
sandbox.destroy()

Configure

OptionEnv varDefaultDescription
api_keyMIOSA_API_KEY-Workspace key (msk_live_*)
base_urlMIOSA_BASE_URLhttps://api.miosa.ai/api/v1API endpoint
timeout-30.0Per-request timeout in seconds
max_retries-3Automatic retries on 429 / 5xx
from miosa import Miosa

# Reads MIOSA_API_KEY from environment
client = Miosa()

# Explicit configuration
client = Miosa(
    api_key="msk_live_...",
    base_url="https://api.miosa.ai/api/v1",
    timeout=60.0,
    max_retries=3,
)

Both clients support the context manager protocol:

with Miosa() as client:
    computers = client.computers.list()

Authentication

The API key is sent as Authorization: Bearer <key> on every request. Keys must start with msk_.

from miosa import Miosa
import os

# From environment variable (recommended for production)
client = Miosa(api_key=os.environ["MIOSA_API_KEY"])

# Custom default headers (advanced  -  for proxy setups)
client = Miosa(
    api_key="msk_live_...",
    default_headers={"X-Tenant-ID": "acme-corp"},
)

Async vs sync

Both clients expose an identical surface; AsyncMiosa methods are coroutines:

# Sync
from miosa import Miosa
client = Miosa()
sandbox = client.sandboxes.create(image="miosa-sandbox")

# Async
import asyncio
from miosa import AsyncMiosa

async def main():
    async with AsyncMiosa(api_key="msk_live_...") as client:
        sandbox = await client.sandboxes.create(image="miosa-sandbox")
        result = await sandbox.run("echo hi")
        print(result.stdout)

asyncio.run(main())

Error handling

from miosa import (
    Miosa,
    MiosaError,
    AuthenticationError,
    NotFoundError,
    PermissionError,
    RateLimitError,
    ValidationError,
    InsufficientCreditsError,
    ServerError,
)

client = Miosa()

try:
    computer = client.computers.get("nonexistent-id")
except NotFoundError:
    print("Computer not found")
except AuthenticationError:
    print("Invalid API key")
except PermissionError:
    print("Access denied")
except RateLimitError as e:
    print(f"Rate limited  -  retry after {e.retry_after}s")
except ValidationError as e:
    print(f"Validation failed: {e.message}")
except InsufficientCreditsError:
    print("Top up credits at miosa.ai/billing")
except ServerError as e:
    print(f"Server error {e.status_code}")
except MiosaError as e:
    print(f"API error {e.status_code}: {e.message}")

Streaming events

sandbox.events.stream() opens an SSE connection and yields SandboxEvent objects. Use sandbox.events.astream() in async code:

# Sync
for event in sandbox.events.stream():
    print(event.type, event.data)
    if event.type in ("build.completed", "build.failed", "build.timed_out"):
        break

# Async (AsyncMiosa  -  AsyncSandboxEvents.stream() is an async generator)
async for event in sandbox.events.stream():
    print(event.get("type"), event.get("data"))
    if event.get("type") in ("build.completed", "build.failed", "build.timed_out"):
        break

Phase 1-5 API Reference

Sandbox lifecycle (Phase 1)

client.sandboxes - sandbox handle returned by create()

# Create  -  full signature
sandbox = client.sandboxes.create(
    image="miosa-sandbox",          # template image
    name="my-sandbox",              # optional display name
    slug="acme-backend",            # optional URL-safe identifier
    external_user_id="usr_123",     # white-label user attribution
    external_project_id="proj_456", # white-label project attribution
    external_workspace_id="ws_789", # white-label workspace attribution
    metadata={"env": "staging"},    # arbitrary key-value pairs
    timeout_sec=300,                # sandbox lifetime in seconds
    always_on=False,                # keep alive past idle timeout
    vcpus=1,
    memory_mb=512,
    idempotency_key="uuid-v4",      # safe for retries
)

sandbox = client.sandboxes.get(sandbox_id)
sandboxes = client.sandboxes.list(state="running")
client.sandboxes.destroy(sandbox_id)

# Update mutable fields on an existing sandbox
sandbox.update({
    "name": "updated-name",
    "slug": "new-slug",
    "timeout_sec": 600,
    "always_on": True,
    "metadata": {"version": "2"},
})

# Mint a short-lived preview token
token_info = sandbox.preview_token(expires_in=3600)
# Returns: {"token": "mp_...", "url": "https://...", "expires_at": "..."}

# Fork from an existing snapshot or running sandbox
new_sandbox = client.sandboxes.fork(
    sandbox_id,
    snapshot_id="snap_abc",   # optional
    name="forked-sandbox",
)
ParameterTypeDescription
namestrDisplay name
slugstrURL-safe identifier
external_user_idstrWhite-label user attribution
external_project_idstrWhite-label project attribution
external_workspace_idstrWhite-label workspace attribution
metadatadictArbitrary key-value pairs
timeout_secintLifetime in seconds
always_onboolStay alive past idle timeout
idempotency_keystrDeduplication key for retries

Tenant preview domain (Phase 1)

client.tenant.preview_domain

# Get current custom preview domain
info = client.tenant.preview_domain.get()
# Returns: {"domain": "preview.acme.com", "verified_at": "...", "cname_target": "..."}

# Set custom preview domain
client.tenant.preview_domain.set("preview.acme.com")

# Trigger DNS verification
result = client.tenant.preview_domain.verify()
# Returns: {"verified": true, "target": "...", "records": [...]}

# Remove custom domain
client.tenant.preview_domain.delete()

Tenant branding (Phase 1)

client.tenant.branding

# Get current branding
branding = client.tenant.branding.get()

# Set branding (all keys optional)
client.tenant.branding.set({
    "product_name": "Acme AI",
    "logo_url": "https://cdn.acme.com/logo.png",
    "support_url": "https://acme.com/support",
    "support_email": "help@acme.com",
    "primary_color": "#ff6600",
    "background_color": "#ffffff",
})

# Reset to platform defaults
client.tenant.branding.delete()
KeyDescription
product_nameWhite-label product name shown in UI
logo_urlURL to your logo image
support_urlSupport page URL
support_emailSupport email address
primary_colorHex color for primary UI accents
background_colorHex color for desktop background

Webhooks (Phase 1)

client.webhooks

# Create a webhook
wh = client.webhooks.create(
    url="https://example.com/webhooks/miosa",
    events=["sandbox.created", "sandbox.ready", "sandbox.destroyed"],
)

# List, get, delete
whs = client.webhooks.list()
wh  = client.webhooks.get(wh_id)
client.webhooks.delete(wh_id)

# Send a test event to verify the endpoint is reachable
client.webhooks.test(wh_id)

Supported events: sandbox.created, sandbox.ready, sandbox.destroyed, sandbox.error, computer.started, computer.stopped, computer.error

Files advanced (Phase 2)

sandbox.files

# Recursive directory tree
tree = sandbox.files.tree("/workspace", recursive=True)

# Batch file write
sandbox.files.write_files([
    FileWriteItem(path="/workspace/app.py", data="print('hello')"),
    FileWriteItem(path="/workspace/config.json", data=b'{"debug": true}'),
])

# Watch for file-system changes (SSE iterator)
for event in sandbox.files.watch_dir("/workspace"):
    print(event.event, event.path)  # "modified", "/workspace/app.py"
    # event.event: "created" | "modified" | "deleted" | "renamed"
    # event.old_path: set when event == "renamed"

Sandbox environment variables (Phase 2)

sandbox.env

# List all env vars
vars = sandbox.env.list()

# Set a variable
sandbox.env.set("DATABASE_URL", "postgres://...")

# Delete a variable
sandbox.env.delete("DATABASE_URL")

Processes (Phase 2)

sandbox.processes

# Start a long-running background process
proc = sandbox.processes.start("npm run dev", env={"NODE_ENV": "development"})
# Returns ProcessInfo with pid, status, started_at

# List all running processes
procs = sandbox.processes.list()

# Stop a process (SIGTERM then SIGKILL)
sandbox.processes.stop(proc.pid)

# Get tail of process output
logs = sandbox.processes.logs(proc.pid, tail=100)

Usage and quotas (Phase 3)

client.usage - client.quotas

# Usage rollup  -  filter by user or group by dimension
report = client.usage.get(
    external_user_id="usr_123",
    group_by="external_user_id",   # or "external_project_id", "workspace_id"
    period="30d",                  # or pass start=/end= ISO timestamps
)
# Returns: {"period_start": "...", "period_end": "...", "results": [...]}

# Current period summary
summary = client.usage.current()

# Set per-user quotas
client.quotas.set(
    "usr_123",
    max_sandboxes=5,
    max_concurrent=2,
    max_storage_gb=10,
    max_credit_cents=10000,  # $100.00
)

# Get current quotas + live usage for a user
quota = client.quotas.get("usr_123")

# Revert to tenant default
client.quotas.delete("usr_123")

Audit log (Phase 3)

client.audit_log

page = 1
while True:
    result = client.audit_log.list(page=page, per_page=50)
    for entry in result.items:
        print(entry.action, entry.created_at)
    if not result.has_next:
        break
    page += 1

Tenant-wide event stream (Phase 3)

# SSE stream of all events across the tenant (requires admin scope)
for event in client.events.stream(tenant_wide=True):
    print(event.type, event.data)

Sharing (Phase 4)

sandbox.share

# Create a public share URL (no API key required to access)
share = sandbox.share.create(expires_in=3600)
# Returns: {"share_id": "ms_...", "share_url": "https://...", "expires_at": "...", "scope": "read"}

# List active shares
shares = sandbox.share.list()

# Revoke a share
sandbox.share.revoke(share["share_id"])

Resource Reference

Full method signatures across all 19 resource groups (185 methods total). Every method on AsyncMiosa is a coroutine with the same signature - prefix calls with await.

Sandboxes

client.sandboxes - client.sandbox_templates

# Sandbox lifecycle
sandbox = client.sandboxes.create(
    image="miosa-sandbox",
    name="my-sandbox",           # optional display name
    vcpus=1,
    memory_mb=512,
    timeout_seconds=300,
    idempotency_key="uuid-v4",   # optional
)
sandbox = client.sandboxes.get(sandbox_id)
sandboxes = client.sandboxes.list(page=1, per_page=20)
sandboxes = client.sandboxes.list(status="running")   # filter: status, name
sandboxes = client.sandboxes.list(name="my-dev")
client.sandboxes.destroy(sandbox_id)

# Execution
result = client.sandboxes.exec(sandbox_id, cmd=["bash", "-c", "echo hi"])
result = sandbox.run("npm install", opts={"cwd": "/app", "timeout_sec": 120})
result = sandbox.exec(["python3", "main.py"])

# Output fields: result.stdout, result.stderr, result.exit_code

# Files (on sandbox handle)
sandbox.files.write("/app/main.py", content="print('hello')")
sandbox.files.write("/app/data.bin", content=b"")
content = sandbox.files.read("/app/main.py")          # → str
data    = sandbox.files.read("/app/data.bin")          # → bytes
entries = sandbox.files.list("/app")                   # → list[FileEntry]
sandbox.files.delete("/app/old.txt")
sandbox.files.mkdir("/app/subdir")
sandbox.files.move("/app/old.txt", "/app/new.txt")

# Events (SSE)
for event in sandbox.events.stream():
    print(event.type, event.data)
    if event.type in ("build.completed", "build.failed", "build.timed_out"):
        break

# Templates
templates = client.sandbox_templates.list()
template  = client.sandbox_templates.get(template_id)
template  = client.sandbox_templates.create(
    name="my-template",
    image="miosa-sandbox",
    description="optional",
)
client.sandbox_templates.delete(template_id)

Computers

client.computers

# CRUD
computer = client.computers.create(
    name="runner-1",
    template_type="miosa-desktop",
    size="small",                       # "xs" | "small" | "medium" | "large" | "xl"  -  default: "small"
    selected_apps=["vscode", "chrome"],
    workspace_id="ws-uuid",             # assign to workspace (optional)
    external_workspace_id="org_abc",    # your own workspace ref (optional)
    external_project_id="proj_xyz",     # your own project ref (optional)
    idempotency_key="uuid-v4",
)
computer = client.computers.get(computer_id)
result   = client.computers.list(workspace_id="ws-uuid")  # also accepts: status="running", name="dev"
client.computers.update(computer_id, agent_session_id="session-uuid")
client.computers.delete(computer_id)

# Lifecycle
client.computers.start(computer_id)
client.computers.stop(computer_id)
client.computers.restart(computer_id)
clone = client.computers.clone(computer_id)
client.computers.resize(computer_id, size="medium")
client.computers.move(computer_id, region="us-east-ny")

# Access tokens
vnc  = client.computers.vnc_credentials(computer_id)
tok  = client.computers.stream_token(computer_id)
urls = client.computers.urls(computer_id)
apps = client.computers.list_apps(computer_id)

# Auto-stop
cfg = client.computers.get_auto_stop(computer_id)
client.computers.update_auto_stop(computer_id, auto_stop_seconds=3600)

# Metrics
metrics = client.computers.metrics(computer_id, window="1h")

# Env vars
env_vars = client.computers.env.list(computer_id)
client.computers.env.create(computer_id, name="DATABASE_URL", value="postgres://...")
client.computers.env.update(computer_id, name="DATABASE_URL", value="postgres://new")
client.computers.env.delete(computer_id, name="DATABASE_URL")

# Port exposure
ports = client.computers.ports.list(computer_id)
client.computers.ports.create(computer_id, port=3000, visibility="public")
client.computers.ports.update(computer_id, port=3000, visibility="private")
client.computers.ports.delete(computer_id, port=3000)

Desktop

client.desktop - desktop control for a running computer. All coordinates are pixels (0,0 = top-left).

# Screenshots
png_bytes = client.desktop.screenshot(computer_id)
png_bytes = client.desktop.screenshot_region(computer_id, x=0, y=0, width=800, height=600)

# Mouse
client.desktop.click(computer_id, x=500, y=300, button="left")  # button: "left"|"right"|"middle"
client.desktop.double_click(computer_id, x=500, y=300)
client.desktop.move(computer_id, x=200, y=400)
client.desktop.drag(computer_id, start_x=100, start_y=200, end_x=400, end_y=500)
client.desktop.scroll(computer_id, x=500, y=300, direction="down", amount=3)

# Keyboard
client.desktop.type(computer_id, text="Hello, World!")
client.desktop.key(computer_id, key="ctrl+c")
client.desktop.hotkey(computer_id, keys=["ctrl", "shift", "t"])

# Clipboard
text = client.desktop.get_clipboard(computer_id)
client.desktop.set_clipboard(computer_id, text="Hello, World!")

# Wallpaper (white-label branding)
client.desktop.set_wallpaper(computer_id, url="https://cdn.example.com/bg.png")

# Accessibility tree (AT-SPI)
tree = client.desktop.accessibility_tree(computer_id)

Workspaces

client.workspaces

workspaces = client.workspaces.list()
workspace  = client.workspaces.create(name="staging", description="Staging environment")
workspace  = client.workspaces.get(workspace_id)
workspace  = client.workspaces.update(workspace_id, name="staging-us-east")
client.workspaces.delete(workspace_id)

# Observability
stats = client.workspaces.stats(workspace_id)
usage = client.workspaces.usage(workspace_id, window="7d", granularity="day")

Tenant / Settings / Regions

client.tenant - client.settings - client.regions

# Tenant plan + counters
plan = client.tenant.current()

# Settings
settings = client.settings.get()
client.settings.update(default_computer_size="medium", auto_stop_default=3600)

# Regions
regions  = client.regions.list()
region   = client.regions.get(region_slug)

Dashboard / Analytics / Audit Log / Usage

client.dashboard - client.analytics - client.audit_log - client.usage

# Dashboard summary
summary = client.dashboard.get()

# Analytics
stats   = client.analytics.computers(window="7d")
stats   = client.analytics.usage(window="30d")

# Audit log (paginated)
page = 1
while True:
    result = client.audit_log.list(page=page, per_page=50)
    for entry in result.items:
        print(entry.action, entry.created_at)
    if not result.has_next:
        break
    page += 1

# Credit & compute usage
usage = client.usage.credits(window="30d")
usage = client.usage.compute(window="30d")

Webhooks

client.webhooks

wh   = client.webhooks.create(url="https://example.com/hook", events=["sandbox.created", "sandbox.ready"])
wh   = client.webhooks.get(wh_id)
whs  = client.webhooks.list()
client.webhooks.update(wh_id, events=["sandbox.created", "sandbox.destroyed"])
client.webhooks.delete(wh_id)
client.webhooks.test(wh_id)

API Keys

client.api_keys

key  = client.api_keys.create(name="ci-runner", scopes=["computers:read", "computers:write"])
key  = client.api_keys.get(key_id)
keys = client.api_keys.list()
client.api_keys.revoke(key_id)

Models / Completions / Embeddings

client.models - client.completions - client.embeddings

# Completions (hosted LLM)
resp = client.completions.create(
    model="qwen3:32b",
    messages=[{"role": "user", "content": "Hello"}],
    max_tokens=512,
    stream=False,
)

# Streaming completions
for chunk in client.completions.stream(model="qwen3:32b", messages=[...]):
    print(chunk.delta, end="", flush=True)

# Embeddings
result  = client.embeddings.create(model="nomic-embed-text", input="The quick brown fox")
vectors = result.embeddings  # list[list[float]]

White-label integrator flow

This is the marquee pattern for platforms building on MIOSA: create sandboxes on behalf of end users, scope by project, and expose previews without sharing API keys.

import os
from miosa import Miosa

client = Miosa(api_key=os.environ["MIOSA_API_KEY"])

# 1. Configure tenant branding once
client.tenant.branding.set({
    "product_name": "Acme AI",
    "logo_url": "https://cdn.acme.com/logo.png",
    "primary_color": "#ff6600",
})

# 2. Optionally set a custom preview domain so previews appear on your domain
client.tenant.preview_domain.set("preview.acme.com")
client.tenant.preview_domain.verify()

# 3. Register a webhook to track lifecycle events
client.webhooks.create(
    url="https://api.acme.com/webhooks/miosa",
    events=["sandbox.created", "sandbox.ready", "sandbox.destroyed"],
)

# 4. Set per-user quotas before provisioning
client.quotas.set(
    "usr_dr_smith",
    max_sandboxes=3,
    max_concurrent=1,
)

# 5. Create a sandbox attributed to a specific user + project
sandbox = client.sandboxes.create(
    image="miosa-sandbox",
    name="smile-dental",
    external_user_id="usr_dr_smith",
    external_project_id="proj_landing_page",
    external_workspace_id="ws_smile_dental",
    metadata={"plan": "pro"},
)

# 6. Write files and run a build
sandbox.files.write_files([
    FileWriteItem(path="/workspace/index.html", data="<h1>Hello</h1>"),
])
result = sandbox.run("echo build done")

# 7. Mint a preview token for the end user (no API key needed on their end)
token = sandbox.preview_token(expires_in=3600)
preview_url = token["url"]   # share this with the end user

# 8. Create a public share link
share = sandbox.share.create(expires_in=86400)
public_url = share["share_url"]

# 9. Query usage per project
report = client.usage.get(
    external_user_id="usr_dr_smith",
    period="30d",
)

# 10. Clean up
sandbox.destroy()

Common patterns

Workspace-scoped creation

from miosa import Miosa

client = Miosa()

# Create a workspace for a customer
ws = client.workspaces.create(name="acme-staging")

# All computers here are isolated inside acme-staging
computer = client.computers.create(
    name="dev-box",
    template_type="miosa-desktop",
    size="small",
    workspace_id=ws.id,
    external_workspace_id="org_acme",
    external_project_id="proj_api",
)

Idempotency key

import uuid

sandbox = client.sandboxes.create(
    image="miosa-sandbox",
    idempotency_key=str(uuid.uuid4()),
)

Pagination

page = 1
while True:
    result = client.audit_log.list(page=page, per_page=50)
    for entry in result.items:
        print(entry.action, entry.created_at)
    if not result.has_next:
        break
    page += 1

Webhook verification

import hmac, hashlib

def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

Where to next

Was this helpful?