# Glac API ## Pages - [Glac API](https://glac.ai/docs/api/index.md) — This is the reference of the Glac API: every call, what it takes and what it answers. The [CLI](/docs/cli/), the [SDKs](/docs/) and the [MCP server](/docs/mcp/) call it. [Download this description](/docs/api/openapi.json) (OpenAPI 3.1). The calls are arranged by product, as the [docs](/docs/) are: Cube (its calls are `/v1/apps`: an app, which is what a Cube is, its versions and its instances), Sandbox, Drive (with the volumes underneath Drives), and then the platform's own calls: the project, keys, secrets, the account and its bill, events. **Address.** Every path is under `https://api.glac.ai`. **One API in front of many machines.** Sandboxes, and what is in them, live on machines (*nodes*, in the calls' own words). A call about a sandbox, a command, a file, a port's link, a snapshot or a volume is passed by the API to the machine that holds the thing, and a call that makes one to a machine the API picks; a list asks every machine and puts their answers together. What the machine answers comes back unchanged: that is what each of these calls lists. A project's secrets are kept by the API for all its machines, and answered in the same words. The API adds a few answers of its own to these calls, the same for each: [Calls passed to a machine](/docs/api/#calls-passed-to-a-machine). The other calls (apps, projects, keys, spend limits, events, the account and its bill, the console) are the API's own. ## Authentication Every call but the console's sign-up and sign-in (`/v1/auth/…`) takes a credential, in `Authorization: Bearer `. - **A project's key** (`ak_…`). Make one in the console, on its API keys page, with the scopes and the expiry you choose, or with `POST /v1/keys`. The key is shown once, when it is made; `DELETE /v1/keys/{id}` revokes it at once. No key is 401 `missing_api_key`, a wrong one `invalid_api_key`, one past its expiry `expired_api_key`. - **A token for one sandbox** (`st_…`), from `POST /v1/sandboxes/{id}/tokens`: short-lived, with some of the scopes `url`, `exec`, `files` and `tty`. On a `GET` it may be the query parameter `sandbox_token` instead, for a browser, which cannot set headers on an `EventSource` or a WebSocket. The API passes a call made with a token to the machine of its sandbox, which checks the token: to a token every other sandbox is 404, and a call that takes a project's key is 403 `needs_api_key`. - **The console's session.** The console's own calls (`/v1/me…`, `/v1/accounts/{id}…`) take the cookie that `POST /v1/auth/sign-in` sets. With it, and `X-Account` and `X-Project` naming the account and the project, the console calls the rest of the API as a key of that project with every scope, within what the user's role in the account allows. **Scopes.** A key allows some of seven scopes: `read`, `write`, `exec`, `files`, `tty`, `url` and `admin` ([what each one opens](/docs/platform/projects/#api-keys)). A key made without `scopes` has all of them but `admin`. Each call ends its description with the scope it takes ("Takes `read`."), and a key without that scope is 403 `key_scope`, whose message names it. ## Projects An account has one or more projects, and a key belongs to one of them. Everything a call makes with it is that project's, and a call sees only that project's sandboxes, snapshots, volumes, secrets, apps and the rest: another project's are not there (404). Names are unique within a project, for each kind of thing. Ids begin with what they name: `sbx-`, `exe-` (a command), `snap-`, `vol-`, `sshkey-`. Wherever a path, a field or the `sandbox_id` filter takes the id of a sandbox, a snapshot, a volume or an SSH key, it takes its name as well (but in the host name of a link to a port). ## Idempotency keys Any `POST` may carry `Idempotency-Key` (1 to 255 characters). The same call again, with the same key and the same body, is answered as the first one was, with `Idempotent-Replayed: true`, and nothing is done twice. The same key with another body is 409 `idempotency_key_reused`; while the first call still runs, a second one waits up to 60 seconds, then is 409 `idempotency_in_progress`. An answer is kept for 24 hours; answers of 409 and of 5xx, and event streams, are not kept, and the call runs again. A command's key (`…/run`, `…/exec`) is kept with the command, for as long as the sandbox has it. A create sent again with its key goes to the machine the first one went to, and is given the size the first one was given, whatever the account's plan is by then. ## Pagination A list takes `limit` (1 to 1000; 100 when not given) and `cursor`, and answers `{"data": […], "next_cursor": "…"}`, newest first (the entries of a directory, and secrets, by name). `next_cursor` is null on the last page; give it as `cursor` for the next one. A cursor names a position, not a page: it stays good while things are made and deleted. ## Errors Every error has one shape: ```json {"error": {"type": "not_found", "code": "sandbox_not_found", "message": "no sandbox sbx-…", "param": null, "retryable": false, "request_id": "req-…", "docs_url": null}} ``` `type` sets the status: `invalid_request` 400, `unauthenticated` 401, `payment_required` 402, `forbidden` 403, `not_found` 404, `conflict` 409, `too_large` 413, `too_many_requests` 429, `internal` 500, `bad_gateway` 502, `unavailable` 503, `timeout` 504. `code` says which failure it is: each call lists its codes under each of its statuses. `message` says it in words for people; `param` names the field or the query parameter at fault; `retryable` says whether the same call again may succeed, and a `Retry-After` header says when, where that is known. A field or a query parameter the call does not take is refused (400 `unknown_field`, `unknown_parameter`), and the message lists what it takes. `request_id` is the call's id, in the answer's `X-Request-Id` header too: send an `X-Request-Id` of your own (1 to 64 letters, digits, `.`, `_` and `-`) and it is used. A 500 says only that something failed: quote its `request_id`. `docs_url`, where an error has one, leads to its code on the [errors](/docs/errors/) page, which says what to do about the refusals that send you there: a plan's cap, a project's Drives and volumes, an account on hold, a locked user. ## Event streams and terminals A command's output can be read as a stream of events (`text/event-stream`): from `POST /v1/run` and `POST /v1/sandboxes/{id}/run` with `stream: true` (or `Accept: text/event-stream`), and from `GET /v1/sandboxes/{id}/exec/{exec_id}/output`. Each event has an `event:` line, for output an `id:` line (the position after the event's last byte, stdout and stderr counted together) and one `data:` line of JSON. A comment, `: waiting`, comes every 15 seconds while nothing happens. A reader that falls behind what the sandbox still keeps is told so with `gap`. To read on after a lost connection, ask `…/exec/{exec_id}/output` with `Last-Event-ID` (or `from`). The events: - `start`: First event of a stream that started the command. Its data: `{id, "object": "exec", sandbox_id, command}`. - `stdout`: A piece of standard output. `id` is the position after its last byte. Its data: `{text}` or `{base64}`. - `stderr`: A piece of standard error. `id` is the position after its last byte. Its data: `{text}` or `{base64}`. - `gap`: Output is no longer kept: what the reader asked for, or what it was about to get. The stream goes on from `available_from`. Its data: `{from, available_from}`. - `exit`: Last event: the command has ended. `id` is the position after the last byte of its output. Its data: `{id, "object": "exec", sandbox_id, command, tty, status, …}` (as `GET /v1/sandboxes/{id}/exec/{exec_id}` answers it). - `end`: Last event of a stream asked for with `follow=false` while the command is still running. `id` is the position reached. Its data: `{"status": "running", offset}`. - `error`: Last event: the command was lost (the sandbox was restarted from its disk, restored or deleted), or the sandbox was moved to another node with its processes, where the command goes on: `sandbox_moved` (retryable), with `to`, `epoch` and `offset`, the id of the event before it; the same stream asked for again through the control plane with `Last-Event-ID` (or `from`) set to `offset` goes on there, missing nothing and repeating nothing. Its data is the error object of every other error. Its data: `{error, …}`, [the error](/docs/api/#errors). A terminal (`GET /v1/sandboxes/{id}/exec/{exec_id}/tty`) is a WebSocket. Binary messages are bytes: what is typed one way, what the terminal shows the other. Text messages are JSON: - `{"type": "eof"}`: Client to server, as a text message: the client sends no more. The guest half-closes its connection to the peer; what the peer still sends comes back. - `{"type": "listening", path}`: Server to client, the first message: where the socket is in the sandbox. - `{"type": "reset"}`: Server to client: every connection to the socket is gone (the sandbox went to sleep); the socket stays. - `{"type": "resize", cols, rows}`: Client to server, as a text message: the terminal has this size now. Out-of-range sizes are dropped without an answer. - `{"type": "signal", …}`: Client to server, as a text message: send the command's process group a signal. - `{"type": "exit", status, exit_code, signal}`: Server to client, as a text message: the command has ended. A close frame (1000) follows. - `{"type": "error", error, …}`: Server to client, as a text message: the command was lost (the sandbox was restarted, restored or deleted), or the sandbox was moved to another node with its processes, where the command goes on (`sandbox_moved`, retryable, with `to`, `epoch` and `offset`: join again through the control plane with `from` set to `offset`), or a key or a message of the client's was refused (`spend_limit_reached`, say). `error` is the error object of every other error. A close frame follows: 1012 with the reason `sandbox_moved` after `sandbox_moved`, 1000 otherwise. A key typed while the sandbox is being moved waits for the move's end: it goes in where the move failed. - `{"type": "gap", from, available_from}`: Server to client, as a text message: the client fell further behind than the sandbox keeps of a command's output (the last 16 MiB), and what the terminal shows next begins at `available_from`, not where it stood. Sent before what follows it, as a stream's `gap` event is. The bytes a terminal shows carry no position: a client that counts them, to join again with `from` after its connection was lost (the SSH gateway, whose sessions outlive a restart of the node's daemon), goes on counting from `available_from`. ## Calls passed to a machine What the machine that holds a thing answers is what each of these calls lists. The API answers some of them itself, before any machine is asked or in place of the machine's answer: - **400** `invalid_json`: Of `POST /v1/sandboxes`, `/v1/run` and `/v1/volumes`: the body is not a JSON object (no body is `{}`). Refused before any machine is asked, whether or not one is there. - **400** `unknown_field`: A create that gives `project_address`: the address of a sandbox on its project's network is the API's to give. Of a sandbox's `drives`, a field a mount does not have. A create or a change of a sandbox that is no Browser session that gives it a `proxy`: a proxy is a session's (`POST /v1/browsers`, `PATCH /v1/browsers/{id}`). - **400** `unknown_region`: A create whose `region` is not one of the regions. - **400** `invalid_field`: A volume made from a backup (`from_backup`) that also gives `size_bytes`, `shared` or `from_backup_index` (it has the size and the kind of the volume the backup was taken of), or a `region` other than the backup's. Of a sandbox's `drives`: no list, a `path` that is not absolute, a `mode` other than `read_write` (the read-only mount of a Drive's snapshot comes later). - **400** `missing_field`: A sandbox's create whose `drives` names a Drive without its `drive` or its `path` (`param` `drives[i].drive`, `drives[i].path`). - **400** `reserved_metadata`: A sandbox's create (`POST /v1/sandboxes`, `/v1/run`) or change (`PATCH /v1/sandboxes/{id}`) whose `metadata` names the key `product` (`param` `metadata.product`): what a sandbox is sold as (a Browser session) is the API's own record, and no metadata says it. - **400** `unknown_parameter`: A list that no machine answers (none is there, or none can be reached), which the API answers from what it last saw, given a query parameter the list does not take. - **402** `account_in_debt`: The account owes for what it stores past its plan. Every call is refused, before the key's scope is looked at, but reading the account, its bill, its usage and its backups, paying, making a volume from a backup, and (unless the API is run otherwise) listing, reading and deleting the sandboxes, snapshots and volumes it stores. The message says how much is owed and how to pay. - **403** `key_scope`: The key does not allow the scope the call takes; the message names it. - **403** `account_held`: The account is held for its payments: every call of its keys and of its sandboxes' tokens but `GET /v1/account`. The message says to write to support. - **403** `spend_limit_reached`: The project, or the sandbox, is at its spend limit: no sandbox, fork, one-shot run, volume (but one made from a backup) or snapshot is made, and a sandbox the limit froze is not woken. - **403** `balance_used_up`: The CPU time or the memory the account's plan includes is used up, and the account has no balance to pay for more: the same calls are refused. - **403** `plan_size_exceeded`: Larger than the account's plan lets a sandbox be: the `limits` asked for (`param` `limits.vcpus`, `limits.memory_bytes`, or `limits` for both), or the shape of the snapshot or template it is made from, forked from or rolled back to. A sandbox that names no size is made as large as the plan allows at most. - **403** `plan_cap_reached`: More than the account may have at once (`param` names which; `GET /v1/account` has them under `caps`): sandboxes awake, their vCPUs or their memory (of a create, a one-shot run, a fork, or a call that wakes a sandbox), sandboxes asleep, or sandboxes made in the last minute or hour (these are `retryable`, with `Retry-After`). A sleep and a delete are never refused. - **403** `volume_limit_reached`: Of `POST /v1/volumes`: one volume more, a larger one, or more in all than the plan lets a project keep (`GET /v1/billing` has the numbers). - **403** `drive_limit_reached`: Of a sandbox's snapshot that carries Drives: a sandbox made from it (`POST /v1/sandboxes` and `POST /v1/run` with `snapshot`), each copy of a fork of a sandbox that has Drives, and a sandbox taken back to it get a new Drive of each, and are refused before any machine is asked where those would take the project past the Drives its plan lets it keep (how many, how large each, how large together), as a fork through `POST /v1/drives/{id}/fork` is. `param` is `snapshot` of a create and a roll-back; the message says the plan's numbers, what the project has and what this would make. - **403** `secret_limit_reached`: Of `PUT /v1/secrets/{name}`: a secret more than the plan lets a project keep, a value larger than one may be (`param` `value`), or one that would give an app's instances more of the project's secrets than one sandbox may be given. - **403** `proxy_not_on_plan`: A change of a Browser session (`PATCH /v1/sandboxes/{id}`) that gives it a `proxy` on the free plan, which has none. - **403** `account_share_reached`: Something new of the account (a sandbox, a one-shot run, a fork's copies, a snapshot, a volume or Drive) would take it past its share of the data drive of the machine it would be on: one account holds at most a share of a machine's drive (a quarter unless the machine is set otherwise). The API puts what is new on a machine where the account has the room, and passes this on when none has. - **404** `backup_not_found`: A volume made from a backup that the project does not have (`param` `from_backup`). - **404** `drive_not_found`: A sandbox's create whose `drives` names a Drive the project does not have (`param` `drives[i].drive`). - **404** `snapshot_not_found`: A sandbox's create whose `drives` mounts a snapshot of a Drive the project does not have (`param` `drives[i].snapshot`). - **409** `not_on_one_machine`: A sandbox and a volume it is to have, or a shared volume that the snapshot it is made from or rolled back to mounts, are on different machines. A volume no sandbox has had yet moves to the sandbox's machine instead. Of a sandbox's `drives`: the Drives it mounts, and the snapshots of Drives it mounts read-only, are of more than one region, or of another region than the create names (`param` `region`); a snapshot is read in the region its Drive was in when it was taken, whether or not the Drive was deleted since. - **409** `snapshot_not_replicated`: A sandbox is to be made from, or rolled back to, a snapshot on another machine than the one that took it (that one is away, or the volumes are elsewhere), and the snapshot is not in the object store yet: its `last_replication_status` says how its copy stands. `param` is `snapshot`. - **409** `not_loadable`: The machine the call goes to cannot make machines from the snapshot as it was published (another build of Firecracker, other settings of the devices, a larger shape, another size of disk); the message says what differs. - **409** `backup_not_ready`, `backup_unusable`: A volume made from a backup that is still being made, or that failed. - **409** `too_many_drives`, `key_drive`, `duplicate_volume`, `drive_attached`: Of a sandbox's create with `drives`: more than 8 Drives and volumes together (`too_many_drives`); a keyed instance's Drive (`key_drive`); one Drive named twice (`duplicate_volume`); a Drive another sandbox has, which stays where it is (`drive_attached`). - **409** `not_copied`, `store_unfenced`: Of a sandbox's create with `drives`: a Drive on another machine than the one the sandbox can be made on, where that machine copies nothing to an object store (`not_copied`), or copies to one that no move goes through (`store_unfenced`). - **409** `managed_by_database`: A sandbox's create (`POST /v1/sandboxes`, `/v1/run`), or a change of its mounts (`PATCH /v1/sandboxes/{id}` with `volumes`), that mounts a database's Drive read-write (`param` names it); a delete, a change or a restore of a sandbox that is a database's instance; any change of a volume that is a database's Drive. What a database is made of is changed through `/v1/databases` alone. - **409** `managed_by_installation`: The same of what an installation of a Managed Application is made of: a sandbox's create, or a change of its mounts, that mounts an installation's Drive read-write; a delete, a change or a restore of a sandbox that is an instance of an installation's app; any change of a volume that is an installation's Drive. What an installation is made of is changed through `/v1/installations` alone. - **409** `checkpoints_unavailable`: A sandbox with `checkpoint` on (its own setting, or a keyed instance's from its app) where no machine takes checkpoints, or where what it is made from is on a machine that does not; and a `PATCH` that turns them on, on a machine that does not. `param` is `checkpoint`. While every machine that takes them is away or full, the create is 503 `no_capacity` instead, `retryable`. - **502** `fetch_failed`: A sandbox made from, or rolled back to, a snapshot on another machine than the one that took it: that machine could not bring the snapshot from the object store. The message has its words. - **503** `node_unavailable`: The machine that holds the thing cannot be reached, or does not have the project's latest secrets yet: nothing is lost, and the same call will do once it is back (`retryable`). - **503** `no_capacity`: No machine has room for a new sandbox or volume (of a volume made from a backup: no machine of the backup's region makes them). - **503** `moving`: The sandbox is moving to another machine: every call for it until the move is over (`retryable`, `Retry-After: 1`). - **503** `upgrading`: The sandbox is a keyed instance its machine is moving over to another version of its app (`POST /v1/apps/{app}/instances/{key}/upgrade`): every call for it until the new version is up, or the instance is put back (`retryable`, `Retry-After: 1`). - **503** `creation_cancelled`, `creation_cleanup_failed`: The machine made the thing and the API could not record it: what the call made was deleted again (`creation_cancelled`), or not all of it could be (`creation_cleanup_failed`: the message lists what is left, which is billed until it is deleted). - **503** `creation_replay_incomplete`, `creation_state_unavailable`, `creation_replay_record_failed`: A create sent again with its `Idempotency-Key`, whose first answer the machine gives again: what that answer named is no longer all there (`creation_replay_incomplete`; another key makes it anew), the machine could not be asked (`creation_state_unavailable`, `retryable`), or it is all there and the API could not record it (`creation_replay_record_failed`). - **503** `copy_behind`, `drive_not_brought`, `not_served_here`, `unavailable`: Of a sandbox's `drives`, each brought first to the machine the sandbox is made on: the object store does not hold the Drive as it is yet (`copy_behind`); its machine did not let it go, or the other could not take it (`drive_not_brought`); no machine of the region that could take it keeps Drives (`not_served_here`); another call is bringing it (`unavailable`). Each is `retryable` but `not_served_here`. - **503** `standby`: This copy of the API is not the one that serves: the answer names the one that does (`active`). `retryable`, with `Retry-After`. A sandbox, snapshot or volume whose machine cannot be reached is still listed and read, as the API last saw it, with `status` `unavailable` (of a sandbox, `awake_because` and `sleep_tier` are null). The creates take `region` (`na`, `eu`, `ap`), which the API reads itself and does not pass on; a sandbox's and a volume's `region` says where it is. ## Sections - [Account](https://glac.ai/docs/api/account/llms.txt) - [Accounts](https://glac.ai/docs/api/accounts/llms.txt) - [Apps](https://glac.ai/docs/api/apps/llms.txt) - [Audit](https://glac.ai/docs/api/audit/llms.txt) - [Auth](https://glac.ai/docs/api/auth/llms.txt) - [Backups](https://glac.ai/docs/api/backups/llms.txt) - [Billing](https://glac.ai/docs/api/billing/llms.txt) - [Browsers](https://glac.ai/docs/api/browsers/llms.txt) - [Catalogue](https://glac.ai/docs/api/catalogue/llms.txt) - [Compose](https://glac.ai/docs/api/compose/llms.txt) - [Databases](https://glac.ai/docs/api/databases/llms.txt) - [Deploys](https://glac.ai/docs/api/deploys/llms.txt) - [Destinations](https://glac.ai/docs/api/destinations/llms.txt) - [Domains](https://glac.ai/docs/api/domains/llms.txt) - [Drive Snapshots](https://glac.ai/docs/api/drive-snapshots/llms.txt) - [Drives](https://glac.ai/docs/api/drives/llms.txt) - [Events](https://glac.ai/docs/api/events/llms.txt) - [Installations](https://glac.ai/docs/api/installations/llms.txt) - [Keys](https://glac.ai/docs/api/keys/llms.txt) - [Limits](https://glac.ai/docs/api/limits/llms.txt) - [Managed](https://glac.ai/docs/api/managed/llms.txt) - [Me](https://glac.ai/docs/api/me/llms.txt) - [Project](https://glac.ai/docs/api/project/llms.txt) - [Run](https://glac.ai/docs/api/run/llms.txt) - [Sandboxes](https://glac.ai/docs/api/sandboxes/llms.txt) - [Secrets](https://glac.ai/docs/api/secrets/llms.txt) - [Snapshots](https://glac.ai/docs/api/snapshots/llms.txt) - [Ssh Keys](https://glac.ai/docs/api/ssh-keys/llms.txt) - [Tags](https://glac.ai/docs/api/tags/llms.txt) - [Usage](https://glac.ai/docs/api/usage/llms.txt) - [Variables](https://glac.ai/docs/api/variables/llms.txt) - [Volumes](https://glac.ai/docs/api/volumes/llms.txt) - [Webhook Endpoints](https://glac.ai/docs/api/webhook-endpoints/llms.txt) - [Webhooks](https://glac.ai/docs/api/webhooks/llms.txt)