On this page

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

AreaWhat shipped
Sandbox lifecycleMiosa.Sandboxes.update/3, Miosa.Sandboxes.preview_token/3, Miosa.Sandboxes.fork/3
Tenant preview domainMiosa.Tenant.get_preview_domain/1, set_preview_domain/2, verify_preview_domain/1, delete_preview_domain/1
Tenant brandingMiosa.Tenant.get_branding/1, set_branding/2, delete_branding/1
WebhooksMiosa.Webhooks.create/2, list/1, get/2, delete/2, test/2
Files advancedMiosa.Sandbox.Files.tree/3, write_many/3, watch/2
Sandbox envMiosa.Sandbox.Env.list/2, set/4, delete/3
ProcessesMiosa.Sandbox.Processes.start/3, list/2, stop/3, logs/3
Fork + templatesMiosa.Sandboxes.fork/3, Miosa.SandboxTemplates.*
Usage / quotasMiosa.Usage.get/2, Miosa.Quotas.set/3, get/2, delete/2
Audit logMiosa.AuditLog.list/2 with cursor pagination
SharingMiosa.Sandbox.Share.create/3, list/2, revoke/3
Workspace membersMiosa.WorkspaceMembers.*, Miosa.WorkspaceInvites.*
Org invitesMiosa.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

OptionEnv varDefaultDescription
api_key (positional)--Workspace key (msk_live_*). Must start with msk_.
:base_urlMIOSA_BASE_URLhttps://api.miosa.ai/api/v1API endpoint
:timeout-30_000Connect timeout in milliseconds
:receive_timeout-60_000Receive timeout for long-running requests
:retry-falsePass 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)
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)

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.

ModuleKey functions
Miosa.Clientnew/1, new/2, stream_sse/3
Miosa.Sandboxescreate/2, get/2, list/1, delete/2, update/3, preview_token/3, fork/3, wait_until_ready/3
Miosa.Sandbox.Fileswrite/4, read/3, list/3, delete/3, mkdir/3, move/4, tree/3, write_many/3, watch/3
Miosa.Sandbox.Execrun/3, bash/3, python/3
Miosa.Sandbox.Eventsstream/3
Miosa.Sandbox.Envlist/2, set/4, delete/3
Miosa.Sandbox.Processesstart/3, list/2, stop/3, logs/4
Miosa.Sandbox.Sharecreate/3, list/2, revoke/3
Miosa.Sandbox.Previewscreate/3, list/2, share/3, destroy/3
Miosa.Sandbox.Snapshotscreate/3, restore/3, list/2
Miosa.Sandbox.Tagsadd/3, remove/3, list/2
Miosa.Sandbox.Terminalopen/2, resize/4
Miosa.SandboxTemplateslist/1, get/2, create/2, delete/2
Miosa.Tenantcurrent/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.Computerscreate/2, get/2, list/2, update/3, delete/2
Miosa.Computerstart/2, stop/2, restart/2, clone/2, resize/3, move/3, destroy/2
Miosa.Computer.Envlist/2, create/3, update/3, delete/3
Miosa.Computer.Portslist/2, create/3, update/3, delete/3
Miosa.Computer.AutoStopget/2, update/3
Miosa.Computer.Metricsget/3
Miosa.Desktopscreenshot/2, click/4, type/3, key/3, drag/6, scroll/5, set_wallpaper/3, accessibility_tree/2
Miosa.Workspaceslist/1, create/2, get/2, update/3, delete/2, stats/2, usage/3
Miosa.Webhookscreate/2, get/2, list/1, update/3, delete/2, test/2
Miosa.Usageget/2, current/1, sessions/2, report/3
Miosa.Quotasget/2, set/3, delete/2
Miosa.AuditLoglist/2
Miosa.ApiKeyscreate/2, get/2, list/1, revoke/2
Miosa.Snapshotscreate/3, get/2, list/2, restore/3, delete/2
Miosa.Deploymentscreate/2, get/2, list/1, update/3, delete/2, publish/2, rollback/3
Miosa.Databasescreate/2, get/2, list/1, update/3, delete/2, credentials/2
Miosa.Storagecreate_bucket/2, list_buckets/1, upload_object/4, download_object/3, signed_url/4
Miosa.Volumescreate/2, get/2, list/1, resize/3, attach/4, detach/3, delete/2
Miosa.Completionscreate/2, stream/3
Miosa.Embeddingscreate/2
Miosa.CommandCentercreate_session/2, get_session/2, list_sessions/1, cancel_session/2
Miosa.WorkspaceMemberslist/2, get/3, update/4, remove/3
Miosa.WorkspaceInvitescreate/3, list/2, get/3, cancel/3, accept/2
Miosa.OrgInvitescreate/2, list/1, get/2, cancel/2, accept/2
Miosa.Admin.Tenantslist/1, get/2, suspend/2, unsuspend/2
Miosa.Creditsbalance/1, history/2
Miosa.Errorstruct: :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

Where to next

Was this helpful?