Skip to content
DocsAll docs
Console

Environments API

Lists the API routes that create, change, run, copy, and reach an environment and its services.

Last updated

On this page

The environment document

Every route that returns an environment returns this object. A field marked "omitted" is absent when it has no value. Field names are snake_case, except inside services[].resources, intercepts, and service_status, which are camelCase.

json
{  "id": "env-1f2e3d4c5b6a7988",  "owner": "acme",  "name": "acme-dev",  "region": "centralindia",  "state": "running",  "placement": "node-1",  "volume": "env-1f2e3d4c5b6a7988",  "services": [    {      "name": "db",      "image": "postgres:16@sha256:5b1c…",      "command": [],      "env": { "POSTGRES_PASSWORD": "dev" },      "mounts": [{ "folder": "pgdata", "path": "/var/lib/postgresql/data" }],      "ports": [5432]    },    {      "name": "api",      "image": "cr.khost.dev/acme/api:1@sha256:9d2e…",      "command": ["node", "server.js"],      "env": { "DATABASE_URL": "postgres://postgres:dev@db:5432/postgres" },      "mounts": [],      "ports": [8080],      "resources": { "cpuRequest": "500m", "cpuLimit": "1", "memoryRequest": "1Gi", "memoryLimit": "2Gi" }    }  ],  "intercepts": [{ "service": "api", "workspace": "ws-0a1b2c3d4e5f6a7b", "ports": [{ "service": 8080, "workspace": 3000 }], "desk": "desk-9c8b7a6d5e4f3a2b" }],  "service_status": [    { "name": "db", "ready": true, "proxy": "none" },    { "name": "api", "ready": true, "interceptedBy": "ws-0a1b2c3d4e5f6a7b", "proxy": "ready" }  ],  "ttl_secs": 604800,  "expires_at": "2026-10-25T09:30:00Z"}
FieldTypeMeaning
idstringenv- and 16 hex digits
ownerstringthe person or team that owns it
namestringthe name you gave it
regionstringthe owner's region
statestringcreating, running, stopped, error, or deleted. Any other phase the node reports (starting, idle, unavailable) reads as creating
placementstring or nullthe node that holds it; null until a node claims it
volumestring or nullthe environment id once it has a snapshot, else null. Create, clone, and restore responses always carry null
servicesService[]the service list as stored; see Service. Images carry the digest they were pinned to
restored_tostring, omittedthe snapshot the disk last landed on, after an in-place restore. Only GET /v1/environments/{id} and …/watch fill it
restore_requested_atstring, omittedRFC 3339; when that restore was asked for. Same two routes only
restoringstring, omittedDraining while the services stop for a restore, Restoring while the disk is swapped; absent otherwise
degradedcondition, omittedpresent with reason StorageUnavailable while its volume is read-only and its host remounts it
decommissioningcondition, omittedpresent with reason NodeLeaving while its node is being retired
restart_requiredcondition, omittedpresent with reason EnvChanged while a running service does not have its current env vars. The message names the services
restart_on_env_changeboolean, omittedtrue when an env change restarts the service
placedcondition, omittedpresent only when no node in the region has room for it (NoCapacity)
idle_sincestring, omittedRFC 3339; set while it is idle-stopped and not woken since
interceptsobject[]the intercepts asked for (the wish): service, workspace (an id), ports, and desk when a desk holds it
service_statusobject[], omittedwhat the node observed per service: name, ready, message (omitted), interceptedBy (the workspace whose process really receives the traffic, omitted), proxy, unreachableSince (Unix seconds, omitted), restartRequired (omitted; the sentence that names the changed env vars, never the values). proxy is starting, ready, failed, or none for a service nobody intercepts. Absent until a node has reported
ttl_secsinteger, omittedunused seconds before it is deleted
expires_atstring, omittedRFC 3339; last used (else created) plus ttl_secs

A condition is { "ready": bool, "reason": string, "message": string }.

intercepts is what was asked for and service_status[].interceptedBy is what is in force. A stopped intercepting workspace leaves the wish in place while the real service answers.

Service

FieldTypeRequiredRule
namestringyes1 to 40 lowercase letters, digits, or -; starts with a letter, ends with a letter or digit, no --. Unique in the environment
imagestringyesPinned to its digest when stored (repo:tag@sha256:…) if the registry can be reached; an image already carrying @ is kept as written
commandstring[]yes[] keeps the image's own
envobjectyesname to value; a name matches [A-Za-z_][A-Za-z0-9_]*. {} for none. A value of exactly ${{ secrets.NAME }} is a secret of the environment's owner, handed to the service as a variable and never shown in the document
mountsobject[]yes[] for none; see below
portsinteger[]no1 to 65535. Published as a cluster address under the service name. Absent means it exposes nothing
resourcesobjectnocpuRequest, cpuLimit, memoryRequest, memoryLimit, quantity strings such as 500m or 2Gi. Absent is the platform's default unit

A mount is { "folder": string, "path": string } (volume is accepted for folder). folder is one name of letters, digits, ., _, and - (not . or ..) inside the environment's own disk; path is absolute with no :. A service that mounts a folder keeps its data across restarts; one with no mount loses what it wrote.

A bad service is 400 with a plain-text sentence naming the service or field: service name "Db" must be 1-40 lowercase letters, digits or '-', start with a letter, end with a letter or digit, no '--', service "db": port must be 1-65535, service "db": env name "FOO-BAR" must match [A-Za-z_][A-Za-z0-9_]*, a mount folder or path rule, or duplicate service name "db".

POST /v1/environments

Creates an environment and returns it in creating. The platform places it and starts the services; poll GET /v1/environments/{id} until state is running.

FieldTypeRequiredRule
namestringyes1 to 40 lowercase letters, digits, or -; starts with a letter, ends with a letter or digit, no --. Not ws, env, api, admin, system, kd, or bld-…, and not shaped like an id. Unique among that owner's environments
servicesService[]noSee Service. Absent is the same as []. Without snapshot_id an empty list creates an environment with no services: it is charged one environments slot and its disk, and nothing runs until PATCH /v1/environments/{id} adds some. With snapshot_id, an empty or absent list means the snapshot's own
ownerstringnoA team you belong to. Absent or your own handle means a personal environment. With snapshot_id, the snapshot must be on a volume of this owner
ttl_secsintegerno600 to 2592000. Absent means never
snapshot_idstringnoA reference (a volume name, name@N, or a snapshot id). Grow the environment from a ready snapshot of yours, including one whose environment is gone. See the snapshot form
json
{  "name": "acme-dev",  "owner": "acme",  "ttl_secs": 604800,  "services": [    { "name": "db", "image": "postgres:16", "command": [], "env": { "POSTGRES_PASSWORD": "dev" }, "mounts": [{ "folder": "pgdata", "path": "/var/lib/postgresql/data" }], "ports": [5432] }  ]}

Response: 202, the environment document in creating, with volume null and placement null.

Errors:

  • 400 — a service breaks its rule (see Service).
  • 403 — not a member of that team; or your access to the team is paused.
  • 404 — no such team (JSON).
  • 409 — an environment of that name exists for that owner (an environment named "acme-dev" already exists here); over quota (environments, diskGb, cpu, or memoryGb); the team has no region yet (JSON).
  • 422 — the name breaks the rule (JSON, the rule as the message); ttl_secs must be 600..=2592000 seconds. image not found in its registry: … when a registry answers that an image does not exist. A registry that is down or private leaves the tag as written, and the API does not refuse it. A service env value that references a secret the owner does not have is `DB_URL` is not a secret of acme (plain text).
  • 503 — team lookup not configured on this node; or the API cannot bind the team's region (JSON).

The snapshot form

With snapshot_id, the call is the same as POST /v1/environments/{id}/clone with a snapshot, except that the snapshot need not belong to an existing environment. Its extra errors:

  • 400 — this snapshot was cut from a workspace; use POST /v1/workspaces/restore; an environment needs at least one service; this snapshot froze none, so pass `services`.
  • 404 — the snapshot does not exist, is not ready, or is not on a volume of owner (absent: you). A volume never clones into another owner.

GET /v1/environments

Your environments, as an array of documents. ?owner={slug} lists one owner's (you or a team you belong to); absent lists yours and every team's you are in. restored_to and restore_requested_at are never filled here.

Response: 200.

json
[  { "id": "env-1f2e3d4c5b6a7988", "owner": "acme", "name": "acme-dev", "region": "centralindia", "state": "stopped", "placement": "node-1", "volume": "env-1f2e3d4c5b6a7988", "services": [], "intercepts": [] }]

Errors:

  • 403 — ?owner names a team where your access is paused; or a desk tool token asking outside its own team.
  • 404 — ?owner names an owner that you cannot act for.

GET /v1/environments/{id}

One environment. {id} is the id or a name; qualify a name with ?owner={team} (see the overview). The answer carries a strong ETag.

Response: 200, the environment document, with restored_to and restore_requested_at when an in-place restore has put the disk on a snapshot.

Errors:

  • 404 — not found: no such environment or not yours to see.
  • 409 — the name matches more than one environment; JSON with matches (see the overview).

GET /v1/environments/{id}/watch

The same document as the plain read, as a long poll. Send the ETag you last saw as If-None-Match. With no tag, or a stale one, the answer is immediate. With the current one, the call is held until the environment changes, or about 50 seconds, and then you ask again.

Response: 200, the environment document with a new ETag; or 304 with an empty body and the unchanged ETag when the hold ran out.

Errors: the same as the plain read. An environment deleted during the hold answers 404.

PATCH /v1/environments/{id}

Replaces the whole service list. The platform starts added services and removes the others. The data folders of a removed service stay on the disk. A changed env does not restart a running service. The service shows restartRequired until it restarts.

FieldTypeRequiredRule
servicesService[]yesThe whole new list. See Service. The handler does not refuse an empty list
restartOnEnvChangebooleannotrue restarts a service when its env vars change, false only marks it. Default false; absent keeps the current value
json
{ "services": [  { "name": "db", "image": "postgres:16", "command": [], "env": { "POSTGRES_PASSWORD": "dev" }, "mounts": [{ "folder": "pgdata", "path": "/var/lib/postgresql/data" }], "ports": [5432] },  { "name": "cache", "image": "redis:7", "command": [], "env": {}, "mounts": [], "ports": [6379] }] }

Response: 200, the environment document with the new services.

Only added services are charged, and only their cpu and memory.

Errors:

  • 400 — a service breaks its rule.
  • 404 — not found, or not yours.
  • 409 — the new list drops a service that an intercept names (api is intercepted by ws-0a1b2c3d4e5f6a7b; release the intercept before removing the service); over the cpu or memoryGb limit.
  • 422 — image not found in its registry: … for an image that is new in this list. A secret reference the owner does not have is `DB_URL` is not a secret of acme (plain text).

POST /v1/environments/{id}/start

Runs a stopped environment, or wakes an idle-stopped one. Takes no body. The call only asks; poll the document for state.

Response: 202, the environment document as it was read before the call, so state still shows stopped.

Errors:

  • 404 — not found, or not yours.
  • 409 — the owner is over its disk limit (a start resumes filling).

POST /v1/environments/{id}/stop

Stops the services, then syncs the disk. Takes no body. An explicit stop stays stopped until you start it.

Response: 202, the environment document with state stopped.

Errors:

  • 404 — not found, or not yours.

POST /v1/environments/{id}/services/{service}/restart

Restarts one service so that it gets the current env vars. Takes no body. A changed env var or a rotated secret does not restart a service. It sets restartRequired on the service in service_status and restart_required on the document. Use this route to apply the change, or set restartOnEnvChange with PATCH /v1/environments/{id}.

Response: 202, the environment document.

Errors:

  • 404 — not found, not yours, or no service of that name.
  • 409 — environment is not running; its next start reads the current env.

GET /v1/environments/{id}/services/{service}/logs

Reads the log of one service. Query: tail (lines, default 200, at most 2000), since (seconds, or 15m, 1h; at most 24 h), previous (the container before the last restart). Secret-shaped text is masked.

Response: 200, {"lines": ["…"], "pod": "api-0", "truncated": false}. truncated is true when the answer was cut to the limits.

Errors:

  • 404 — not found, not yours, no service of that name, or the service is not running (stopped or intercepted).
  • 503 — log unavailable.

DELETE /v1/environments/{id}

Deletes the environment. Every desk connected to it is disconnected, not ended. A volume that still has snapshots survives, detached, and can be restored from. Takes no body.

Response: 202, the document with state deleted and volume null. warning is present when the API cannot disconnect a connected desk (some desks may still name this deleted environment) or cannot list the desks.

json
{  "id": "env-1f2e3d4c5b6a7988",  "owner": "acme",  "name": "acme-dev",  "region": "centralindia",  "state": "deleted",  "placement": "node-1",  "volume": null,  "services": [],  "intercepts": []}

Errors:

  • 403 — a pipeline token deleting an environment its own run did not clone (a pipeline deletes only the environment clones its own run made).
  • 404 — not found, or not yours.

PUT /v1/environments/{id}/ttl

Sets or clears how long the environment can stay unused before it is deleted. An environment with any desk connected, awake or idle, is in use and never deleted by its time to live.

FieldTypeRequiredRule
ttl_secsinteger or nullno600 to 2592000 to set; null or an absent key clears it
json
{ "ttl_secs": 86400 }

Response: 200, the environment document with ttl_secs and expires_at.

Errors:

  • 404 — not found, or not yours.
  • 422 — ttl_secs must be 600..=2592000 seconds.

POST /v1/environments/{id}/clone

Makes a new environment from this one. Without snapshot_id it copies the live disk and the service list as they are now. With snapshot_id it creates the new environment from one of the snapshots of this environment. The copy joins no desk and carries no intercepts.

FieldTypeRequiredRule
namestringyesThe same rule as create; unique among the new owner's environments
ttl_secsintegerno600 to 2592000. The copy has its own time to live; the source's is never inherited
snapshot_idstringnoA reference (a volume name, name@N, or a snapshot id) to a ready snapshot of this environment's own volume. Without it, owner and services are refused
ownerstringwith snapshot_idA team you belong to; absent means the source's owner. The snapshot must be on a volume of this owner, else 404
servicesService[]with snapshot_idOverrides the frozen list. An empty or absent list means the snapshot's own; a snapshot that froze none then needs one
json
{ "name": "acme-dev-2", "ttl_secs": 3600 }

Response: 202, the environment document of the new environment in creating. It also carries based_on: snapshot, at, age_seconds (always 0), and interrupted (always false). A live clone makes a new cut of the source.

Using a snapshot this way moves its expiry to a full default life from now when that is later. See how long a snapshot lives.

Errors:

  • 400 — owner and services apply only with snapshot_id; plus the snapshot form errors.
  • 403 — your access to the owner is paused.
  • 404 — the source is not found or not yours. You cannot spend the owner's quota. The snapshot is not found, not ready, not on this environment's volume, or not yours.
  • 409 — not ready yet: no volume for this workspace (the source has never been placed); the storage pool is full; retry later or ask an operator; the name is taken; over quota.
  • 422 — the name breaks the rule (JSON); ttl_secs must be 600..=2592000 seconds.

POST /v1/environments/{id}/snapshots

Pushes a snapshot of the environment: its disk and the service list, frozen together. A snapshot expires on its own schedule; see how long a snapshot lives. The first push of a volume must name it. See Names and versions.

FieldTypeRequiredRule
messagestringnoA note shown in the lineage
ttl_secsintegerno3600 to 31536000. Absent means the region's default (30 days unless an admin changed it)
neverbooleannotrue keeps it with no expiry. Exclusive with ttl_secs
volumestringon the first pushThe volume name: 1 to 40 characters of a-z, 0-9, and -; starts with a letter, ends with a letter or digit. After the first push it is optional, and when given it must equal the name
json
{ "message": "before the schema change", "never": true, "volume": "stack-data" }

Response: 202. The snapshot is working until the platform has cut it; read it back from the lineage of the volume.

json
{ "id": "env-1f2e3d4c5b6a7988-8a7b6c91", "phase": "working", "volume": "env-1f2e3d4c5b6a7988", "expires_at": null, "name": "stack-data", "version": 1 }

expires_at is null when never was set. volume is the volume id. name and version are the volume name and the number of this push.

Errors:

  • 400 — invalid snapshot body; or ttl_secs and never are exclusive.
  • 403 — your access to the owner is paused.
  • 404 — not found, or not yours; the volume no longer exists.
  • 409 — not ready yet: no volume for this workspace (the environment has no volume yet); a snapshot is already being cut for this workspace; over the snapshots or disk limit; volume {name} already exists; delete it first; this volume is named {a}, not {b}; the version counter of {name} is busy; retry.
  • 422 — ttl_secs must be between 3600 and 31536000; volume: this volume has no name yet; name it on its first push; volume {name}: … when the name breaks the rule.

POST /v1/environments/{id}/restore

Puts this environment back to one of its own snapshots, in place: the services and the data both. The call writes the wish and answers. The platform drains the services, swaps the disk, and starts the services of the snapshot (watch restoring, then restored_to, on the document). The disk size becomes the size the snapshot froze.

A service that keeps data now and that the snapshot does not have loses that data. The restore refuses until you list every such service in discard.

FieldTypeRequiredRule
snapshot_idstringyesA reference (a volume name, name@N, or a snapshot id) to a ready snapshot on this environment's own volume
discardstring[]noService names you agree to lose the data of. Must name exactly the services the restore would lose it for
json
{ "snapshot_id": "env-1f2e3d4c5b6a7988-8a7b6c91", "discard": ["redis"] }

Response: 202, what the restore will do to the service list, by name.

json
{  "snapshot_id": "env-1f2e3d4c5b6a7988-8a7b6c91",  "rolled_back": ["db"],  "discarded": ["redis"],  "added": [],  "removed": ["redis"]}

rolled_back are services whose data folders are all in the snapshot. discarded are the services you named. added and removed are services that arrive or leave with the snapshot's list.

Using a snapshot this way moves its expiry out to a full default life from now when that is later.

Errors:

  • 404 — the environment is not found or not yours; the snapshot is not found, not ready, not on this environment's volume, or not yours.
  • 409 — not ready yet: no volume for this workspace; the restore loses data (JSON: {"error": "these services keep data that the snapshot does not have; name them in `discard` to lose it", "lost": [{"service": "redis", "folders": ["rd"]}]}); a service the restore removes is intercepted (api is intercepted by ws-…; release the intercept before restoring); over the cpu or memoryGb limit for services it adds.
  • 400 — a service in the snapshot's frozen list no longer passes the service rules.
  • 422 — snapshot predates saved services; clone it into a new environment instead (the snapshot froze no service list); or discard names services that lose nothing (plain text, listing them).

POST /v1/environments/{id}/intercepts

Asks for one workspace to receive everything the environment sends to one service. Only a wish is written. The platform makes it true, and for this reason the route answers 202. A second call for the same service and workspace replaces the ports. An intercept a desk holds is that desk's: this route neither replaces nor removes it, and a body that names a desk is refused. Take and release those through POST /v1/desks/{id}/intercepts.

FieldTypeRequiredRule
servicestringyesA service of this environment that declares at least one port
workspacestringyesA workspace of yours (id, name, or team/name) that is running and is in a desk connected to this environment
portsobject[]no{ "service": int, "workspace": int }: callers dial service, the traffic lands on workspace. Each service port must be one the service declares, each workspace port non-zero, none mapped twice. A port with no entry is forwarded to the same number. Absent means every port 1:1
json
{ "service": "api", "workspace": "ws-0a1b2c3d4e5f6a7b", "ports": [{ "service": 8080, "workspace": 3000 }] }

Response: 202, the environment document. intercepts now holds the wish; service_status[].interceptedBy and proxy follow when the platform has made it so.

Errors:

  • 404 — the environment is not found or not yours; this environment has no service named "api"; the workspace is not found or not yours.
  • 409 — that workspace is not a member of a desk on this environment; add it to a desk connected to it; that workspace is stopped; start it before it can answer for a service; desk {name} ({owner}) already intercepts this service; it must be released first (or the workspace id when no desk holds it); api is held by desk login-fix (acme); release it through that desk when a desk already holds the service; the environment was changed while this was being written; try again.
  • 422 — `desk` is set by POST /v1/desks/{id}/intercepts (the body named a desk); service api declares no ports; nothing to intercept; this service does not declare port 9090, so nothing dials it; port 8080 must answer on a real port of the workspace, and 0 is not one; port 8080 is mapped twice; one service port answers on one workspace port; a port that lands on the tool server port 7788 of the workspace (port 8080 would land on workspace port 7788, the tool server's; map it to another port).

DELETE /v1/environments/{id}/intercepts/{service}

Removes the wish for one service. Only this route removes a wish. Stopping or deleting the workspace releases the traffic but keeps the wish. The wish takes hold again when the workspace returns. Deleting the workspace removes its wishes, on a best-effort basis.

Releasing a service that has none is a success. An intercept a desk holds is not removed here.

Response: 204 with an empty body.

Errors:

  • 404 — the environment is not found or not yours.
  • 409 — api is held by desk login-fix (acme); release it through that desk when a desk holds the service (release it with DELETE /v1/desks/{id}/intercepts/{service}); the environment was changed while this was being written; try again.