API
The ix API is a binary RPC protocol, not HTTP with JSON. The CLI and the TypeScript, Python and Rust SDKs speak it, so use one of them.
Address and transport
- Base URL:
https://api.ix.dev. SetIX_SERVERto use another one. - RPC endpoint:
https://api.ix.dev/rpc. The client appendsrpcto the base URL. - Transport: WebTransport, which is HTTP/3 over QUIC. Each call opens one stream. The call name travels in the
x-ix-methodrequest header, and the body is a binary frame. - Browsers:
@indexable/sdk/browseruses the native WebTransport API. Node and Bun have no WebTransport, so their SDK is a native addon that runs the same client in Rust.
Authentication
The CLI and the SDKs send your credential as a bearer token in the Authorization header and find it in this order:
- An explicit argument, for example
new Client({ token })orix_sdk.Client(token="..."). - The
IX_TOKENenvironment variable: an API key, for CI and servers. - The login
ix loginstored in~/.config/ix/config.toml.
If none is found the SDK raises Unauthorized with the text run `ix login`.
ix login # a person, once
ix me # confirms who you are
ix keys create my-agent --limit 25 # CI or a server: prints a key once
export IX_TOKEN="the-key"Each key can carry a spending ceiling in dollars (--limit) and permission scopes (--scope vm:*). A key cannot grant more than it holds. See secrets and keys.
SDK clients
// Node and Bun. Reads IX_TOKEN, then ~/.config/ix/config.toml.
import { Client, Machine } from "@indexable/sdk"
const client = new Client() // or new Client({ token, baseUrl })
// Browser. WebTransport only.
import { Client as BrowserClient } from "@indexable/sdk/browser"
const web = new BrowserClient({ token, baseUrl: "https://api.ix.dev" })Operations
Python uses the TypeScript names in snake case. The wire method is the value of x-ix-method.
| Operation | CLI | TypeScript | Wire method |
|---|---|---|---|
| Create a machine | ix new [image] | Machine.create(options), client.machines.create(options) | vm.build_commit |
| List machines | ix ls | Machine.list() | vm.list |
| Find a machine by name | ix ls | Machine.attach(name) | vm.get_by_name |
| Run a command | ix shell <vm> --noninteractive -- <cmd> | machine.exec([...]), machine.exec([...], { check: true }) | vm.exec |
| Read, write and list files | none | machine.readFile, writeFile, listDir | vm.fs.read, vm.fs.write, vm.fs.list |
| Take a snapshot | ix snapshot create <vm> | machine.snapshot() | vm.snapshot |
| List snapshots | ix snapshot ls <vm> | client.snapshots.list(machineId) | vm.vcs.list_snapshots |
| Restore a snapshot | ix new <snapshot-id> | client.snapshots.restore(id, name) | vm.vcs.restore |
| Start, stop, restart | ix start, ix stop, ix restart | machine.start(), stop(), restart() | vm.start, vm.stop, vm.restart |
| Remove a machine | ix rm <vm> | machine.delete() | vm.delete |
| Read logs | ix logs <vm> | machine.logs(), machine.tailLogs() | vm.logs, vm.logs_tail |
| Reach a port | ix new --l7-proxy-port <port> publishes a port | machine.forwardPort(port), machine.connectPort(port) | vm.port_forward |
| Store a secret | ix secret set <name> | client.secrets.set(name, value) | secret.set |
| Create an API key | ix keys create <name> | client.keys.create(name, limitUsd) | billing.create_token |
| Create a network group | ix group create <slug> | client.groups.create(slug) | group.create |
Run ix <command> --help for the flags of each command.
Errors
A call that fails returns a typed error with a code, such as Unauthorized, NotFound, PermissionDenied, Conflict or a payment error when the balance is empty. A non-zero exit code from a command is a result of exec, not an error. Pass check: true to turn it into one.