Dock Docs
API reference

Conventions

Base URL, authentication, envelopes, errors, idempotency, confirmation, operations, and rate limits for the Dock API.

These conventions apply to every route in the API reference. The resource pages (Docks, Snapshots and templates, Environments, Organizations, Keys, Webhooks, GitHub, Hosts, Account) list each route, and Objects describes what the routes return.

Base URL

The API lives under <DOCK_PUBLIC_URL>/v1 on your self-hosted control plane. The running API serves its OpenAPI contract at GET /openapi.json (outside /v1, no credentials).

Authentication

CredentialHow to send itUsed for
API keyAuthorization: Bearer dock_...Automation. A key acts as the user who created it.
Browser sessionThe cookie set by the dashboard sign-in (/api/auth)The dashboard. Used only when no Authorization header is sent.

Create and revoke API keys in the dashboard under Developers > API keys, or with POST /v1/api-keys. The raw value is shown once and only its hash is stored.

Three kinds of route differ:

  • Public routes need no credentials: GET /v1/config, POST /v1/contact, and the machine-only routes that are not part of the public API.
  • Session-only routes refuse API keys with session_required (403). Each page marks them with "Session only".
  • Dock-bound keys (issued to a single Dock) may call only GET /v1/docks/:id, the three port routes, and GET /v1/docks/:id/stats, and only for their own Dock. Anything else fails with dock_key_forbidden (403).

Organization roles are member, admin, and owner, in increasing order. A route's "Role" line is the lowest role allowed. A caller who is not a member of the owning organization gets 404, not 403, so ids never leak across organizations. A member whose role is below the route's Role gets forbidden (403).

A closed account can only call POST /v1/account/reopen and POST /v1/account/erase; every other route fails with account_closed (403), or 410 once the data is erased.

Envelope

Every successful response is wrapped:

{ "ok": true, "data": { "...route payload..." } }

Every failure has the same shape with a stable machine-readable code:

{ "ok": false, "code": "dock_not_found", "message": "Dock not found" }

Examples on the resource pages show the data value only. Two things are not enveloped: GET /v1/snapshots/:id/files returns raw bytes, and the terminal WebSocket is a stream.

Request bodies are JSON. Unknown body fields are rejected with validation_error (400).

Errors

Codes are snake_case. Generic codes follow the HTTP status; the rest are specific to an operation.

CodeHTTP statusMeaning
validation_error400A body field, query parameter, or header failed validation.
unauthorized401Missing, invalid, expired, or revoked credentials.
forbidden403Authenticated, but not allowed.
not_found404The resource does not exist or is not visible to you.
conflict409The state does not allow the request, for example resuming a Dock that is not archived.
rate_limited429A rate limit was hit.
internal_error500Unexpected failure.
bad_gateway502An upstream answered badly.
service_unavailable503A dependency is unavailable; the rate limiter fails closed.
session_required403The route needs a signed-in browser session, not an API key.
dock_key_forbidden403A Dock-bound key called a route it may not use.
account_closed403 (410 once erased)The account is closed or erased.
confirm_required400A destructive call lacked its exact confirmation.
image_unknown400The image is not in the Dockyard catalog.
image_unavailable400The image is in the catalog but no online host serves it.
idempotency_key_reused409An idempotency-key was reused with a different body.
idempotency_in_progress409The first request with this key is still running.
dock_not_found404No such Dock, or it is not yours.
dock_name_taken409The name is in use in that workspace.
dock_busy409The Dock is busy with another operation; retry shortly.
dock_not_running409The call needs a ready Dock.
dock_delete_forbidden403Only the Dock's creator or an organization admin can delete it.
capacity_exhausted503No host has room for the requested cpus and memoryMiB.
no_host_capacity503No online host can place the Dock.
base_image_unavailable503The host lacks the base image the snapshot needs.
exec_unavailable409The Dock has no exec agent, or a Run drives it.
exec_timeout504The command exceeded timeoutSeconds and was killed.
exec_busy503The host could not take the command; retry later.
exec_failed503The host failed the command; retry later.
exec_result_expired409A replayed command already ran but its result was not kept; it is not run again.
port_not_found404The port is not exposed.
port_limit_reached409A Dock exposes at most 10 ports.
ports_unavailable503Hosted ports are not configured on this deployment.
snapshot_not_found404No such Snapshot, or no such path in it.
snapshot_in_use409A newer Snapshot or a Template still depends on it.
not_a_regular_file422The requested path in a Snapshot is not a regular file.
file_too_large413The file is too large to return.
template_not_found404No such ready Template.
template_limit_reached409An organization keeps at most 10 Templates.
template_source_missing409The Template points at a Snapshot that no longer exists.
zdr_blocks_templates403Templates are disabled while retention is enabled.
operation_not_found404No such operation, or it is not yours.
environment_not_found404No such Environment.
environment_name_taken409The Environment name is in use.
environment_secrets_admin_only403The Environment passes secrets into Docks and only admins may launch such Docks.
organization_not_found404No such organization, or you are not a member.
workspace_not_found404No such workspace.
workspace_not_empty409Delete the Docks in the workspace first.
member_not_found404No such member.
user_not_found404No user with that id.
invitation_not_found404The invitation is unknown, expired, revoked, used, or not addressed to you.
slug_taken409The slug is in use.
seats_exceeded409The organization has no free seat.
cannot_remove_owner400An organization must keep at least one owner.
sole_owner_of_shared_org409You are the only owner of an organization that has other members.
api_key_not_found404No such API key.
api_key_limit_reached409At most 20 active API keys per user.
ssh_key_not_found404No such SSH key.
ssh_key_exists409The key is already added.
ssh_key_limit_reached409At most 20 SSH keys per user.
provider_keys_not_found404No key stored for that provider.
webhook_endpoint_not_found404No such webhook endpoint.
endpoints_exceeded409At most 10 webhook endpoints per organization.
notification_not_found404No such notification.
last_login_method409You cannot remove your only way to sign in.
chatgpt_not_connected404No ChatGPT subscription is connected.
chatgpt_signin_disabled403ChatGPT sign-in is off on this instance.
chatgpt_unavailable502OpenAI answered badly or was unreachable.
device_code_expired400The ChatGPT device sign-in expired; start again.
installation_not_found404No such GitHub installation, or, on GitHub routes, no such workspace.
installation_not_accessible403The installation cannot be linked by you.
run_not_found404No such Run.
run_not_promotable409The Run cannot be promoted.
fast_run_disabled503Fast Run is off on this deployment.
fast_run_concurrency_exceeded409The plan's concurrent Fast Run limit is reached.
invalid_state400The GitHub link or manifest state is unknown, expired, or not yours.
invalid_code400GitHub rejected the manifest code.
github_unavailable502GitHub was unreachable or answered badly.
github_app_oauth_not_configured503GITHUB_APP_CLIENT_ID and GITHUB_APP_CLIENT_SECRET are not set.
unavailable503The link state store is unavailable.
instance_admin_required403Only instance admins may call this.
email_verification_required403Verify your email to use instance admin features.
host_not_found404No such host.
mail_unavailable503The contact message could not be delivered.
subscription_required402An active subscription is required to start compute.
concurrency_exceeded409The plan's concurrent Dock limit is reached.
budget_exceeded403The monthly budget is exhausted.

Idempotency

POST /v1/docks, POST /v1/docks/:id/branch, and POST /v1/docks/:id/exec accept an optional idempotency-key header. A retry with the same key returns the original result instead of repeating the work.

For POST /v1/docks and POST /v1/docks/:id/branch, keys are scoped to the organization and kept for 24 hours. Reusing a key with a different body fails with idempotency_key_reused (409), and retrying while the first request still runs fails with idempotency_in_progress (409).

On POST /v1/docks/:id/exec the key is 1 to 255 characters and scoped to the Dock; a reused key replays the first command's result whatever the body.

Delete confirmation

DELETE /v1/docks/:id and DELETE /v1/snapshots/:id need an x-confirm-delete header whose value is exactly the id being deleted. Without it they fail with confirm_required (400). Two other destructive calls confirm with a phrase in the body instead: POST /v1/account/erase and PATCH /v1/account/retention.

Operations

Calls that delete a Dock, Snapshot, or Template, and POST /v1/environments/:ref/upgrade, return an operation. Poll it with GET /v1/operations/:id until status is completed. An operation with status blocked could not finish; retrying the same action reuses it.

Rate limits

Limits are fixed windows counted in Redis. Past the limit the call fails with rate_limited (429). If the counter store is down the call fails with service_unavailable (503) rather than running unthrottled.

ScopeLimit
POST /v1/docks/:id/exec, per Dock120 per minute, and 4 in flight
POST /v1/docks/:id/exec, per caller600 per minute
POST /v1/docks/:id/terminal, per caller30 per minute; at most 4 open terminals per Dock and 8 per user
POST /v1/api-keys, per user10 per hour
POST /v1/ssh-keys, per user10 per hour
POST /v1/chatgpt/connection/device, per user10 per hour
POST /v1/contact, per client address3 per hour
GitHub Runs started from mentions, per installation and repository10 per hour
Outbound webhook deliveries, per endpoint60 per minute

Pagination and filtering

Two listings page with an opaque cursor: Snapshots (GET /v1/snapshots, GET /v1/docks/:id/snapshots) and GET /v1/notifications. Send limit (1 to 100) and the cursor from the previous response's nextCursor; a null nextCursor ends the list. GET /v1/docks filters by state group with filter and all. Every other list returns everything in one response.

Routes that act inside an organization take organizationId in the query string (or in the body for writes that say so), and GitHub routes take workspaceId.

On this page