API Reference
The Scalable REST API lets you programmatically manage your web apps, databases, and account resources — the same surface the dashboard and CLI use, with stable contracts and webhooks.
Overview
All request and response bodies are application/json. Successful responses return 200 OK or 201 Created. Deletions return 204 No Content. Errors return a JSON body with an error field.
Generate API tokens from Settings → API Tokens in your account dashboard. Tokens are scoped per account.
error field; rate-limit hits return 429 with a Retry-After header.Authentication
The API uses Bearer token authentication. Include your API token in the Authorization header of every request — see the example to the right.
Create and revoke tokens under Settings → API Tokens in the dashboard. Each token is scoped to a single account.
scalable.yaml
Define multiple services in a single repository using a scalable.yaml configuration file. Place it at the repo root to declare web apps and workers with per-service Dockerfiles, build contexts, change-detection filters, and build-time secrets. When you connect the repo via "Deploy from GitHub", the file is auto-detected and all services are created.
On subsequent pushes, each service only rebuilds when files matching its filter pattern have changed — saving build time in monorepo setups. See the full schema on the right.
MCP Server
The platform exposes a Model Context Protocol (MCP) server, letting AI agents and tools manage your web apps directly — no copy-pasting curl commands. Endpoint: https://api.scalable.systems/v0/mcp. Pass your API token as a Bearer header on the SSE connection.
mcpServers in your client config. Available tools include list_web_apps, create_web_app, update_web_app, get_web_app_logs, run_web_app_command, and the equivalent cron job tools. Sample config on the right.Web Apps
Deploy and manage containerised web applications. Each web app runs in a chosen region and can be scaled within your plan limits.
GET List web apps
/accounts/{account_id}/web-apps
Returns all web apps belonging to the account.
| Path param | |
|---|---|
| account_idrequiredstring (uuid) | UUID of the account. |
POST Create web app
/accounts/{account_id}/web-apps
Deploy a new web app from a container image. The app starts deploying immediately (status SCALING).
| Body | |
|---|---|
| namerequiredstring | A unique name for the web app within the account. |
| imagerequiredstring | Container image reference, e.g. nginx:latest. |
| portrequiredinteger | Port your container listens on. |
| protocolstring | "http" (default) or "grpc". gRPC apps are served over TLS externally and h2c internally. |
grpc:// internal URL using cleartext HTTP/2 (h2c) on port 80.GET Get web app
/accounts/{account_id}/web-apps/{web_app_id}
Returns a single web app with its current status, configuration, and metrics time series.
| Query | |
|---|---|
| rangestring | Time range for metrics as a Go duration string, e.g. "1h", "24h", "7d". Defaults to "24h". |
metrics object contains time-series arrays for cpu and memory. Values are percentages (0–100). Each point has an RFC 3339 timestamp and a numeric value.PATCH Update web app
/accounts/{account_id}/web-apps/{web_app_id}
At least one field must be provided; omitted fields are left unchanged. Changing the image triggers a new deployment. Setting env_vars to {} clears all variables.
| Body | |
|---|---|
| imagestring | New container image to deploy. |
| portinteger | Container port. |
| env_varsobject | Full replacement of environment variables. Pass {} to clear all variables. |
| volume_idstring (uuid) | UUID of a volume to attach, or "" to detach the currently attached one. |
DELETE Delete web app
/accounts/{account_id}/web-apps/{web_app_id}
Deletes the web app and all associated Kubernetes resources and registry images. This action cannot be undone. Returns 204 No Content.
POST Restart
/accounts/{account_id}/web-apps/{web_app_id}/restart
Triggers a rolling restart of all pods without changing the image or configuration. The app continues serving traffic during the restart.
POST Suspend
/accounts/{account_id}/web-apps/{web_app_id}/suspend
Scales the deployment to zero replicas and removes its HPA, freeing namespace quota. The app stops serving traffic immediately. Use resume to restore it.
POST Resume
/accounts/{account_id}/web-apps/{web_app_id}/resume
Restores a suspended web app to its previous replica count and re-creates its HPA.
POST Configure scaling
/accounts/{account_id}/web-apps/{web_app_id}/scaling
Adjusts the HPA's min/max replica bounds and which metric drives scale-out. Cannot be called on suspended apps or apps with a volume attached.
| Body | |
|---|---|
| min_replicasinteger | Minimum number of replicas. Must be ≥ plan minimum. |
| max_replicasinteger | Maximum number of replicas. Must be ≥ min_replicas and ≥ 1. |
| scaling_modestring | Which metric drives scale-out: 'cpu', 'memory', or 'both' (default). |
POST Configure resources per replica
/accounts/{account_id}/web-apps/{web_app_id}/resources
Sets the memory and CPU request for each pod. memory_mi sets both request and limit (MiB); cpu_milli sets only the CPU request in millicores (the plan ceiling remains the limit). Set either to 0 to reset it to the plan default.
memory_mi × max_replicas ≤ account memory quota.GET Get logs
/accounts/{account_id}/web-apps/{web_app_id}/logs
Returns log lines from all pods. When follow=true the response is a plain-text stream (Content-Type: text/plain) using chunked transfer-encoding.
| Query | |
|---|---|
| tailinteger | Max lines to return (1–10000). Default 1000. |
| sinceinteger | Seconds in the past (max 604800 / 7 days). Default 86400. Ignored when from is set. |
| fromdate-time | Return logs from this RFC 3339 timestamp onwards. |
| todate-time | Return logs up to this RFC 3339 timestamp. |
| followboolean | Stream logs in real time. Default false. |
GET List registry images
/accounts/{account_id}/web-apps/{web_app_id}/images
Returns all container images pushed to this web app's registry repository, newest first.
DELETE Delete registry image
/accounts/{account_id}/web-apps/{web_app_id}/images/{digest}
Deletes a specific image from the registry by its digest, e.g. sha256:abc123....
GET Get run context
/accounts/{account_id}/web-apps/{web_app_id}/run
Returns the working directory of a running pod. Returns 409 if no running pod is available — the app may be stopped or still starting.
POST Run command
/accounts/{account_id}/web-apps/{web_app_id}/run
Executes a one-off command inside a running pod and returns the combined stdout/stderr output. Tokens are joined and passed to sh -c. Times out after 10 minutes.
| Body | |
|---|---|
| commandrequiredstring[] | Command tokens to execute, e.g. ["rails", "db:migrate"]. |
409 if no running pod is available. A non-zero exit_code means the command failed. Arguments containing password=, secret=, token=, key=, or credential= are redacted in server logs.Custom Domains
Attach one or more custom domains to a web app. Both apex (root) domains and CNAME subdomains are supported. Ownership must be verified by adding DNS records before a domain becomes active.
GET List custom domains
/accounts/{account_id}/web-apps/{web_app_id}/domains
Returns all custom domains attached to the web app, including verification status and required DNS records for each.
POST Add custom domain
/accounts/{account_id}/web-apps/{web_app_id}/domains
Registers a new custom domain and returns the DNS records required for verification. Subdomains require a CNAME record; apex (root) domains require an A record. The domain is not active until verified.
| Body | |
|---|---|
| domainrequiredstring | The custom domain to attach, e.g. app.example.com or example.com. |
POST Verify custom domain
/accounts/{account_id}/web-apps/{web_app_id}/domains/{domain_id}/verify
Checks DNS records for the specified domain. On success, the TLS certificate is provisioned and the domain becomes active.
DELETE Delete custom domain
/accounts/{account_id}/web-apps/{web_app_id}/domains/{domain_id}
Removes a custom domain from the web app and revokes its TLS certificate.
Preview Environments
Ephemeral single-replica deployments scoped to a parent web app. Each preview gets its own Ingress, Service, and Deployment. Resource limits are inherited from the account plan. Status is always derived live from Kubernetes and is never stored in the database. Previews are automatically deleted after 7 days.
GET List previews
/accounts/{account_id}/web-apps/{web_app_id}/previews
Returns all active previews for the web app. Returns an empty array when none exist.
POST Create preview
/accounts/{account_id}/web-apps/{web_app_id}/previews
Deploys a new ephemeral preview environment. The deployment scales up immediately (status SCALING) and becomes READY within seconds. Returns 403 on Free plan accounts. Returns 409 if the namespace has reached the plan's pod quota.
| Body | |
|---|---|
| imagerequiredstring | Full image reference to deploy. |
| slugstring | URL-safe label. Defaults to the first 8 characters of the preview UUID. |
| env_overridesobject | Key-value pairs merged on top of the parent web app's env vars. |
env_overrides in the response is what was stored at creation — not the full merged env var set. To resolve the full set, fetch the parent web app's env_vars and merge client-side.GET Get preview
/accounts/{account_id}/web-apps/{web_app_id}/previews/{preview_id}
Returns a single preview with its live status derived from Kubernetes.
DELETE Delete preview
/accounts/{account_id}/web-apps/{web_app_id}/previews/{preview_id}
Best-effort deletes the Ingress, Service, and Deployment from Kubernetes, then soft-deletes the database record. Never returns an error for missing Kubernetes resources.
Workers
Deploy and manage long-running background processes — queue consumers, scheduled jobs, data pipelines — without an exposed HTTP port. Workers run as Kubernetes Deployments and support rolling restarts and replica scaling.
GET List workers
/accounts/{account_id}/workers
POST Create worker
/accounts/{account_id}/workers
If image is omitted the worker record is created without a Kubernetes Deployment — supply an image via PATCH or push one via the registry to start the deployment.
| Body | |
|---|---|
| namerequiredstring | A unique name for the worker within the account. |
| imagestring | Container image reference. Can be omitted and supplied later. |
| commandstring | Override the image CMD/ENTRYPOINT, e.g. "bin/jobs". |
| replicasinteger | Number of replicas (1–5). Defaults to 1. |
GET Get worker
/accounts/{account_id}/workers/{worker_id}
Returns a single worker with its current status, restart count, and last deployed timestamp.
PATCH Update worker
/accounts/{account_id}/workers/{worker_id}
Update image, command, replica count, environment variables, or attached volume. Changing the image or command triggers a rolling restart.
DELETE Delete worker
/accounts/{account_id}/workers/{worker_id}
Deletes the worker and its Kubernetes Deployment. This action cannot be undone.
POST Restart worker
/accounts/{account_id}/workers/{worker_id}/restart
Triggers a rolling restart of all worker pods without changing the image or configuration.
GET Get logs
/accounts/{account_id}/workers/{worker_id}/logs
Returns log lines from all pods. When follow=true the response is a plain-text stream. Log lines are formatted as HH:MM:SS.mmm [pod_name] message.
GET Get images
/accounts/{account_id}/workers/{worker_id}/images
Returns registry images pushed to this worker's repository, newest first.
POST Run command
/accounts/{account_id}/workers/{worker_id}/run
Executes a one-off command inside a running worker pod and returns combined stdout/stderr output. Times out after 10 minutes. Secret-like arg values are redacted in server logs.
Cron Jobs
Scheduled tasks that run on a cron expression. Two modes: container (runs a pod with a container image) and http (sends a request to any URL via curl). concurrencyPolicy is Forbid — only one instance runs at a time. Run history is stored in the platform database and available via the runs endpoint.
GET List cron jobs
/accounts/{account_id}/cron-jobs
POST Create cron job
/accounts/{account_id}/cron-jobs
Creates a Kubernetes CronJob in the account namespace. If the previous run has not finished, the next scheduled run is skipped.
| Body | |
|---|---|
| namerequiredstring | Unique name for the cron job within the account. |
| moderequiredstring | "container" — runs a pod with a container image. "http" — sends a request to a URL using curl. |
| schedulerequiredstring | Standard 5-field cron expression, e.g. "0 9 * * 1". |
| timezonestring | IANA timezone name. Defaults to "UTC". |
| imagestring | Container image. Required for container mode. |
| commandstring | Command override for container mode (space-split tokens). |
| urlstring | Target URL. Required for http mode. |
| http_methodstring | "GET" or "POST". Default "POST". |
| http_bodystring | Optional JSON body string for http mode POST requests. |
| env_varsobject | Environment variables. Supports secret:// references. |
| timeout_secondsinteger | Max run duration. Default 3600. Pod is killed if it exceeds this. |
GET Get cron job
/accounts/{account_id}/cron-jobs/{cron_job_id}
status values: PENDING (no runs yet), RUNNING, IDLE (last run succeeded), FAILED (last run failed), ERROR (CronJob resource missing from cluster).
PATCH Update cron job
/accounts/{account_id}/cron-jobs/{cron_job_id}
The Kubernetes CronJob is updated in-place — the next scheduled run will use the new values. Any running job is unaffected.
DELETE Delete cron job
/accounts/{account_id}/cron-jobs/{cron_job_id}
Deletes the CronJob and any owned Jobs from the cluster (background propagation). Run history is also deleted.
POST Trigger (manual run)
/accounts/{account_id}/cron-jobs/{cron_job_id}/trigger
Creates a Kubernetes Job from the CronJob's template immediately, outside the schedule. concurrencyPolicy Forbid is not applied — a manual job can run in parallel with a scheduled one.
GET List run history
/accounts/{account_id}/cron-jobs/{cron_job_id}/runs
Returns the 50 most recent runs, newest first. The platform reconciler writes run records from Kubernetes Job completions every 60 seconds, so there may be a short delay before a completed run appears.
scheduledfired by cron expressionmanualfired via /trigger endpointsucceededexit_code 0Postgres
Managed PostgreSQL databases with automated backups, connection credentials, and private networking within the account namespace.
GET List databases
/v0/accounts/{account_id}/databases
POST Create database
/v0/accounts/{account_id}/databases
Create a new PostgreSQL database. A dedicated user and password are generated automatically. Body: name (required), size (small | medium | large, plan-dependent), engine ("postgres").
PROVISIONING → STARTING → HEALTHY. Poll GET until HEALTHY before connecting. The host is only reachable within the account namespace. Free plan does not allow any databases. Returns 403 if size not allowed for plan; 409 on name conflict or quota reached.GET Get database
/v0/accounts/{account_id}/databases/{database_id}
Status values: PROVISIONING, STARTING, HEALTHY, RESTARTING, RESTORING, FAILED, TERMINATING, UNKNOWN. Credentials are always returned — there is no separate credentials endpoint.
PATCH Resize database
/v0/accounts/{account_id}/databases/{database_id}
Change size to small | medium | large. The database remains available during the resize. Returns 403 if the new size is not allowed for your plan tier.
DELETE Delete database
/v0/accounts/{account_id}/databases/{database_id}
Data cannot be recovered.
GET Get logs
/v0/accounts/{account_id}/databases/{database_id}/logs
Query: ?tail=100 (max 1000), ?from=<rfc3339>, ?to=<rfc3339>. Returns 400 if tail exceeds 1000.
GET POST Backups & restore
/v0/accounts/{account_id}/databases/{database_id}/backups · /restore
List completed backups. POST to trigger a manual backup (returns 202). Restore via POST /restore with {"backup_id": "..."}. All data written after the backup point will be lost.
RESTORING, then back to HEALTHY on success. Automatic backups are re-enabled after a successful restore.GET POST Extensions
/v0/accounts/{account_id}/databases/{database_id}/extensions[/available]
List currently-enabled extensions. Available lists what you can enable. POST { "extension": "pgvector" } enables one — poll until status reaches applied. Returns 409 if already enabled (or pending). Currently supported: pgvector.
Redis
Managed Redis instances with private networking. Credentials are returned on every read — store them securely. Instances are only accessible within the account namespace.
GET List instances
/v0/accounts/{account_id}/redis
POST Create instance
/v0/accounts/{account_id}/redis
Body: { "name": "cache" }. A secure password is generated automatically. Status will be SCALING immediately after creation, transitioning to READY once available. The connection_string uses the redis:// URI scheme and can be passed directly to most clients.
GET Get instance
/v0/accounts/{account_id}/redis/{redis_id}
Status values: PENDING, SCALING, READY, DEGRADED, FAILED. The password is always returned.
DELETE Delete instance
/v0/accounts/{account_id}/redis/{redis_id}
Data cannot be recovered after deletion.
Volumes
Persistent volumes are backed by PersistentVolumeClaims on SeaweedFS. Create a volume first, then attach it to a web app or worker via the volume_id field on PATCH /web-apps/:id or PATCH /workers/:id. Max size depends on your tier (Small: 5 GB, Medium/Large: 10 GB).
GET List volumes
/accounts/{account_id}/volumes
POST Create volume
/accounts/{account_id}/volumes
Provisions a PersistentVolumeClaim. Status starts as PENDING until bound by the storage class.
| Body | |
|---|---|
| namerequiredstring | Unique name within the account. Must be a valid DNS label. |
| size_gbrequiredinteger | Size in gigabytes. Min 1, max depends on tier. |
| mount_pathrequiredstring | Absolute path where the volume will be mounted, e.g. /data. |
GET Get volume
/accounts/{account_id}/volumes/{volume_id}
status is one of PENDING, BOUND, or ERROR. attached_to is the name of the worker currently mounting this volume, or omitted if unattached.
PATCH Update volume
/accounts/{account_id}/volumes/{volume_id}
Updates the mount path metadata. To attach or detach from a worker, use PATCH /workers/:id instead.
DELETE Delete volume
/accounts/{account_id}/volumes/{volume_id}
Permanently deletes the PVC and its data. The pvc-protection finalizer is force-removed so the PVC terminates immediately. Returns 204 No Content.
Secrets
Store encrypted values and reference them in web app and worker environment variables using the secret:// scheme. Secret values are never returned by the API — only metadata (id, name, timestamps) is exposed.
GET List secrets
/accounts/{account_id}/secrets
POST Create secret
/accounts/{account_id}/secrets
Body: { "name": "DATABASE_PASSWORD", "value": "..." }. The value is encrypted at rest and never returned.
secret://<secret_id> when creating or updating a web app or worker. The platform resolves the reference to the plaintext value before injecting it into the container.PUT Update secret
/accounts/{account_id}/secrets/{secret_id}
Replaces the encrypted value. The name cannot be changed. All resources referencing this secret will pick up the new value on their next deployment or restart.
DELETE Delete secret
/accounts/{account_id}/secrets/{secret_id}
Soft-deletes the secret. Any web app or worker env vars referencing it via secret:// will fail to resolve on the next deployment.
GET List access logs
/accounts/{account_id}/secrets/{secret_id}/access-logs
Audit log of create, update, and delete actions performed on the secret, ordered newest first. user_id is omitted when the action was performed via an API token with no associated user session.
Users
Endpoints for the authenticated user's own profile.
GET Get current user
/users/me
Returns the authenticated user's profile. When called with an API token the response also includes a token object containing the token's name and the account it is scoped to — useful for resolving an account ID in CI/CD without visiting the dashboard.
token field is only present when authenticated with an API token. It is omitted for browser sessions and JWT auth.Registry
The platform includes a built-in private container registry. Each web app gets its own repository. Push images to deploy new versions of your app.
- Log in —
docker login registry.<your-domain> --username <your-email> --password <api-token> - Tag — registry path is
registry.<your-domain>/<account-name>/<web-app-name> - Push —
docker push registry.<your-domain>/<account-name>/<web-app-name>:v1.0.0 - Deploy — update the web app's
imagefield viaPATCH /web-apps/{web_app_id}
PATCH call rolls the new image into production.