CTRL API

v1 · api.getctrl.dev


Authentication

All /v1/* endpoints require a Bearer token in the Authorization header:

Authorization: Bearer <api_key>

Tokens are issued via GitHub OAuth at /auth/github. Internal routes (/internal/*) are not publicly accessible.


Plans

PlanvCPURAMDiskPrice
hobby12 GB30 GB$5/mo
builder24 GB60 GB$15/mo
pro48 GB100 GB NVMe$75/mo
scale816 GB200 GB NVMe$140/mo

Apps

GET /v1/apps
List all apps for your account.

Response 200:

[
  {
    "app_id":     "my-api",
    "name":       "My API",
    "status":     "running",
    "region":     "us-east",
    "subdomain":  "my-api.getctrl.dev",
    "created_at": "2026-05-01T12:00:00Z"
  }
]

POST /v1/apps
Deploy a new application from a Docker image or GitHub repository.

Request body:

{
  "name":          "My API",
  "repo":          "my-repo",
  "branch":        "main",
  "image_ref":     "ghcr.io/acme/my-api:latest",  // or use repo_url instead
  "repo_url":      null,                           // git clone URL (alternative to image_ref)
  "region":        "us-east",
  "exposed_port":  8080,
  "vcpu_count":    1,
  "mem_size_mb":   2048,
  "disk_size_mb":  30720,
  "env_vars":      [["PORT", "8080"]],
  "add_database":  false
}

Response 201:

{
  "app_id":    "my-api",
  "vm_id":     "550e8400-e29b-41d4-a716-446655440000",
  "status":    "starting",
  "region":    "us-east",
  "node_id":   "us-1",
  "subdomain": "my-api.getctrl.dev"
}

Errors:


GET /v1/apps/:app_id
Get detailed info for a specific app.

Response 200:

{
  "app_id":         "my-api",
  "name":           "My API",
  "vm_id":          "550e8400-...",
  "status":         "running",
  "region":         "us-east",
  "node_id":        "us-1",
  "image_ref":      "ghcr.io/acme/my-api:latest",
  "exposed_port":   8080,
  "repo":           "my-repo",
  "branch":         "main",
  "subdomain":      "my-api.getctrl.dev",
  "repo_full_name": "acme/my-repo",
  "env_vars":       "[["PORT","8080"]]",
  "created_at":     "2026-05-01T12:00:00Z"
}

PUT /v1/apps/:app_id/env
Update environment variables. Triggers an automatic redeploy for Git-sourced apps.

Request body:

{
  "env_vars": [["PORT", "8080"], ["NODE_ENV", "production"]]
}

Response 200:

{ "ok": true, "redeployed": true }

DELETE /v1/apps/:app_id
Delete an app. Stops the VM, removes routing, and hard-deletes all records.

Response 200:

{ "ok": true }

GET /v1/apps/:app_id/logs
Fetch recent application logs.

Query parameters:

ParamDefaultDescription
tail100Number of lines (max 1000)

Response 200:

{ "lines": ["[2026-05-01 12:00:00] Server listening on :8080", "..."] }

Deploys

GET /v1/apps/:app_id/deploys
List deploy history for an app, newest first.

Response 200:

[
  {
    "id":          "deploy-uuid",
    "commit_sha":  "abc1234",
    "commit_msg":  "fix: login bug",
    "trigger":     "push",
    "status":      "success",
    "duration_ms": 45000,
    "created_at":  "2026-05-01T12:00:00Z"
  }
]

POST /v1/apps/:app_id/rollback
Rollback to a previous deploy by re-deploying its commit.

Request body:

{ "deploy_id": "deploy-uuid" }

Response 200:

{ "ok": true, "deploy_id": "new-deploy-uuid" }

Errors:


Metrics

GET /v1/apps/:app_id/metrics
Retrieve time-series CPU, memory, and network metrics.

Query parameters:

ParamValuesDefault
period1h, 24h, 7d1h

Response 200:

{
  "period": "1h",
  "points": [
    {
      "timestamp":  "2026-05-01T12:00:00Z",
      "cpu":        12.5,
      "memory_mb":  50.0,
      "rx_kb":      1024.0,
      "tx_kb":      512.0
    }
  ]
}

Errors:


Databases

POST /v1/apps/:app_id/database
Provision a managed MySQL database for an app. Connection string is automatically injected as DATABASE_URL env var.

Response 201:

{
  "database_id":       "my-api-db",
  "db_name":           "my_api_db",
  "db_user":           "u_abc123",
  "host":              "db-1.nodes.getctrl.dev",
  "port":              3306,
  "connection_string": "mysql://u_abc123:pass@host:3306/my_api_db",
  "status":            "active"
}

Errors:


GET /v1/apps/:app_id/database
Get database connection details (password masked).

DELETE /v1/apps/:app_id/database
Delete the managed database. Removes DATABASE_URL from app env vars.

GET /v1/apps/:app_id/database/backups
List database backups. Backups run automatically at 03:00 UTC daily.

Response 200:

[
  {
    "id":         "backup-uuid",
    "size_bytes": 1048576,
    "status":     "completed",
    "created_at": "2026-05-01T03:00:00Z"
  }
]

POST /v1/apps/:app_id/database/restore
Restore database from a specific backup.

Request body:

{ "backup_id": "backup-uuid" }

Errors:


Account

GET /v1/account
Get your account details, plan, and resource usage.

Response 200:

{
  "id":            "acc-uuid",
  "github_login":  "octocat",
  "plan": {
    "id":           "hobby",
    "display_name": "Hobby",
    "vcpu_limit":   1,
    "mem_limit_mb": 2048,
    "disk_limit_mb": 30720
  },
  "usage": {
    "vcpu":    1,
    "mem_mb":  2048,
    "disk_mb": 30720
  },
  "subscription_status": "active",
  "created_at":          "2026-04-01T00:00:00Z"
}

GitHub

GET /v1/github/repos
List your GitHub repositories (requires GitHub OAuth connection).

Response 200:

[
  {
    "full_name":      "octocat/my-repo",
    "name":           "my-repo",
    "private":        false,
    "default_branch": "main",
    "language":       "TypeScript",
    "pushed_at":      "2026-05-01T12:00:00Z"
  }
]

GET /v1/github/repos/:owner/:repo/branches
List branches for a repository.

GET /v1/github/repos/:owner/:repo/dockerfile
Check if a Dockerfile exists in a repo. Accepts ?ref=branch query param.

Response 200:

{ "exists": true }

Authentication

GET /auth/github
Start GitHub OAuth flow. Redirects to GitHub for authorization.

GET /auth/github/callback
OAuth callback. Sets ctrl_session cookie (JWT, 7-day expiry) and redirects to dashboard.

GET /auth/logout
Clear session cookie and redirect to login page.