Endpoint Reference
Every route below is exact — verified against the handlers under src/app/api/.
For auth, conventions, and the error shape, read the API Overview first.
Conventions
- Auth. REST routes authenticate with your dashboard session (NextAuth
cookie). Personal API tokens (
cldy_…) are minted at/api/me/tokensand consumed by the MCP server — they are not yet accepted on these REST routes. - Roles. Workspace routes gate on a minimum role: viewer → developer → admin → owner (ascending). A route marked developer admits developer and above.
- Env + region. App routes resolve the target instance from query params:
?env=(proddefault) and?region=.regionis required only when asvcIdexists in more than one region — otherwise you get400 region_required. - Error shape.
{ error: { code, message } }(some legacy routes use a flat{ error, code }). Validation failures from the zod body are400.
Workspaces
GET /api/workspaces
Auth: signed in. Lists your workspaces plus reference data.
{ "workspaces": [...], "regions": [...], "tiers": [...] }POST /api/workspaces
Auth: signed in. Creates a workspace and activates its first region.
Body
{
"name": "Neptolab",
"slug": "neptolab",
"hues": [210, 280],
"tier": "free",
"region": "hil1",
"iconUrl": "data:image/…"
}slug—^[a-z0-9][a-z0-9-]*$, ≤40 chars.hues—[number, number], each0 ≤ h < 360.region,iconUrl— optional.
Responses
201 { workspace }— created (free or paid with an existing payment method).202 { workspace, needsPayment: true, checkoutUrl }— finish in Stripe Checkout.400 bad_reference·409 free_unavailable(one free Hobby per account) ·503 billing_misconfigured.
GET /api/workspaces/[slug]
Auth: member. Returns the hydrated { workspace } (pools/plans per region,
instances, etc.). 404 not_found if you are not a member.
PATCH /api/workspaces/[slug]
Auth: admin. Partial update — every field optional.
{
"name": "…", "slug": "…", "hues": [h1, h2],
"autoRestart": true, "nightlyBackups": false, "iconUrl": null
}Renaming the slug runs a PV-preserving namespace migration. Responses:
{ workspace } · 409 slug_taken · 409 rename_blocked (an app is in a
non-terminal state) · 400 bad_reference.
DELETE /api/workspaces/[slug]
Auth: owner. { ok: true }.
Helpers
GET /api/workspaces/slug-check?slug=…— slug availability.GET /api/workspaces/[slug]/events?limit=100— activity log (viewer).limitdefaults to 100.
Regions & Plans
POST /api/workspaces/[slug]/regions/[regionId]/activate
Auth: admin. Activates a region for the workspace (creates the plans row +
the per-region Stripe subscription). Idempotent — a region that already has a
live subscription returns { ok: true }.
Body
{ "tier": "starter" }Responses
200 { ok: true }— activated directly (existing payment method / free).200 { ok: true, region: { status: "warming" } }— a virtual region was provisioned; the cluster is warming up.202 { checkoutUrl }— pay in Stripe to finish.404 not_found(unknown region) ·409 region_unavailable(region isdraining/failed/etc.) ·400 bad_reference(unknown tier) ·409 free_unavailable·503 pricing_missing(no Stripe price — runscripts/stripe-setup.ts) ·503 billing_misconfigured·502 provider_error(provision failed; the subscription was cancelled).
DELETE /api/workspaces/[slug]/regions/[regionId]/activate
Auth: admin. Deactivates the region: cancels the subscription and drops the
plans row. No-op { ok: true } if there was no plan.
409 region_not_empty— delete every app in the region first.
Reference reads
GET /api/regions— all regions (signed in).GET /api/tiers— all tiers (signed in).
Applications
Apps are instances in storage. The hydrated GET /api/workspaces/[slug]
response carries the workspace’s apps; the routes below act on a single app by
svcId.
POST /api/workspaces/[slug]/apps/install/[applicationId]
Auth: developer. Installs a catalog application as a new instance. The
applicationId segment selects the catalog entry (and any per-app install
handler).
Body
{
"name": "DailyFrog",
"region": "hil1",
"env": "prod",
"version": "6.5",
"profiles": ["redis"],
"prompts": { "GITHUB_REPO_URL": "https://github.com/me/app" }
}region— required.env— one of the workspace env ids, defaultsprod.name,version— optional (version defaults to the catalogcurrent).profiles— active compose profiles (drives component checkboxes).prompts— values for the version’s declared${VAR}prompts. Required prompts gated by an inactive profile are not enforced.
Responses
201 { app }.404 not_found(unknown application).400 missing_prompts—{ error, missing: [...] }.400 bad_reference(no compose template for the version).402 activation_required— region not activated for this workspace.409 insufficient_pool—{ needs, free, missing }; pool can’t fit the app.409 region_at_capacity·502 provider_error.
PATCH /api/workspaces/[slug]/apps/[svcId]
Auth: developer. Rename / change version / start-stop.
{ "name": "New name", "status": "running", "version": "6.6" }status— one ofrunning,degraded,stopped,error. Settingrunningon a stopped app re-runs the capacity gate (may queue a node buy).
Responses: { app } · 404 not_found · 400 region_required ·
409 region_at_capacity · 502 provider_error.
DELETE /api/workspaces/[slug]/apps/[svcId]
Auth: developer. Deletes the instance and purges its k8s resources
(including data PVCs). { ok: true } · 404 not_found · 400 region_required.
POST /api/workspaces/[slug]/apps/[svcId]/redeploy
Auth: developer. Forces a rollout through runDeploy. { app } ·
404 · 400 region_required · 502 reconcile_failed.
POST /api/workspaces/[slug]/apps/[svcId]/transfer
Auth: developer (you must also be a member of the target). Moves the app to another workspace in the same region.
{ "targetWorkspaceSlug": "other-ws" }{ app, targetWorkspaceSlug, newSvcId } · 400 bad_target ·
402 activation_required · 409 insufficient_pool · 502 transfer_failed.
Live reads
GET …/apps/[svcId]/services— running pod containers (state, readiness, restarts, ports). viewer.GET …/apps/[svcId]/status— SSE, 5s ticks of instance + container status. viewer.GET …/apps/[svcId]/metrics— SSE, 5s ticks of cpu/ram metrics. viewer.GET …/apps/[svcId]/services/[serviceName]/logs— SSE log tail for a container (clone/buildstream the init steps). viewer.GET …/apps/[svcId]/credentials— install-time credentials. developer.
Deploys
Immutable records of “we tried to ship version X”.
GET /api/workspaces/[slug]/apps/[svcId]/deploys
Auth: viewer. { deploys: [...] } · 404 · 400 region_required.
POST /api/workspaces/[slug]/apps/[svcId]/deploy
Auth: developer. Triggers a deploy. Commit metadata is optional — when
absent, runDeploy resolves the tip commit of the configured branch.
{ "sha": "abc123", "commitMsg": "fix", "author": "me" }201 { deploy } · 502 reconcile_failed ({ error, deploy }).
Deploy streams
GET …/apps/[svcId]/deploys/active— SSE, 2s ticks of the active (or latest) deploy. viewer.GET …/apps/[svcId]/deploys/[deployId]/logs— SSE build-log stream; snapshots the stored log once the deploy is terminal. viewer.
Domains
GET /api/workspaces/[slug]/apps/[svcId]/domains
Auth: viewer. { domains }.
POST /api/workspaces/[slug]/apps/[svcId]/domains
Auth: developer. Attaches a custom hostname.
{ "host": "app.example.com", "isPrimary": true, "redirectTo": null }201 { domain } · 409 host_taken · 404 · 400 region_required.
PATCH /api/workspaces/[slug]/apps/[svcId]/domains/[domainId]
Auth: developer. Only { redirectTo } is editable. { domain } ·
404 not_found · 400 region_required.
DELETE /api/workspaces/[slug]/apps/[svcId]/domains/[domainId]
Auth: developer. { ok: true } · 404 not_found.
GET /api/workspaces/[slug]/apps/[svcId]/domains/certs
Auth: viewer. Live cert-manager status per custom domain (polled by the UI).
Environment Variables
GET /api/workspaces/[slug]/apps/[svcId]/vars
Auth: developer. { vars } (secret values masked).
POST /api/workspaces/[slug]/apps/[svcId]/vars
Auth: developer.
{ "key": "DATABASE_URL", "value": "postgres://…", "isSecret": true }key—^[A-Z_][A-Z0-9_]*$(SCREAMING_SNAKE_CASE), ≤80.value— ≤4096.isSecretencrypts at rest.
201 { var } · 409 key_taken · 404 · 400 region_required.
PATCH /api/workspaces/[slug]/apps/[svcId]/vars/[varId]
Auth: developer. { value?, isSecret? } → { var } ·
404 not_found · 400 bad_request.
DELETE /api/workspaces/[slug]/apps/[svcId]/vars/[varId]
Auth: developer. { ok: true } · 404 not_found.
GET /api/workspaces/[slug]/apps/[svcId]/vars/[varId]/reveal
Auth: developer. Returns the decrypted { value } of a secret var.
Members
GET /api/workspaces/[slug]/members
Auth: viewer. { members, viewerRole }.
Invitations live under /api/workspaces/[slug]/invitations and
/api/invitations/[token] (accept / decline).
Billing
GET /api/workspaces/[slug]/billing?region=…
Auth: viewer. Live-read billing state for one region (tier, status, price).
region may be omitted only when the workspace has exactly one plan —
otherwise 400 region_required. { billing }.
POST /api/workspaces/[slug]/billing/plan?region=…
Auth: admin. Changes the tier for an already-activated region.
{ "tier": "pro" }regionquery param required → else400 region_required.
Responses
200 { ok: true }— tier changed (or unchanged).202 { checkoutUrl }— region had no live subscription; pay to attach one.400 bad_reference(unknown tier) ·409 not_activated·409 free_unavailable·409 pool_exceeded(current usage exceeds the target tier’s pool —{ details }) ·503 stripe_unavailable·503 pricing_missing/503 billing_misconfigured.
Account-level billing (the user, not a workspace) lives at /api/me/billing.
Catalog
GET /api/templates
Auth: signed in. The installable catalog ({ templates }) — applications, their
versions, components, footprint. This is the source for the install picker.
Tokens
Personal API tokens, scoped to your user (optionally pinned to a workspace). Used by the MCP server.
GET /api/me/tokens
Auth: signed in. { tokens } — metadata only (prefix, scope, last used). The
secret is shown once at creation.
POST /api/me/tokens
Auth: signed in.
{ "name": "ci", "scope": "deploy", "expires": "90d" }scope—full(default) ·deploy·read.expires—never(default) ·30d·90d·1y.
201 { token, secret } — secret (cldy_…) is returned only here.
DELETE /api/me/tokens/[id]
Auth: signed in. Revokes the token. { ok: true } · 404 not_found.