openapi: 3.1.0 info: title: Wheelmachine API version: "1.0.0" summary: Machines for agents and people, by the second. description: | One API for machines that start in milliseconds from ready spares, pause with their memory kept, copy while they run, and keep their files sealed with your account's own key. Every call carries an API key: `Authorization: Bearer ak_live_...`. A key holds only the scopes it was made with, and keys made for other work (models) hold no machine scope at all. People sign in at the dashboard, https://app.wheelmachine.ai, with GitHub or Google, in the browser. The dashboard calls these same routes on its own address with the person's sign-in instead of a key - a cookie for that address alone, and the sign-in's token in `X-CSRF-Token` on every call (on a WebSocket's handshake, which carries no header of a page's own, as the protocol `wm.csrf.` beside `wm.terminal`) - and may do what a `cloud:*` key may. A program gets its key the same way, through the browser (`wm login`): it starts a sign-in, the person approves it at https://app.wheelmachine.ai/device, and the program is given a new key of the account, once. Errors are `application/problem+json` (RFC 9457) with a stable `code`. Every answer carries `X-Request-Id`; give it when you ask us about a call. Calls that create something accept an `Idempotency-Key`, so a retry after a dropped connection never makes two. servers: - url: https://api.wheelmachine.ai description: Wheelmachine - url: https://app.wheelmachine.ai description: The dashboard, where people sign in; it calls the same routes with their sign-in security: - key: [] - session: [] csrf: [] tags: - name: Account - name: Sign-in - name: Projects - name: Keys - name: Machines - name: Files and commands - name: Ports description: | A program listening on a port inside a machine, at a web address of its own: `https://-.run.wheelmachine.ai`. HTTP, WebSockets and streams pass through; a paused or archived machine is started by the first visit (a browser is shown a page that says so while it starts). An address is private unless the port was opened `public`: it answers a call that carries an API key able to see the machine (`Authorization: Bearer ak_live_...`, which never reaches the program) and a browser that opened a link made for it; anyone else is answered 401, and an address of a port that is not open, or of another account's machine, 404. - name: Secrets - name: Catalogue paths: /v1/me: get: tags: [Account] operationId: getMe summary: The account and key this call is made with responses: "200": description: Who is calling content: application/json: schema: { $ref: "#/components/schemas/Me" } "401": { $ref: "#/components/responses/Unauthorized" } /v1/projects: get: tags: [Projects] operationId: listProjects summary: The account's projects parameters: - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/After" responses: "200": description: A page of projects content: application/json: schema: { $ref: "#/components/schemas/ProjectList" } "400": $ref: "#/components/responses/BadRequest" description: "`limit` or `after` is not right" "401": { $ref: "#/components/responses/Unauthorized" } post: tags: [Projects] operationId: createProject summary: Make a project description: Needs the `cloud:projects:write` scope. A key limited to some projects cannot make new ones. parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ProjectCreate" } responses: "201": description: The new project content: application/json: schema: { $ref: "#/components/schemas/Project" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "409": { $ref: "#/components/responses/Conflict" } "422": $ref: "#/components/responses/BadRequest" description: "This `Idempotency-Key` was used with a different request" "429": { $ref: "#/components/responses/TooManyRequests" } /v1/projects/{project_id}: parameters: - $ref: "#/components/parameters/ProjectId" get: tags: [Projects] operationId: getProject summary: One project responses: "200": description: The project content: application/json: schema: { $ref: "#/components/schemas/Project" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } patch: tags: [Projects] operationId: updateProject summary: Rename a project requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ProjectUpdate" } responses: "200": description: The project as it is now content: application/json: schema: { $ref: "#/components/schemas/Project" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } delete: tags: [Projects] operationId: deleteProject summary: Delete an empty project description: A project that still holds machines is refused with `409 project_not_empty`. responses: "204": { description: Deleted } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } /v1/keys: get: tags: [Keys] operationId: listKeys summary: The account's machine keys description: Keys that hold at least one `cloud:` scope. Secrets are never shown again after a key is made. responses: "200": description: The keys content: application/json: schema: { $ref: "#/components/schemas/KeyList" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } post: tags: [Keys] operationId: createKey summary: Make a key description: | Needs the `cloud:keys:write` scope. A new key can hold only scopes and projects the calling key holds, so a key can never make a stronger one. The secret is in this answer only. parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/KeyCreate" } responses: "201": description: The new key, with its secret shown this once content: application/json: schema: { $ref: "#/components/schemas/KeyCreated" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": $ref: "#/components/responses/NotFound" description: "A project in `projects` is not one this key reaches" "409": $ref: "#/components/responses/Conflict" description: "The first call with this `Idempotency-Key` is still running" "422": $ref: "#/components/responses/BadRequest" description: "This `Idempotency-Key` was used with a different request" "429": { $ref: "#/components/responses/TooManyRequests" } /v1/keys/{key_id}: parameters: - $ref: "#/components/parameters/KeyId" delete: tags: [Keys] operationId: revokeKey summary: Revoke a key description: The key stops working within seconds, everywhere. A key can revoke itself. responses: "204": { description: Revoked } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } /v1/ssh/certificates: post: tags: [Files and commands] operationId: signSSHCertificate summary: Sign a short-lived SSH certificate for a machine description: | Needs the `cloud:machines:exec` scope. Give a public key and a machine; get back an SSH certificate good for at most an hour, for that machine alone (its one principal is the machine's id, which is also the SSH user to connect as). Present it to the SSH server (`ssh_host`) to open a real SSH session - for `scp`, `sftp` and editors' remote-SSH. There is no password or bare public-key login. `wm ssh-config` prints the matching `~/.ssh/config` block. A certificate works only while the key or the sign-in that asked for it does: revoking that key, or signing out, ends the SSH sessions it opened, and it opens no more. requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/SSHCertificateRequest" } responses: "201": description: The signed certificate content: application/json: schema: { $ref: "#/components/schemas/SSHCertificate" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": $ref: "#/components/responses/Conflict" description: "The first call with this `Idempotency-Key` is still running" "422": $ref: "#/components/responses/BadRequest" description: "This `Idempotency-Key` was used with a different request" "501": { $ref: "#/components/responses/NotImplemented" } /v1/ssh: get: tags: [Files and commands] operationId: getSSH summary: Whether real SSH is open here, and how to reach and trust it description: | Open when SSH certificates are signed here and the SSH server's address is set: what `wm ssh-config` needs to point your own `ssh`, `scp` and `sftp` at a machine. Public keys only. responses: "200": description: The SSH door content: application/json: schema: { $ref: "#/components/schemas/SSHDoor" } "401": { $ref: "#/components/responses/Unauthorized" } /v1/snapshots: get: tags: [Machines] operationId: listSnapshots summary: The account's snapshots, newest first description: | Needs the `cloud:machines:read` scope. Every snapshot taken through the API of a machine that is not deleted, in the projects this key reaches - each one a new machine can start from with `from_snapshot`. A deleted machine's snapshots go with it. parameters: - $ref: "#/components/parameters/SnapshotLimit" responses: "200": description: The snapshots content: application/json: schema: { $ref: "#/components/schemas/SnapshotList" } "400": $ref: "#/components/responses/BadRequest" description: "`limit` is not between 1 and 200" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /v1/machines: get: tags: [Machines] operationId: listMachines summary: Machines, newest first parameters: - name: project in: query description: Only this project's machines. schema: { type: string } - name: state in: query schema: { $ref: "#/components/schemas/MachineState" } - name: label in: query description: "`key=value`; repeat to ask for every one of several labels." schema: type: array items: { type: string } style: form explode: true - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/After" responses: "200": description: A page of machines content: application/json: schema: { $ref: "#/components/schemas/MachineList" } "400": $ref: "#/components/responses/BadRequest" description: "A `state`, `label`, `limit` or `after` that is not right" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": $ref: "#/components/responses/NotFound" description: "The `project` is not one this key reaches" post: tags: [Machines] operationId: createMachine summary: Make a machine description: | Answers when the machine is running (201), usually in well under a second. When no server has room just now one is started for it; if that takes longer than `wait_s`, the answer is `202` with the machine `starting`, and `GET /v1/machines/{machine_id}/wait` follows it. parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/MachineCreate" } responses: "201": description: Running content: application/json: schema: { $ref: "#/components/schemas/Machine" } "202": description: Starting; follow it with the wait call content: application/json: schema: { $ref: "#/components/schemas/Machine" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": { $ref: "#/components/responses/PaymentRequired" } "403": { $ref: "#/components/responses/Forbidden" } "404": $ref: "#/components/responses/NotFound" description: "The `project`, or the machine in `from_snapshot`, is not one this key reaches" "409": $ref: "#/components/responses/Conflict" description: "The first call with this `Idempotency-Key` is still running; or its internet is held back and no server here can hold back a machine's internet yet (`not_open`)" "422": $ref: "#/components/responses/BadRequest" description: "This `Idempotency-Key` was used with a different request" "429": { $ref: "#/components/responses/TooManyRequests" } "503": { $ref: "#/components/responses/Unavailable" } /v1/machines/{machine_id}: parameters: - $ref: "#/components/parameters/MachineId" get: tags: [Machines] operationId: getMachine summary: One machine responses: "200": description: The machine content: application/json: schema: { $ref: "#/components/schemas/Machine" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } patch: tags: [Machines] operationId: updateMachine summary: Rename a machine, move it to another project, or change its labels, secrets or internet description: | Labels merge: a label given `null` is taken off. A machine moves only to a project this key reaches, and keeps everything of its own as it goes. Everything asked is changed at once, or nothing is. What it was made as - its size and when it sleeps by itself - stays as it was made: a new machine started from a snapshot of it can be made otherwise. requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/MachineUpdate" } responses: "200": description: The machine as it is now content: application/json: schema: { $ref: "#/components/schemas/Machine" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": $ref: "#/components/responses/NotFound" description: "The machine, or the `project` it is moved to, is not one this key reaches" "409": $ref: "#/components/responses/Conflict" description: "Its internet was held back while it runs on a server that cannot hold it back (`not_open`): the rule is kept, and takes effect when it next starts on a server that can" delete: tags: [Machines] operationId: deleteMachine summary: Delete a machine and its files responses: "204": { description: Deleted } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } /v1/machines/{machine_id}/start: parameters: - $ref: "#/components/parameters/MachineId" post: tags: [Machines] operationId: startMachine summary: Run it again, from paused, stopped or archived requestBody: content: application/json: schema: { $ref: "#/components/schemas/WaitOptions" } responses: "200": description: Running content: application/json: schema: { $ref: "#/components/schemas/Machine" } "202": description: Starting content: application/json: schema: { $ref: "#/components/schemas/Machine" } "400": $ref: "#/components/responses/BadRequest" description: "`wait_s` is not between 0 and 600" "401": { $ref: "#/components/responses/Unauthorized" } "402": { $ref: "#/components/responses/PaymentRequired" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": $ref: "#/components/responses/Conflict" description: "The machine cannot start in the state it is in; or its internet is held back and no server here can hold back a machine's internet yet (`not_open`)" "429": $ref: "#/components/responses/TooManyRequests" description: "Too many calls just now, or the account has as many machines running or kept as it may" "503": { $ref: "#/components/responses/Unavailable" } /v1/machines/{machine_id}/pause: parameters: - $ref: "#/components/parameters/MachineId" post: tags: [Machines] operationId: pauseMachine summary: Pause it with its memory kept description: Nothing runs and no running time is charged; `start` brings it back where it was. responses: "200": description: Paused content: application/json: schema: { $ref: "#/components/schemas/Machine" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } /v1/machines/{machine_id}/stop: parameters: - $ref: "#/components/parameters/MachineId" post: tags: [Machines] operationId: stopMachine summary: Stop it; its files stay responses: "200": description: Stopped content: application/json: schema: { $ref: "#/components/schemas/Machine" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": $ref: "#/components/responses/Conflict" description: "The machine cannot stop in the state it is in" /v1/machines/{machine_id}/archive: parameters: - $ref: "#/components/parameters/MachineId" post: tags: [Machines] operationId: archiveMachine summary: Put its files away in storage, the cheapest way to keep it description: Sealed with the account's key when the machine's files are sealed. `start` brings it back. responses: "200": description: Archived content: application/json: schema: { $ref: "#/components/schemas/Machine" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": $ref: "#/components/responses/Conflict" description: "The machine cannot be archived in the state it is in" /v1/machines/{machine_id}/copy: parameters: - $ref: "#/components/parameters/MachineId" post: tags: [Machines] operationId: copyMachine summary: A new machine, a copy of this one as it is now description: | Made beside the original where there is room, so the files are copied in a moment. The copy's internet is held back as the original's is: when it is and no server here can hold back a machine's internet yet, the answer is `409 not_open`. parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: content: application/json: schema: { $ref: "#/components/schemas/MachineCopy" } responses: "201": description: The copy, running content: application/json: schema: { $ref: "#/components/schemas/Machine" } "400": $ref: "#/components/responses/BadRequest" description: "A name, label or `wait_s` that is not right" "401": { $ref: "#/components/responses/Unauthorized" } "402": { $ref: "#/components/responses/PaymentRequired" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": $ref: "#/components/responses/Conflict" description: "The first call with this `Idempotency-Key` is still running" "422": $ref: "#/components/responses/BadRequest" description: "This `Idempotency-Key` was used with a different request" "429": { $ref: "#/components/responses/TooManyRequests" } "503": { $ref: "#/components/responses/Unavailable" } /v1/machines/{machine_id}/snapshots: parameters: - $ref: "#/components/parameters/MachineId" post: tags: [Machines] operationId: snapshotMachine summary: Keep its files as they are now description: A new machine can be made from a snapshot with `from_snapshot`. parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: content: application/json: schema: type: object additionalProperties: false properties: note: { type: string, maxLength: 200 } responses: "201": description: The snapshot content: application/json: schema: { $ref: "#/components/schemas/Snapshot" } "400": $ref: "#/components/responses/BadRequest" description: "The note is longer than 200 characters" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "422": $ref: "#/components/responses/BadRequest" description: "This `Idempotency-Key` was used with a different request" get: tags: [Machines] operationId: listMachineSnapshots summary: The machine's snapshots, newest first description: Needs the `cloud:machines:read` scope. Those taken through the API; a deleted machine has none. parameters: - $ref: "#/components/parameters/SnapshotLimit" responses: "200": description: Its snapshots content: application/json: schema: { $ref: "#/components/schemas/SnapshotList" } "400": $ref: "#/components/responses/BadRequest" description: "`limit` is not between 1 and 200" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } /v1/machines/{machine_id}/wait: parameters: - $ref: "#/components/parameters/MachineId" get: tags: [Machines] operationId: waitForMachine summary: Answer when the machine reaches a state parameters: - name: state in: query required: true schema: { $ref: "#/components/schemas/MachineState" } - name: timeout_s in: query schema: { type: number, minimum: 0, maximum: 600, default: 60 } responses: "200": description: It is in that state content: application/json: schema: { $ref: "#/components/schemas/Machine" } "400": $ref: "#/components/responses/BadRequest" description: "No `state`, or a `timeout_s` that is not between 0 and 600" "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } "408": { $ref: "#/components/responses/Timeout" } /v1/machines/{machine_id}/metrics: parameters: - $ref: "#/components/parameters/MachineId" get: tags: [Machines] operationId: getMachineMetrics summary: What the machine used, step by step, over the last day description: | Needs the `cloud:machines:read` scope. Its processors as a share of its own size, the most memory it held and the most it may hold, and the bytes a second it read and wrote on its disk and received and sent on its network - a point for each `step` from `since` on. Its server reads it every few seconds while it runs and keeps a point a minute for the last day, in memory: a step it was paused or stopped through has no point, and the numbers begin again when the machine moves to another server or its server's program starts anew (`kept_since` says how far back they go). A machine that is not on a server just now has no points. parameters: - name: since in: query description: "From when: a time (RFC 3339), or how long ago (`90s`, `15m`, `6h`, `1d`); the last hour when left out, and at most a day back." schema: { type: string, examples: ["6h", "2026-10-23T09:00:00Z"] } - name: step in: query description: "How long each point covers: whole minutes from 1 to 60, in seconds (`300`) or as a span (`5m`)." schema: { type: string, default: "60", examples: ["60", "5m", "1h"] } responses: "200": description: Its points, the oldest first content: application/json: schema: { $ref: "#/components/schemas/MachineMetrics" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "501": $ref: "#/components/responses/NotImplemented" description: "It runs on a server that does not keep its numbers yet: none come while it runs there" "503": { $ref: "#/components/responses/Unavailable" } /v1/machines/{machine_id}/exec: parameters: - $ref: "#/components/parameters/MachineId" post: tags: [Files and commands] operationId: execInMachine summary: Run a command in the machine and get what it wrote description: | Needs the `cloud:machines:exec` scope. A paused machine is woken for it. Commands run as the machine's user unless `root` is asked for; root is inside the machine only, never on our servers. requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ExecRequest" } responses: "200": description: It ran; its exit code says how it ended content: application/json: schema: { $ref: "#/components/schemas/ExecResult" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": { $ref: "#/components/responses/PaymentRequired" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": $ref: "#/components/responses/Conflict" description: "The machine cannot run a command in the state it is in; or its internet is held back and no server here can hold back a machine's internet yet (`not_open`)" "429": { $ref: "#/components/responses/TooManyRequests" } "503": $ref: "#/components/responses/Unavailable" description: "The command's result did not come back: it may or may not have run, so check before running it again" "504": $ref: "#/components/responses/Timeout" description: "The command ran longer than 10 minutes; it may still be running in the machine" /v1/machines/{machine_id}/terminal: parameters: - $ref: "#/components/parameters/MachineId" get: tags: [Files and commands] operationId: openTerminal summary: Open an interactive terminal in the machine (WebSocket) description: | Needs the `cloud:machines:exec` scope. This route is a WebSocket upgrade, not a plain request: connect with `Upgrade: websocket` (version 13) and the usual `Authorization: Bearer` header. A paused machine is woken for it; the terminal is a login shell unless a command is given, as the machine's user unless `root` is asked for. Over the socket, each message is one event. From you: a **binary** message is typed in (keystrokes, bytes as they are); a **text** message `{"resize":{"cols":C,"rows":R}}` sets the window. From us: a **binary** message is what the program wrote on its terminal; a final **text** message `{"exit_code":N}` (or `{"reason":..., "message":...}` when it did not end by itself) says how it ended, just before the socket closes. A caller may ask for the protocol `wm.terminal` on the socket, and is answered with it; the dashboard's page does, beside its sign-in's token (`wm.csrf.`), from its own address. Such a caller - a browser is never shown the answer to a handshake it was refused - is told a terminal that cannot open over the socket instead: it is switched, sent one text message `{"reason": , "status": , "message": ...}`, and closed with the code 4000 and that status (4501: not open for this machine). Any other caller is answered as below, before anything is switched. An open terminal stays open only while its caller still may hold it. When the sign-in it was opened with ends (signed out, here or from another browser), the key is revoked or expires, or the account is held or can no longer spend, it is ended - at once, or at the latest at the next half-minute ping - and every caller is told why the same way: one text message `{"reason": ..., "status": ..., "message": ...}` (4401 signed out or the key revoked, 4403 the account held, 4402 it cannot spend), then a close with the code 4000 and that status. We never log what is typed or shown. The audit records only that a terminal opened and closed. parameters: - { name: cols, in: query, schema: { type: integer, minimum: 1, maximum: 4096, default: 80 } } - { name: rows, in: query, schema: { type: integer, minimum: 1, maximum: 4096, default: 24 } } - name: user in: query schema: { type: string, enum: [user, root], default: user } - name: term in: query description: The terminal's kind, set as `TERM` inside (default `xterm-256color`). schema: { type: string, maxLength: 64 } - name: cwd in: query description: Where it starts, an absolute path; the work folder when left out. schema: { type: string, maxLength: 1024 } - name: cmd in: query description: A word of the command to run; repeat for each word. Left out, a login shell opens. schema: { type: string } responses: "101": { description: "Switching to the terminal's WebSocket" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": { $ref: "#/components/responses/PaymentRequired" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "426": { $ref: "#/components/responses/BadRequest" } "429": { $ref: "#/components/responses/TooManyRequests" } "501": { $ref: "#/components/responses/NotImplemented" } /v1/machines/{machine_id}/files: parameters: - $ref: "#/components/parameters/MachineId" - name: path in: query required: true description: "Inside the machine's home (`home/...`) or its work folder, where commands start (`work/...`)." schema: { type: string, maxLength: 1024 } get: tags: [Files and commands] operationId: readFile summary: Read a file description: Needs the `cloud:machines:exec` scope. responses: "200": description: The file's bytes content: application/octet-stream: schema: { type: string, contentMediaType: application/octet-stream } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } put: tags: [Files and commands] operationId: writeFile summary: Write a file, making the folders on the way description: Needs the `cloud:machines:exec` scope and a `Content-Length`. requestBody: required: true content: application/octet-stream: schema: { type: string, contentMediaType: application/octet-stream } responses: "204": { description: Written } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "411": { $ref: "#/components/responses/BadRequest" } "413": { $ref: "#/components/responses/TooLarge" } delete: tags: [Files and commands] operationId: deleteFile summary: Delete a file, or a folder description: | Needs the `cloud:machines:exec` scope. A folder goes only when it is empty, unless `recursive` is true: then everything in it goes with it. A link is taken away as itself, never what it points to. The home and the work folder themselves cannot be deleted. parameters: - name: recursive in: query description: With a folder, everything in it too. schema: { type: boolean, default: false } responses: "204": { description: Deleted } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": { $ref: "#/components/responses/PaymentRequired" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": $ref: "#/components/responses/Conflict" description: "The folder is not empty and `recursive` is not true, it belongs to another user of the machine, or the machine cannot reach its files in the state it is in" /v1/machines/{machine_id}/folder: parameters: - $ref: "#/components/parameters/MachineId" - name: path in: query required: true description: "A folder of the machine's home (`home`, `home/...`) or of its work folder, where commands start (`work`, `work/...`)." schema: { type: string, maxLength: 1024 } get: tags: [Files and commands] operationId: listFolder summary: What a folder holds description: | Needs the `cloud:machines:exec` scope. Its own entries, folders first and then by name, each with its kind, its size and when it last changed; at most 1,000, and `truncated` says when there were more. A paused machine is woken for it. responses: "200": description: The folder content: application/json: schema: { $ref: "#/components/schemas/Folder" } "400": $ref: "#/components/responses/BadRequest" description: "The path is not one of the machine's home or work folder, or it names a file" "401": { $ref: "#/components/responses/Unauthorized" } "402": { $ref: "#/components/responses/PaymentRequired" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": $ref: "#/components/responses/Conflict" description: "The machine cannot reach its files in the state it is in" "501": $ref: "#/components/responses/NotImplemented" description: "The machine has no `find` that can say what each file is" /v1/machines/{machine_id}/ports: parameters: - $ref: "#/components/parameters/MachineId" get: tags: [Ports] operationId: listPorts summary: The machine's ports open at web addresses description: Needs the `cloud:machines:read` scope. responses: "200": description: Its open ports, lowest first content: application/json: schema: { $ref: "#/components/schemas/PortList" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } post: tags: [Ports] operationId: openPort summary: Open a port of the machine at a web address of its own description: | Needs the `cloud:machines:write` scope. Private unless `public` is true. A port that is open already is answered `200`, made public or private as asked (left out, `public` is false). The ports the machine itself uses (5900, 6080, 7070, 9222, 9223 and 9224) cannot be opened (`400`), and a machine has at most 20 open (`429` with the code `limit_reached`). Until web addresses are served, the answer is `503` with the code `not_open`, which waiting a moment does not change. requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/PortOpen" } responses: "200": description: It was open already; it is public or private as asked content: application/json: schema: { $ref: "#/components/schemas/Port" } "201": description: Open content: application/json: schema: { $ref: "#/components/schemas/Port" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/TooManyRequests" } "503": { $ref: "#/components/responses/Unavailable" } /v1/machines/{machine_id}/ports/{port}: parameters: - $ref: "#/components/parameters/MachineId" - $ref: "#/components/parameters/Port" delete: tags: [Ports] operationId: closePort summary: Close a port's web address description: | Needs the `cloud:machines:write` scope. The address answers 404 from then on, and links made for it no longer open it. Closing a port that is not open is answered `204` all the same. responses: "204": { description: Closed } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } /v1/machines/{machine_id}/ports/{port}/link: parameters: - $ref: "#/components/parameters/MachineId" - $ref: "#/components/parameters/Port" post: tags: [Ports] operationId: linkPort summary: A link that lets one browser into a private address for a while description: | Needs the `cloud:machines:read` scope. The link opens once, within five minutes. The browser that opens it is given access to that one address for `access_s` seconds (a cookie for that address alone) and is taken to `path` on it; a page that only previews links does not use it up. The link is shown in this answer only, and each call makes a new one: it is not sent again once it may have reached the API. A port that is not open is answered `404`; until web addresses are served, `503` with the code `not_open`. requestBody: content: application/json: schema: { $ref: "#/components/schemas/PortLinkCreate" } responses: "201": description: The link content: application/json: schema: { $ref: "#/components/schemas/PortLink" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "503": { $ref: "#/components/responses/Unavailable" } /v1/secrets: get: tags: [Secrets] operationId: listSecrets summary: The account's secrets, by name, and the machines that get each description: | Needs the `cloud:machines:read` scope. A secret's value is never shown again once it is set: not here, not in any other answer, not in the audit. A key limited to some projects lists only the secrets its projects' machines get, and sees only those machines. responses: "200": description: The secrets content: application/json: schema: { $ref: "#/components/schemas/SecretList" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "503": { $ref: "#/components/responses/NotSetUp" } /v1/secrets/{name}: parameters: - $ref: "#/components/parameters/SecretName" put: tags: [Secrets] operationId: setSecret summary: Set a secret, or set it anew description: | Needs the `cloud:secrets:write` scope and a key of the whole account: secrets belong to the account, and a key limited to some projects cannot set, delete or give them (403). The value is sealed at once with a key of the account's own and kept only so; the machines given the secret get the new value from their next command. An account keeps at most 200 secrets. requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/SecretSet" } responses: "200": description: Set anew content: application/json: schema: { $ref: "#/components/schemas/Secret" } "201": description: Set for the first time content: application/json: schema: { $ref: "#/components/schemas/Secret" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "413": { $ref: "#/components/responses/TooLarge" } "429": { $ref: "#/components/responses/TooManyRequests" } "503": { $ref: "#/components/responses/NotSetUp" } delete: tags: [Secrets] operationId: deleteSecret summary: Delete a secret description: | Needs the `cloud:secrets:write` scope and a key of the whole account. The secret is taken off every machine that was given it: one set again later under the same name goes to none of them until it is given anew. responses: "204": { description: Deleted } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "503": { $ref: "#/components/responses/NotSetUp" } /v1/sizes: get: tags: [Catalogue] operationId: listSizes summary: The sizes a machine without a GPU comes in, and what an hour of each costs description: What an hour of a machine of each size running comes to - the price its running time is counted at. A paused machine runs no time. responses: "200": description: The sizes, the smallest first content: application/json: schema: { $ref: "#/components/schemas/SizeList" } "401": { $ref: "#/components/responses/Unauthorized" } /v1/regions: get: tags: [Catalogue] operationId: listRegions summary: Places a machine can be asked to run near description: The machine says where it actually runs in `location`. responses: "200": description: The places content: application/json: schema: type: object required: [object, data] properties: object: { const: list } data: type: array items: { $ref: "#/components/schemas/Region" } "401": { $ref: "#/components/responses/Unauthorized" } /v1/gpus: get: tags: [Catalogue] operationId: listGpus summary: GPUs, where they are and what an hour costs responses: "200": description: The GPUs on offer now content: application/json: schema: { $ref: "#/components/schemas/GpuList" } "401": { $ref: "#/components/responses/Unauthorized" } /v1/usage: get: tags: [Account] operationId: getUsage summary: What each machine ran for and cost in a month parameters: - name: month in: query description: "`YYYY-MM`; this month when left out." schema: { type: string, pattern: "^[0-9]{4}-[0-9]{2}$" } - name: project in: query schema: { type: string } responses: "200": description: The month's usage content: application/json: schema: { $ref: "#/components/schemas/Usage" } "400": $ref: "#/components/responses/BadRequest" description: "`month` is not `YYYY-MM`" "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": $ref: "#/components/responses/NotFound" description: "The `project` is not one this key reaches" /v1/login/devices: post: tags: [Sign-in] operationId: startDeviceLogin summary: Start signing a program in through the browser description: | For a program on a computer with a browser (`wm login`): no key is needed. The answer holds `device_code`, which the program keeps to itself, and `user_code`, which the person types at `verification_uri`, signed in to the dashboard, and approves. The code is never carried in an address (there is no `verification_uri_complete`): a link with someone else's code in it is how such sign-ins are stolen. The codes last 10 minutes and are used once. Then ask for the key every `interval` seconds (`/v1/login/devices/token`). security: [] requestBody: content: application/json: schema: { $ref: "#/components/schemas/DeviceLoginRequest" } responses: "200": description: The sign-in, waiting for the person content: application/json: schema: { $ref: "#/components/schemas/DeviceLoginStarted" } "400": { $ref: "#/components/responses/BadRequest" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/TooManyRequests" } /v1/login/devices/token: post: tags: [Sign-in] operationId: getDeviceLoginKey summary: The program's key, once the person approved description: | `202` while the person has not decided; `201` with the new key, its secret in this answer only, once they approved - named as the program asked ("wm on laptop") and holding the scopes they allowed; `403 login_denied` when they refused (or signed out since); `410 login_expired` when the code ran out or was used. Asked more often than `interval`: `429` with `Retry-After`. security: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/DeviceLoginToken" } responses: "201": description: The new key content: application/json: schema: { $ref: "#/components/schemas/KeyCreated" } "202": description: Still waiting for the person content: application/json: schema: { $ref: "#/components/schemas/DeviceLoginPending" } "400": { $ref: "#/components/responses/BadRequest" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "410": { $ref: "#/components/responses/Gone" } "429": { $ref: "#/components/responses/TooManyRequests" } /v1/login/devices/lookup: post: tags: [Sign-in] operationId: lookupDeviceLogin summary: What a program asks, by the code the person typed description: The dashboard's own, signed in. The code goes in the body, never in an address. security: - session: [] csrf: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/DeviceLoginDecision" } responses: "200": description: The program's sign-in, waiting content: application/json: schema: { $ref: "#/components/schemas/DeviceLogin" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/TooManyRequests" } /v1/login/devices/approve: post: tags: [Sign-in] operationId: approveDeviceLogin summary: Let a program in description: The dashboard's own, signed in. `scopes` narrows what the program asked for; it never widens it. security: - session: [] csrf: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/DeviceLoginDecision" } responses: "200": description: Approved; the program is given its key when it next asks content: application/json: schema: { $ref: "#/components/schemas/DeviceLogin" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "429": { $ref: "#/components/responses/TooManyRequests" } /v1/login/devices/deny: post: tags: [Sign-in] operationId: denyDeviceLogin summary: Refuse a program security: - session: [] csrf: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/DeviceLoginDecision" } responses: "200": description: Refused; the program is told so content: application/json: schema: { $ref: "#/components/schemas/DeviceLogin" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } /v1/session: delete: tags: [Sign-in] operationId: signOut summary: Sign out of the dashboard here security: - session: [] csrf: [] responses: "204": { description: Signed out } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /v1/sessions: get: tags: [Sign-in] operationId: listSessions summary: Where the account is signed in to the dashboard security: - session: [] csrf: [] responses: "200": description: Every sign-in that still works, this one marked `current` content: application/json: schema: { $ref: "#/components/schemas/SessionList" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } delete: tags: [Sign-in] operationId: signOutEverywhere summary: Sign out of the dashboard everywhere description: Every sign-in of the account ends, this one too. Keys are not touched; revoke those on their own. security: - session: [] csrf: [] responses: "204": { description: Signed out everywhere } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /v1/sessions/{session_id}: parameters: - $ref: "#/components/parameters/SessionId" delete: tags: [Sign-in] operationId: signOutBrowser summary: Sign one browser out of the dashboard description: That sign-in ends, and no other; this one too when it is this one. Another account's sign-in, or one that has ended, is not found. security: - session: [] csrf: [] responses: "204": { description: Signed out } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } /auth/providers: servers: - url: https://app.wheelmachine.ai get: tags: [Sign-in] operationId: signInProviders summary: Which ways of signing in are open description: A way that is not set up yet says `false`, and its door sends the browser back with `signin=not_set_up`. security: [] responses: "200": description: The doors content: application/json: schema: { $ref: "#/components/schemas/Providers" } /auth/github/start: servers: - url: https://app.wheelmachine.ai get: tags: [Sign-in] operationId: signInWithGitHub summary: Continue with GitHub description: | Sends the browser to GitHub with a state, PKCE and the scopes `read:user user:email`. `then` is a place on the dashboard to come back to (`/device`, `/keys`); anything else is dropped. security: [] parameters: - $ref: "#/components/parameters/Then" responses: "302": { description: "On to GitHub, or back to the dashboard with `signin=` saying why not" } /auth/github/callback: servers: - url: https://app.wheelmachine.ai get: tags: [Sign-in] operationId: signInWithGitHubBack summary: Back from GitHub description: | Signs the person in when the state is the one this browser set out with, in time, and GitHub verified an e-mail address of theirs; the account is found - by the GitHub account's number, else by that address - or made. Otherwise back to the dashboard with `signin=` one of `state`, `cancelled`, `unverified`, `held`, `young`, `unavailable`, `failed`. security: [] responses: "302": { description: "Signed in and on to the dashboard, or back to it with `signin=` saying why not" } /auth/google/start: servers: - url: https://app.wheelmachine.ai get: tags: [Sign-in] operationId: signInWithGoogle summary: Continue with Google description: Sends the browser to Google with a state, PKCE, a nonce and the scopes `openid email profile`. security: [] parameters: - $ref: "#/components/parameters/Then" responses: "302": { description: "On to Google, or back to the dashboard with `signin=` saying why not" } /auth/google/callback: servers: - url: https://app.wheelmachine.ai get: tags: [Sign-in] operationId: signInWithGoogleBack summary: Back from Google description: As GitHub's way back; the ID token must be Google's, for this application, in time, this sign-in's (nonce), with `email_verified`. security: [] responses: "302": { description: "Signed in and on to the dashboard, or back to it with `signin=` saying why not" } components: securitySchemes: key: type: http scheme: bearer description: "An API key: `ak_live_` and 40 characters." session: type: apiKey in: cookie name: __Host-wm_session description: The person's sign-in to the dashboard, on https://app.wheelmachine.ai only. It lasts 30 days from its last call. csrf: type: apiKey in: header name: X-CSRF-Token description: | The sign-in's token (the cookie `__Host-wm_csrf`, which the dashboard's page reads), on every call made with the sign-in; a call that changes anything must also come from the dashboard's address (`Origin`). parameters: MachineId: name: machine_id in: path required: true schema: { type: string, pattern: "^m_[0-9a-z]{26}$" } ProjectId: name: project_id in: path required: true schema: { type: string, pattern: "^prj_[0-9a-z]{26}$" } SessionId: name: session_id in: path required: true schema: { type: string, pattern: "^ses_[0-9a-z]{26}$" } SnapshotLimit: name: limit in: query schema: { type: integer, minimum: 1, maximum: 200, default: 100 } KeyId: name: key_id in: path required: true schema: { type: string, pattern: "^key_[0-9a-f]{16}$" } Port: name: port in: path required: true schema: { type: integer, minimum: 1, maximum: 65535 } SecretName: name: name in: path required: true description: Written as an environment variable's name is, which is what a machine's commands get it as. schema: { type: string, pattern: "^[A-Z_][A-Z0-9_]{0,63}$", examples: [DATABASE_URL] } Limit: name: limit in: query schema: { type: integer, minimum: 1, maximum: 200, default: 50 } After: name: after in: query description: The `next` of the page before. schema: { type: string } Then: name: then in: query description: Where on the dashboard to go once signed in, such as `/device`. schema: { type: string } IdempotencyKey: name: Idempotency-Key in: header description: | Any string up to 255 characters, unique per attempt. The same key with the same request within 24 hours gets the first answer again (with `Idempotent-Replayed: true`); with a different request it is refused. schema: { type: string, maxLength: 255 } headers: RequestId: description: This call's id. schema: { type: string } RetryAfter: description: Seconds to wait before trying again. schema: { type: integer } responses: BadRequest: description: The request is not right content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } Unauthorized: description: No key, or a key that does not work content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } PaymentRequired: description: The account cannot spend now - its limit for this month is reached, or the account is held content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } Forbidden: description: The key does not hold the scope or project this needs content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } NotFound: description: Nothing of that id that this key can see content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } Timeout: description: The state was not reached in time content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } Conflict: description: Not possible in the state it is in content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } NotImplemented: description: Not open for this machine content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } Gone: description: The sign-in's code ran out, or was used already content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } TooLarge: description: Too large content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } TooManyRequests: description: A limit is reached; `Retry-After` says when to try again headers: Retry-After: { $ref: "#/components/headers/RetryAfter" } content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } Unavailable: description: No room just now; `Retry-After` says when to try again headers: Retry-After: { $ref: "#/components/headers/RetryAfter" } content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } NotSetUp: description: Secrets are not set up on this API yet (`unavailable`, with a `Retry-After` of an hour) headers: Retry-After: { $ref: "#/components/headers/RetryAfter" } content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } schemas: Problem: type: object required: [type, title, status, code] properties: type: { type: string, description: "A link to the page that explains this `code`." } title: { type: string } status: { type: integer } detail: { type: string } code: type: string enum: [invalid_request, unauthorized, key_revoked, key_expired, forbidden, not_found, conflict, project_not_empty, payment_required, rate_limited, limit_reached, unavailable, timeout, too_large, not_open, internal, login_denied, login_expired] request_id: { type: string } retry_after_s: { type: number } Me: type: object required: [object, account, key, standing] properties: object: { const: me } account: type: object required: [id] properties: id: { type: string } name: { type: string, description: "With the dashboard's sign-in: the person's name, as their door gave it." } email: { type: string, description: "With the dashboard's sign-in: the e-mail address GitHub or Google verified." } key: description: The key this call is made with; null for a call made with the dashboard's sign-in. oneOf: - $ref: "#/components/schemas/Key" - type: "null" session: description: The dashboard's sign-in this call is made with, when it is. $ref: "#/components/schemas/Session" standing: { $ref: "#/components/schemas/Standing" } Standing: type: object required: [can_spend] properties: can_spend: { type: boolean, description: "False when the account's limit for this month is reached or the account is held." } balance_usd: type: [number, "null"] description: What is left to spend this month under the account's limit; null for an account with no money limit. reason: { type: string, description: "Why it cannot spend, in words a person reads." } Project: type: object required: [id, object, name, created_at] properties: id: { type: string, examples: [prj_01jb2xk3n4p5q6r7s8t9v0w1x2] } object: { const: project } name: { type: string } created_at: { type: string, format: date-time } ProjectCreate: type: object additionalProperties: false required: [name] properties: name: { type: string, minLength: 1, maxLength: 80 } ProjectUpdate: type: object additionalProperties: false required: [name] properties: name: { type: string, minLength: 1, maxLength: 80 } ProjectList: type: object required: [object, data, next] properties: object: { const: list } data: type: array items: { $ref: "#/components/schemas/Project" } next: { type: [string, "null"] } Scope: type: string description: | `cloud:machines:read` see machines, and the names of secrets; `cloud:machines:write` make, change, pause, copy and delete them; `cloud:machines:exec` run commands and read or write files in them; `cloud:gpus` make GPU machines; `cloud:projects:write` make and change projects; `cloud:keys:write` make and revoke keys; `cloud:secrets:write` set and delete secrets, and give them to machines; `cloud:*` all of these. enum: ["cloud:*", "cloud:machines:read", "cloud:machines:write", "cloud:machines:exec", "cloud:gpus", "cloud:projects:write", "cloud:keys:write", "cloud:secrets:write"] Key: type: object required: [id, object, name, scopes, projects, shown, state, created_at] properties: id: { type: string, examples: [key_3f9a1c2b4d5e6f70] } object: { const: key } name: { type: string } scopes: type: array items: { $ref: "#/components/schemas/Scope" } projects: type: array description: The projects it is limited to; empty for every project of the account. items: { type: string } shown: { type: string, description: "Its first and last characters, to recognise it by.", examples: ["ak_live_abcd…wxyz"] } state: { type: string, enum: [active, revoked, expired] } rate_per_minute: { type: [integer, "null"] } created_at: { type: string, format: date-time } expires_at: { type: [string, "null"], format: date-time } last_used_at: { type: [string, "null"], format: date-time } KeyCreate: type: object additionalProperties: false required: [name, scopes] properties: name: { type: string, minLength: 1, maxLength: 80 } scopes: type: array minItems: 1 items: { $ref: "#/components/schemas/Scope" } projects: type: array items: { type: string } expires_at: { type: string, format: date-time } rate_per_minute: { type: integer, minimum: 1, maximum: 6000 } KeyCreated: allOf: - $ref: "#/components/schemas/Key" - type: object required: [secret] properties: secret: { type: string, description: "The key itself. Shown in this answer only." } KeyList: type: object required: [object, data] properties: object: { const: list } data: type: array items: { $ref: "#/components/schemas/Key" } MachineState: type: string description: | `starting` being made ready; `running`; `paused` memory kept, nothing running; `stopped` files kept, memory not; `archived` files in storage; `recovering` its server was lost and it is being brought back from its last moments; `deleted`. enum: [starting, running, paused, stopped, archived, recovering, deleted] Machine: type: object required: [id, object, name, project, state, labels, network, files_sealed, created_at] properties: id: { type: string, examples: [m_01jb2xk3n4p5q6r7s8t9v0w1x2] } object: { const: machine } name: { type: string } project: { type: string } state: { $ref: "#/components/schemas/MachineState" } gpu: oneOf: - type: "null" - $ref: "#/components/schemas/GpuRef" size: type: [object, "null"] properties: cpus: { type: integer } memory_gb: { type: number } disk_gb: { type: number } location: type: [object, "null"] description: Where it runs now. properties: city: { type: string } country: { type: string, description: ISO 3166-1 alpha-2 } cpus: type: [integer, "null"] description: Its processors, with 2 GB of memory each; null for a GPU machine, whose size comes with its GPU. enum: [2, 4, 8, 16, null] memory_gb: type: [number, "null"] description: Its memory; null for a GPU machine. sleep_when_idle: type: boolean description: | True: it pauses by itself once it is idle - no call or connection has reached it for `idle_after_s` and its programs have been quiet as long. False: it never pauses by itself. idle_after_s: type: [integer, "null"] description: How long it must be idle before it pauses by itself; null when it never does. labels: type: object additionalProperties: { type: string } secrets: type: array description: The names of the secrets every command run in it gets as environment variables; never their values. items: { type: string } network: { $ref: "#/components/schemas/Network" } files_sealed: type: boolean description: Its files in storage are sealed with the account's own key. running_seconds: { type: number, description: "All the time it has run, which is what is charged." } disk_bytes: { type: integer } cpu_cores: { type: number, description: "Processors it used, about a minute's average, when running." } memory_bytes: { type: integer } created_at: { type: string, format: date-time } last_used_at: { type: [string, "null"], format: date-time } ready_ms: type: number description: How long making it ready took, when this answer made it ready (create, start, copy). GpuRef: type: object required: [id] properties: id: { type: string, description: "An `id` from `GET /v1/gpus`." } model: { type: string } count: { type: integer } memory_gb: { type: integer } MachineCreate: type: object additionalProperties: false properties: name: { type: string, maxLength: 80, description: "For people; ids are made by us." } project: { type: string, description: "Its project; the account's first project when left out." } region: type: string description: "A region `id` from `GET /v1/regions` to run near. Where it runs is said in `location`." gpu: { type: string, description: "A GPU `id` from `GET /v1/gpus`. Needs the `cloud:gpus` scope." } cpus: type: integer enum: [2, 4, 8, 16] default: 2 description: | Its processors, with 2 GB of memory each: 16 is sixteen processors and 32 GB. Charged by the second at the 2-processor price that many times over (16 is 8 times). Not with `gpu`: a GPU machine's size comes with it. labels: type: object maxProperties: 32 additionalProperties: { type: string, maxLength: 200 } secrets: { $ref: "#/components/schemas/SecretNames" } network: allOf: - $ref: "#/components/schemas/NetworkSet" description: | Its internet policy, the outbound gate. Left out, the machine is open - it reaches the internet as every machine always could. A machine made from a snapshot keeps the source machine's policy unless this sets its own. from_snapshot: type: object additionalProperties: false required: [machine] properties: machine: { type: string } snapshot: { type: string, description: "A snapshot's id; its latest when left out." } sleep_when_idle: type: boolean default: true description: | True: it pauses by itself once it is idle - no call or connection has reached it for `idle_after_s` and its programs have been quiet as long (under 5% of one CPU on average), so a job left running keeps it going. Paused, it keeps its memory and no running time is charged. False: it never pauses by itself; it runs, and is charged, until it is paused or stopped. idle_after_s: type: integer minimum: 60 maximum: 86400 default: 300 description: How long it must be idle before it pauses by itself. Only with `sleep_when_idle` true. wait_s: { type: number, minimum: 0, maximum: 600, default: 60 } MachineUpdate: type: object additionalProperties: false properties: name: { type: string, maxLength: 80 } project: { type: string, description: "Another of the account's projects to move it to, one this key reaches." } labels: type: object additionalProperties: { type: [string, "null"], maxLength: 200 } secrets: allOf: - $ref: "#/components/schemas/SecretNames" description: | Every secret it is to get, said whole: the list takes the place of the one it had, and `[]` takes them all away (which needs no `cloud:secrets:write`: it gives nothing). network: allOf: - $ref: "#/components/schemas/NetworkSet" description: | Its internet policy, said whole: it takes the place of the one it had. A running machine has its policy changed on its host at once, without a restart. Network: type: object required: [internet, allow] description: A machine's internet policy - the outbound gate. properties: internet: type: string enum: [open, closed, allowed] description: | `open`: it reaches the internet as every machine always could. `closed`: no outbound connections at all (its own server's model door and DNS aside). `allowed`: only what `allow` lists. allow: type: array description: Where an `allowed` machine may reach; empty for `open` and `closed`. items: { type: string } NetworkSet: type: object additionalProperties: false required: [internet] properties: internet: type: string enum: [open, closed, allowed] allow: type: array maxItems: 64 description: | For `allowed` only (empty or left out otherwise). Each entry is a domain name for HTTP and HTTPS - exact (`api.github.com`) or a single leading `*.` for every name under it (`*.githubusercontent.com`) - or an IPv4/IPv6 range as a CIDR (`140.82.112.0/20`) for any other port. HTTP and HTTPS reach only the listed names (the gate reads the TLS SNI or the HTTP Host; a connection with no SNI or an unlisted one is refused); other ports reach only the listed ranges. items: { type: string, maxLength: 253 } SecretNames: type: array maxItems: 200 uniqueItems: true description: | Names of the account's secrets that every command run in the machine gets as environment variables. Giving them needs the `cloud:secrets:write` scope too, and a key of the whole account; each must be set already. A command can read its environment: any key that may run commands in the machine can read what it was given. items: { type: string, pattern: "^[A-Z_][A-Z0-9_]{0,63}$" } MachineCopy: type: object additionalProperties: false properties: name: { type: string, maxLength: 80 } labels: type: object maxProperties: 32 additionalProperties: { type: string, maxLength: 200 } wait_s: { type: number, minimum: 0, maximum: 600, default: 60 } WaitOptions: type: object additionalProperties: false properties: wait_s: { type: number, minimum: 0, maximum: 600, default: 60 } MachineList: type: object required: [object, data, next] properties: object: { const: list } data: type: array items: { $ref: "#/components/schemas/Machine" } next: { type: [string, "null"] } Snapshot: type: object required: [id, object, machine, created_at] properties: id: { type: string } object: { const: snapshot } machine: { type: string } note: { type: string } created_at: { type: string, format: date-time } SnapshotList: type: object required: [object, data] properties: object: { const: list } data: type: array items: { $ref: "#/components/schemas/Snapshot" } Folder: type: object required: [object, machine, path, entries, truncated] properties: object: { const: folder } machine: { type: string } path: { type: string, examples: ["home/notes"] } entries: type: array items: { $ref: "#/components/schemas/FolderEntry" } truncated: { type: boolean, description: "True when it holds more than 1,000 and the list was cut there." } FolderEntry: type: object required: [name, type, size, modified_at] properties: name: { type: string } type: { type: string, enum: [file, folder, link, other] } size: { type: integer, description: "Bytes; 0 for a folder." } modified_at: { type: [string, "null"], format: date-time } MachineMetrics: type: object required: [object, machine, step_s, cpus, kept_since, points] properties: object: { const: metrics } machine: { type: string } step_s: { type: integer, description: "How long each point covers." } cpus: { type: [number, "null"], description: "Its processors: what cpu_percent is a share of." } kept_since: type: [string, "null"] format: date-time description: The oldest point its server keeps; null when it keeps none. points: type: array items: { $ref: "#/components/schemas/MetricsPoint" } MetricsPoint: type: object required: [at] description: | A step of the machine. A number its server could not read is null (a machine of the microVM kind says its processors and memory alone), and so are the rates of a step its readings cover none of - the first after it woke. properties: at: { type: string, format: date-time, description: "Where the step begins." } cpu_percent: { type: [number, "null"], description: "Its processors' use, a share of its own size, 0 to 100." } memory_bytes: { type: [number, "null"], description: "The most memory it held in the step." } memory_limit_bytes: { type: [number, "null"], description: "The most memory it may hold." } disk_read_bps: { type: [number, "null"], description: "Bytes a second read from its disk." } disk_write_bps: { type: [number, "null"], description: "Bytes a second written to its disk." } net_rx_bps: { type: [number, "null"], description: "Bytes a second received on its network." } net_tx_bps: { type: [number, "null"], description: "Bytes a second sent on its network." } ExecRequest: type: object additionalProperties: false description: Give `command` (the words, no shell) or `shell` (a line for `sh -lc`), not both. properties: command: type: array minItems: 1 maxItems: 4096 items: { type: string } examples: [["python3", "-c", "print(2+2)"]] shell: { type: string, maxLength: 131072, examples: ["ls -la | wc -l"] } env: type: object maxProperties: 256 additionalProperties: { type: string } description: | Environment variables for this command, beside the machine's secrets, which it gets as variables too. Of a name in both, the one given here wins: the secret is left out of this command, and the answer does not say so. The command, its variables and the machine's secrets are at most 1 MiB together (413). cwd: { type: string, description: "Where it starts; the work folder when left out." } user: { type: string, enum: [user, root], default: user } binary_output: { type: boolean, default: false, description: "Give stdout as base64 in `stdout_base64`." } ExecResult: type: object required: [exit_code, stderr, duration_ms, truncated] properties: exit_code: { type: integer } stdout: { type: string } stdout_base64: { type: string } stderr: { type: string } duration_ms: { type: number } truncated: { type: boolean, description: "True when the output was longer than 8 MiB and was cut there." } Port: type: object required: [object, machine, port, public, url, created_at] properties: object: { const: port } machine: { type: string } port: { type: integer, minimum: 1, maximum: 65535 } public: type: boolean description: | True: anyone may reach the address. False: only a call with an API key that can see the machine, and a browser that opened a link made for it. url: type: string format: uri examples: ["https://8080-01jb2xk3n4p5q6r7s8t9v0w1x2.run.wheelmachine.ai"] created_at: { type: string, format: date-time } PortOpen: type: object additionalProperties: false required: [port] properties: port: { type: integer, minimum: 1, maximum: 65535 } public: { type: boolean, default: false } PortList: type: object required: [object, data] properties: object: { const: list } data: type: array items: { $ref: "#/components/schemas/Port" } PortLinkCreate: type: object additionalProperties: false properties: access_s: type: integer minimum: 60 maximum: 3600 default: 1800 description: How long the browser that opens the link may reach the address. path: type: string maxLength: 2048 default: / description: | Where on the address the link lands: a path beginning with a single `/` (not `//` or `/\`), with no space or backslash in it; a query may follow it. PortLink: type: object required: [object, machine, port, url, expires_at, access_s] properties: object: { const: port_link } machine: { type: string } port: { type: integer, minimum: 1, maximum: 65535 } url: { type: string, format: uri, description: "Opens once. Shown in this answer only." } expires_at: { type: string, format: date-time, description: "When the link stops opening, if it was not opened." } access_s: { type: integer, description: "How long the browser that opens it may reach the address." } Secret: type: object required: [object, name, machines, created_at, updated_at] description: A secret as it is shown, always without its value. properties: object: { const: secret } name: { type: string, examples: [DATABASE_URL] } machines: type: array description: The machines given it (those this key can see). items: { type: string } outside: oneOf: - type: "null" - $ref: "#/components/schemas/Outside" description: | Set when the secret is kept outside the machine: its value never enters the machine (its environment holds only a placeholder) and the outbound gate puts it into a request header going to one of the listed hosts. created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time, description: "When its value was last set." } Outside: type: object required: [hosts, header, format] description: How a secret kept outside the machine is sent; never its value. properties: hosts: type: array minItems: 1 maxItems: 32 description: | The hosts its value may be sent to (exact, or a leading `*.` for every name under one). When the machine is not `open`, each must also be allowed by its `network` policy. items: { type: string, maxLength: 253 } header: type: string description: The request header its value is written into, such as `Authorization`. format: type: string description: How the value is written, with `{value}` once; `Bearer {value}`, or `{value}` for the value alone. SecretList: type: object required: [object, data] properties: object: { const: list } data: type: array items: { $ref: "#/components/schemas/Secret" } SecretSet: type: object additionalProperties: false required: [value] properties: value: type: string minLength: 1 maxLength: 32768 writeOnly: true description: The value, up to 32 KiB of text with no NUL in it. It is never shown again. outside: type: object additionalProperties: false required: [hosts, header] description: | Keep the secret outside the machine: its value never enters the machine - not its environment (which holds only a placeholder `wm_secret__`), its files, its process memory or any answer. The outbound gate on the host, which is given the value and the machine is not, puts it into the `header` of requests going to one of `hosts`. Left out, the value reaches the machine's commands as an environment variable. properties: hosts: type: array minItems: 1 maxItems: 32 description: | Where its value may be sent (exact, or a leading `*.`). When the machine is not `open` these must also be allowed by its `network` policy, or the gate could never reach them. items: { type: string, maxLength: 253 } header: type: string maxLength: 64 description: The request header its value is written into, such as `Authorization`. format: type: string maxLength: 256 description: How the value is written, with `{value}` exactly once; `{value}` (the value alone) by default. SSHCertificateRequest: type: object required: [public_key, machine] properties: public_key: { type: string, description: "One line of an OpenSSH public key (ssh-ed25519 AAAA..., or ecdsa/rsa)." } machine: { type: string, description: "The machine's id; the certificate opens that machine alone." } ttl_s: { type: integer, minimum: 1, maximum: 3600, description: "How long it lives, in seconds; one hour at most, and the default." } SSHDoor: type: object required: [object, open, ssh_host, ca_public_key, host_public_key] properties: object: { const: ssh } open: { type: boolean, description: "False until certificates are signed here and the server's address is set." } ssh_host: { type: [string, "null"], description: "The address to reach the SSH server by." } ca_public_key: { type: string, description: "The authority's public key (@cert-authority); empty when none is set." } host_public_key: { type: string, description: "The SSH server's host key, to pin in known_hosts; empty when none is set." } SSHCertificate: type: object required: [object, certificate, principal, ssh_user, expires_at] properties: object: { type: string, enum: [ssh_certificate] } certificate: { type: string, description: "The signed certificate, one OpenSSH line; save it next to the public key." } principal: { type: string, description: "The one principal: the machine's id." } ssh_user: { type: string, description: "The SSH user to connect as (the machine's id)." } expires_at: { type: string, format: date-time } ca_public_key: { type: string, description: "The authority's public key, to trust the certificate (@cert-authority)." } host_public_key: { type: string, description: "The SSH server's host key, to pin in known_hosts." } ssh_host: { type: string, description: "The address to reach the SSH server by, when one is set." } MachineSize: type: object required: [cpus, memory_gb, price_per_hour_usd] properties: cpus: { type: integer, enum: [2, 4, 8, 16] } memory_gb: { type: integer } price_per_hour_usd: { type: number, description: "What an hour of it running comes to." } SizeList: type: object required: [object, data] properties: object: { const: list } data: type: array items: { $ref: "#/components/schemas/MachineSize" } Region: type: object required: [id, city, country] properties: id: { type: string, examples: [mumbai] } city: { type: string } country: { type: string } GpuList: type: object required: [object, data, for_sale] properties: object: { const: list } for_sale: type: boolean description: False while GPU machines are not yet open to every account; the list shows what is coming. data: type: array items: { $ref: "#/components/schemas/GpuOffer" } GpuOffer: type: object required: [id, model, count, memory_gb, price_per_hour_usd, available] properties: id: { type: string } model: { type: string, examples: ["H100 80GB"] } count: { type: integer } memory_gb: { type: integer } cpus: { type: integer } ram_gb: { type: number } disk_gb: { type: number } location: type: object properties: city: { type: string } country: { type: string } price_per_hour_usd: { type: number } interruptible: { type: boolean, description: "May be taken back at any time; cheaper for it." } available: { type: boolean } Usage: type: object required: [object, month, machines, total_usd] properties: object: { const: usage } month: { type: string } total_usd: { type: number } machines: type: array items: type: object required: [machine, running_seconds, usd] properties: machine: { type: string } project: { type: string } running_seconds: { type: number } usd: { type: number } Session: type: object required: [id, object, provider, device, address, created_at, last_seen_at, expires_at, current] properties: id: { type: string, examples: [ses_01jb2xk3n4p5q6r7s8t9v0w1x2] } object: { const: session } provider: { type: string, enum: [github, google] } device: { type: string, description: "The browser it was made in, as a person names it.", examples: ["Firefox on Windows"] } address: { type: string, description: "Where it was made from." } created_at: { type: string, format: date-time } last_seen_at: { type: string, format: date-time } expires_at: { type: string, format: date-time } current: { type: boolean, description: "The sign-in this call is made with." } SessionList: type: object required: [object, data] properties: object: { const: list } data: type: array items: { $ref: "#/components/schemas/Session" } DeviceLoginRequest: type: object additionalProperties: false properties: name: { type: string, maxLength: 80, description: "What the key will be called, such as `wm on laptop`." } scopes: type: array description: What the program asks for; `cloud:*` when left out. The person may give it less. items: { $ref: "#/components/schemas/Scope" } DeviceLoginStarted: type: object required: [object, device_code, user_code, verification_uri, expires_in, expires_at, interval] properties: object: { const: device_login_start } device_code: { type: string, description: "The program's own: never shown, never sent anywhere else." } user_code: { type: string, examples: [BCDF-GHJK], description: "What the person types at verification_uri, from their terminal." } verification_uri: { type: string, examples: ["https://app.wheelmachine.ai/device"] } expires_in: { type: integer, description: "Seconds the codes last: 600." } expires_at: { type: string, format: date-time } interval: { type: integer, description: "Seconds between two asks for the key." } DeviceLoginToken: type: object additionalProperties: false required: [device_code] properties: device_code: { type: string } DeviceLoginPending: type: object required: [object, state, interval, expires_at] properties: object: { const: device_login } state: { const: pending } interval: { type: integer } expires_at: { type: string, format: date-time } DeviceLoginDecision: type: object additionalProperties: false required: [user_code] properties: user_code: { type: string, description: "As the person typed it; case, spaces and dashes do not matter." } scopes: type: array description: Approving only - what the program is given, within what it asked for. items: { $ref: "#/components/schemas/Scope" } DeviceLogin: type: object required: [id, object, name, scopes, state, address, device, created_at, expires_at] properties: id: { type: string } object: { const: device_login } name: { type: string } scopes: type: array description: What it asked for; once decided, what it was given. items: { $ref: "#/components/schemas/Scope" } state: { type: string, enum: [pending, approved, denied, done] } address: { type: string, description: "Where the program asked from: compare it with where you are." } device: { type: string, examples: ["wm 1.0.0 on Linux"] } created_at: { type: string, format: date-time } expires_at: { type: string, format: date-time } Providers: type: object required: [object, github, google] properties: object: { const: providers } github: { type: boolean } google: { type: boolean }