Skip to Content
API ReferenceEndpoint Reference

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/tokens and consumed by the MCP server — they are not yet accepted on these REST routes.
  • Roles. Workspace routes gate on a minimum role: viewerdeveloperadminowner (ascending). A route marked developer admits developer and above.
  • Env + region. App routes resolve the target instance from query params: ?env= (prod default) and ?region=. region is required only when a svcId exists in more than one region — otherwise you get 400 region_required.
  • Error shape. { error: { code, message } } (some legacy routes use a flat { error, code }). Validation failures from the zod body are 400.

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], each 0 ≤ 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). limit defaults 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 is draining/failed/etc.) · 400 bad_reference (unknown tier) · 409 free_unavailable · 503 pricing_missing (no Stripe price — run scripts/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" } }
  • regionrequired.
  • env — one of the workspace env ids, defaults prod.
  • name, version — optional (version defaults to the catalog current).
  • 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 of running, degraded, stopped, error. Setting running on 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]/statusSSE, 5s ticks of instance + container status. viewer.
  • GET …/apps/[svcId]/metricsSSE, 5s ticks of cpu/ram metrics. viewer.
  • GET …/apps/[svcId]/services/[serviceName]/logsSSE log tail for a container (clone/build stream 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/activeSSE, 2s ticks of the active (or latest) deploy. viewer.
  • GET …/apps/[svcId]/deploys/[deployId]/logsSSE 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. isSecret encrypts 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" }
  • region query param required → else 400 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" }
  • scopefull (default) · deploy · read.
  • expiresnever (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.

Last updated on