The official Elixir SDK for MIOSA. Requires Elixir 1.15+ and OTP 25+. All functions return {:ok, result} or {:error, %Miosa.Error{}}.
Install
# mix.exs
{:miosa, "~> 1.1"} Then run mix deps.get.
What’s new in 1.1.0
| Area | What shipped |
|---|---|
| Sandbox lifecycle | Miosa.Sandboxes.update/3, Miosa.Sandboxes.preview_token/3, Miosa.Sandboxes.fork/3 |
| Tenant preview domain | Miosa.Tenant.get_preview_domain/1, set_preview_domain/2, verify_preview_domain/1, delete_preview_domain/1 |
| Tenant branding | Miosa.Tenant.get_branding/1, set_branding/2, delete_branding/1 |
| Webhooks | Miosa.Webhooks.create/2, list/1, get/2, delete/2, test/2 |
| Files advanced | Miosa.Sandbox.Files.tree/3, write_many/3, watch/2 |
| Sandbox env | Miosa.Sandbox.Env.list/2, set/4, delete/3 |
| Processes | Miosa.Sandbox.Processes.start/3, list/2, stop/3, logs/3 |
| Fork + templates | Miosa.Sandboxes.fork/3, Miosa.SandboxTemplates.* |
| Usage / quotas | Miosa.Usage.get/2, Miosa.Quotas.set/3, get/2, delete/2 |
| Audit log | Miosa.AuditLog.list/2 with cursor pagination |
| Sharing | Miosa.Sandbox.Share.create/3, list/2, revoke/3 |
| Workspace members | Miosa.WorkspaceMembers.*, Miosa.WorkspaceInvites.* |
| Org invites | Miosa.OrgInvites.* |
First request
client = Miosa.Client.new("msk_live_...")
# Create a sandbox, run a command, destroy it
{:ok, sandbox} = Miosa.Sandboxes.create(client, %{name: "quick-exec"})
:ok = Miosa.Computer.start(client, sandbox.id)
{:ok, %{output: output}} =
Miosa.Exec.run(client, sandbox.id, command: "python3 -c 'print(1 + 1)'")
IO.puts(output) # "2\n"
:ok = Miosa.Computer.destroy(client, sandbox.id) Configure
| Option | Env var | Default | Description |
|---|---|---|---|
api_key (positional) | - | - | Workspace key (msk_live_*). Must start with msk_. |
:base_url | MIOSA_BASE_URL | https://api.miosa.ai/api/v1 | API endpoint |
:timeout | - | 30_000 | Connect timeout in milliseconds |
:receive_timeout | - | 60_000 | Receive timeout for long-running requests |
:retry | - | false | Pass to Req retry middleware |
client = Miosa.Client.new("msk_live_...",
base_url: "https://api.miosa.ai/api/v1",
timeout: 30_000,
receive_timeout: 60_000,
retry: false
) The client struct is a plain value - pass it as the first argument to every resource function. There is no global state or process registration.
Authentication
# From environment variable (recommended for production)
client = Miosa.Client.new(System.fetch_env!("MIOSA_API_KEY"))
# With additional headers (e.g., for white-label proxies)
client = Miosa.Client.new("msk_live_...",
headers: [{"x-tenant-id", "acme-corp"}]
) Error handling
All functions follow the {:ok, result} | {:error, %Miosa.Error{}} contract:
case Miosa.Computers.get(client, "nonexistent-id") do
{:ok, computer} ->
IO.puts("Found: #{computer.name}")
{:error, %Miosa.Error{status: 401}} ->
IO.puts("Invalid API key")
{:error, %Miosa.Error{status: 403}} ->
IO.puts("Access denied")
{:error, %Miosa.Error{status: 404}} ->
IO.puts("Computer not found")
{:error, %Miosa.Error{status: 429, message: msg}} ->
IO.puts("Rate limited: #{msg}")
{:error, %Miosa.Error{status: 402}} ->
IO.puts("Insufficient credits - top up at miosa.ai/billing")
{:error, %Miosa.Error{status: status, message: msg}} ->
IO.puts("API error #{status}: #{msg}")
end The %Miosa.Error{} struct fields are: :message, :status, :code, :body.
Streaming events
Miosa.Sandbox.Events.stream/3 opens an SSE connection and calls your callback for each event. Return :stop to close the stream, :continue to keep reading:
Miosa.Sandbox.Events.stream(client, sandbox_id, fn event ->
IO.puts("[#{event.type}] #{inspect(event.data)}")
if event.type in ~w(build.completed build.failed build.timed_out) do
:stop
else
:continue
end
end) Phase 1-5 API Reference
Sandbox lifecycle (Phase 1)
Miosa.Sandboxes
# Create - full options
{:ok, sandbox} = Miosa.Sandboxes.create(client, %{
name: "my-sandbox",
slug: "acme-backend",
external_user_id: "usr_123",
external_project_id: "proj_456",
external_workspace_id: "ws_789",
metadata: %{"env" => "staging"},
timeout_sec: 300,
always_on: false,
idempotency_key: UUID.uuid4()
})
{:ok, sandbox} = Miosa.Sandboxes.get(client, sandbox_id)
{:ok, list} = Miosa.Sandboxes.list(client)
:ok = Miosa.Sandboxes.delete(client, sandbox_id)
# Update mutable fields
{:ok, updated} = Miosa.Sandboxes.update(client, sandbox_id, %{
name: "updated-name",
slug: "new-slug",
timeout_sec: 600,
always_on: true,
metadata: %{"version" => "2"}
})
# Mint a short-lived preview token
{:ok, token} = Miosa.Sandboxes.preview_token(client, sandbox_id,
expires_in: 3600,
scope: "read"
)
# token["token"], token["url"], token["expires_at"]
# Fork from a snapshot or running sandbox
{:ok, new_sb} = Miosa.Sandboxes.fork(client, sandbox_id,
snapshot_id: "snap_abc",
name: "forked-sandbox",
external_user_id: "usr_123"
)
# Wait until ready (streams SSE, falls back to polling)
{:ok, true} = Miosa.Sandboxes.wait_until_ready(client, sandbox_id, timeout: 30) Tenant preview domain (Phase 1)
Miosa.Tenant - flat functions, no sub-struct
{:ok, info} = Miosa.Tenant.get_preview_domain(client)
# %{"domain" => "preview.acme.com", "verified_at" => "...", "cname_target" => "..."}
{:ok, _} = Miosa.Tenant.set_preview_domain(client, "preview.acme.com")
{:ok, result} = Miosa.Tenant.verify_preview_domain(client)
# %{"verified" => true, "target" => "...", "records" => [...]}
{:ok, _} = Miosa.Tenant.delete_preview_domain(client) Tenant branding (Phase 1)
Miosa.Tenant
{:ok, branding} = Miosa.Tenant.get_branding(client)
{:ok, _} = Miosa.Tenant.set_branding(client, %{
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"
})
{:ok, _} = Miosa.Tenant.delete_branding(client) | Key | Description |
|---|---|
product_name | White-label product name shown in UI |
logo_url | URL to your logo image |
support_url | Support page URL |
support_email | Support email address |
primary_color | Hex color for primary UI accents |
background_color | Hex color for desktop background |
Webhooks (Phase 1)
Miosa.Webhooks
{:ok, wh} = Miosa.Webhooks.create(client, %{
url: "https://example.com/webhooks/miosa",
events: ["sandbox.created", "sandbox.ready", "sandbox.destroyed"]
})
{:ok, list} = Miosa.Webhooks.list(client)
{:ok, wh} = Miosa.Webhooks.get(client, wh_id)
{:ok, _} = Miosa.Webhooks.delete(client, wh_id)
{:ok, _} = Miosa.Webhooks.test(client, wh_id) Supported events: sandbox.created, sandbox.ready, sandbox.destroyed, sandbox.error, computer.started, computer.stopped, computer.error
Files advanced (Phase 2)
Miosa.Sandbox.Files
# Recursive directory tree
{:ok, tree} = Miosa.Sandbox.Files.tree(client, sandbox_id, path: "/workspace", depth: 5)
# Batch file write
{:ok, result} = Miosa.Sandbox.Files.write_many(client, sandbox_id, [
%{path: "/workspace/app.ex", content: ~s(IO.puts("hello"))},
%{path: "/workspace/config.json", content: "{}"}
])
# Watch for file-system changes (SSE with callback)
Miosa.Sandbox.Files.watch(client, sandbox_id, fn event ->
IO.puts("#{event["event"]} #{event["path"]}")
:continue
end) Sandbox environment variables (Phase 2)
Miosa.Sandbox.Env
{:ok, vars} = Miosa.Sandbox.Env.list(client, sandbox_id)
{:ok, _} = Miosa.Sandbox.Env.set(client, sandbox_id, "DATABASE_URL", "postgres://...")
{:ok, _} = Miosa.Sandbox.Env.delete(client, sandbox_id, "DATABASE_URL") Processes (Phase 2)
Miosa.Sandbox.Processes
# Start a long-running background process
{:ok, proc} = Miosa.Sandbox.Processes.start(client, sandbox_id, %{
command: "npm run dev",
env: %{"NODE_ENV" => "development"},
name: "dev-server"
})
{:ok, list} = Miosa.Sandbox.Processes.list(client, sandbox_id)
{:ok, _} = Miosa.Sandbox.Processes.stop(client, sandbox_id, proc["pid"])
{:ok, logs} = Miosa.Sandbox.Processes.logs(client, sandbox_id, proc["pid"], tail: 100) Usage and quotas (Phase 3)
Miosa.Usage - Miosa.Quotas
# Usage rollup
{:ok, report} = Miosa.Usage.get(client, %{
external_user_id: "usr_123",
group_by: "external_user_id",
period: "30d"
})
# Set per-user quotas
{:ok, quota} = Miosa.Quotas.set(client, "usr_123", %{
max_sandboxes: 5,
max_concurrent: 2,
max_storage_gb: 10,
max_credit_cents: 10_000
})
{:ok, quota} = Miosa.Quotas.get(client, "usr_123")
{:ok, _} = Miosa.Quotas.delete(client, "usr_123") Audit log (Phase 3)
Miosa.AuditLog
defp fetch_all_audit_log(client, after_cursor \ nil, acc \ []) do
params = %{limit: 50} |> then(fn p ->
if after_cursor, do: Map.put(p, :after, after_cursor), else: p
end)
case Miosa.AuditLog.list(client, params) do
{:ok, %{items: items, next_cursor: nil}} ->
{:ok, acc ++ items}
{:ok, %{items: items, next_cursor: cursor}} ->
fetch_all_audit_log(client, cursor, acc ++ items)
{:error, _} = err ->
err
end
end Sharing (Phase 4)
Miosa.Sandbox.Share
# Create a public share URL (no API key required to access)
{:ok, share} = Miosa.Sandbox.Share.create(client, sandbox_id, expires_in: 3600)
# share["share_id"], share["share_url"], share["expires_at"]
{:ok, shares} = Miosa.Sandbox.Share.list(client, sandbox_id)
{:ok, _} = Miosa.Sandbox.Share.revoke(client, sandbox_id, share_id) White-label integrator flow
client = Miosa.Client.new(System.fetch_env!("MIOSA_API_KEY"))
# 1. Configure tenant branding once
{:ok, _} = Miosa.Tenant.set_branding(client, %{
product_name: "Acme AI",
logo_url: "https://cdn.acme.com/logo.png",
primary_color: "#ff6600"
})
# 2. Set and verify a custom preview domain
{:ok, _} = Miosa.Tenant.set_preview_domain(client, "preview.acme.com")
{:ok, result} = Miosa.Tenant.verify_preview_domain(client)
IO.inspect(result["verified"])
# 3. Register webhook
{:ok, wh} = Miosa.Webhooks.create(client, %{
url: "https://api.acme.com/webhooks/miosa",
events: ["sandbox.created", "sandbox.ready", "sandbox.destroyed"]
})
# 4. Cap per-user resources
{:ok, _} = Miosa.Quotas.set(client, "usr_dr_smith", %{
max_sandboxes: 3,
max_concurrent: 1
})
# 5. Create a sandbox attributed to a user + project
{:ok, sandbox} = Miosa.Sandboxes.create(client, %{
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. Wait until ready, then write files
{:ok, true} = Miosa.Sandboxes.wait_until_ready(client, sandbox.id, timeout: 30)
{:ok, _} = Miosa.Sandbox.Files.write_many(client, sandbox.id, [
%{path: "/workspace/index.html", content: "<h1>Hello</h1>"}
])
# 7. Mint a preview token for the end user
{:ok, token} = Miosa.Sandboxes.preview_token(client, sandbox.id, expires_in: 3600)
preview_url = token["url"]
# 8. Create a public share link
{:ok, share} = Miosa.Sandbox.Share.create(client, sandbox.id, expires_in: 86_400)
public_url = share["share_url"]
# 9. Query usage
{:ok, report} = Miosa.Usage.get(client, %{
external_user_id: "usr_dr_smith",
period: "30d"
})
# 10. Clean up
:ok = Miosa.Computer.destroy(client, sandbox.id) Module reference
Every resource group maps to a dedicated module under the Miosa namespace. All public functions accept a %Miosa.Client{} as their first argument.
| Module | Key functions |
|---|---|
Miosa.Client | new/1, new/2, stream_sse/3 |
Miosa.Sandboxes | create/2, get/2, list/1, delete/2, update/3, preview_token/3, fork/3, wait_until_ready/3 |
Miosa.Sandbox.Files | write/4, read/3, list/3, delete/3, mkdir/3, move/4, tree/3, write_many/3, watch/3 |
Miosa.Sandbox.Exec | run/3, bash/3, python/3 |
Miosa.Sandbox.Events | stream/3 |
Miosa.Sandbox.Env | list/2, set/4, delete/3 |
Miosa.Sandbox.Processes | start/3, list/2, stop/3, logs/4 |
Miosa.Sandbox.Share | create/3, list/2, revoke/3 |
Miosa.Sandbox.Previews | create/3, list/2, share/3, destroy/3 |
Miosa.Sandbox.Snapshots | create/3, restore/3, list/2 |
Miosa.Sandbox.Tags | add/3, remove/3, list/2 |
Miosa.Sandbox.Terminal | open/2, resize/4 |
Miosa.SandboxTemplates | list/1, get/2, create/2, delete/2 |
Miosa.Tenant | current/1, get_preview_domain/1, set_preview_domain/2, verify_preview_domain/1, delete_preview_domain/1, get_branding/1, set_branding/2, delete_branding/1 |
Miosa.Computers | create/2, get/2, list/2, update/3, delete/2 |
Miosa.Computer | start/2, stop/2, restart/2, clone/2, resize/3, move/3, destroy/2 |
Miosa.Computer.Env | list/2, create/3, update/3, delete/3 |
Miosa.Computer.Ports | list/2, create/3, update/3, delete/3 |
Miosa.Computer.AutoStop | get/2, update/3 |
Miosa.Computer.Metrics | get/3 |
Miosa.Desktop | screenshot/2, click/4, type/3, key/3, drag/6, scroll/5, set_wallpaper/3, accessibility_tree/2 |
Miosa.Workspaces | list/1, create/2, get/2, update/3, delete/2, stats/2, usage/3 |
Miosa.Webhooks | create/2, get/2, list/1, update/3, delete/2, test/2 |
Miosa.Usage | get/2, current/1, sessions/2, report/3 |
Miosa.Quotas | get/2, set/3, delete/2 |
Miosa.AuditLog | list/2 |
Miosa.ApiKeys | create/2, get/2, list/1, revoke/2 |
Miosa.Snapshots | create/3, get/2, list/2, restore/3, delete/2 |
Miosa.Deployments | create/2, get/2, list/1, update/3, delete/2, publish/2, rollback/3 |
Miosa.Databases | create/2, get/2, list/1, update/3, delete/2, credentials/2 |
Miosa.Storage | create_bucket/2, list_buckets/1, upload_object/4, download_object/3, signed_url/4 |
Miosa.Volumes | create/2, get/2, list/1, resize/3, attach/4, detach/3, delete/2 |
Miosa.Completions | create/2, stream/3 |
Miosa.Embeddings | create/2 |
Miosa.CommandCenter | create_session/2, get_session/2, list_sessions/1, cancel_session/2 |
Miosa.WorkspaceMembers | list/2, get/3, update/4, remove/3 |
Miosa.WorkspaceInvites | create/3, list/2, get/3, cancel/3, accept/2 |
Miosa.OrgInvites | create/2, list/1, get/2, cancel/2, accept/2 |
Miosa.Admin.Tenants | list/1, get/2, suspend/2, unsuspend/2 |
Miosa.Credits | balance/1, history/2 |
Miosa.Error | struct: :message, :status, :code, :body |
Common patterns
Idempotency key
{:ok, sandbox} = Miosa.Sandboxes.create(client, %{
name: "my-sandbox",
idempotency_key: UUID.uuid4()
}) Custom base URL (self-hosted)
client = Miosa.Client.new("msk_live_...",
base_url: "https://api.your-domain.com/api/v1"
) Webhook verification
defp verify_webhook(payload, signature, secret) do
expected = :crypto.mac(:hmac, :sha256, secret, payload) |> Base.encode16(case: :lower)
Plug.Crypto.secure_compare(expected, signature)
end