{
  "openapi": "3.0.3",
  "info": {
    "title": "Dockup PaaS API",
    "description": "Dockup is a self-hosted container PaaS. Deploy Docker containers, manage apps, view logs, execute commands, and more.\n\n## Authentication\nUse Bearer token in Authorization header. Get token via POST /auth/login or use API key (prefix dkp_). API keys work for all endpoints including WebSockets (pass as ?token= query param).\n\n## Quick Start\n1. Use API key or POST /auth/login to get token\n2. GET /projects to list projects (create one with POST /projects if empty)\n3. POST /projects/{id}/apps to create app (name, slug, image, internal_port)\n4. POST /apps/{id}/deploy to deploy it\n5. App is live at https://{slug}-app.{base_domain}\n\n## Important Rules\n- SLUG IS GLOBALLY UNIQUE. It becomes the subdomain. Cannot be reused across any user. Lowercase alphanumeric + hyphens only.\n- App URL format: https://{slug}-app.{base_domain}\n- Image sources: Docker Hub (e.g. nginx:alpine), git repos (set git_repo_url), or direct upload (POST /apps/{id}/upload-image).\n- Git deploy: Set git_repo_url=owner/repo and git_provider=github. POST /apps/{id}/deploy will clone, auto-detect language (Node/Python/Go/Ruby/PHP/Rust/static), generate Dockerfile, build, deploy. No Dockerfile needed.\n- Upload image: POST /apps/{id}/upload-image with Docker tar. Re-upload replaces image and redeploys. Env vars preserved.\n- Resources: cpu_limit (cores, e.g. 0.5), memory_limit (MB, e.g. 512). Changeable on running containers via PUT /apps/{id} - applied live, no restart.\n- DO NOT create apps with duplicate slugs (returns 409 slug_taken).\n- DO NOT set image field for git deploys (auto-generated).\n\n## CI/CD Deploy\nUse POST /deploy/{slug} for CI/CD pipelines. The slug is the stable deploy target.\n- Redeploy same image: POST /deploy/{slug} (empty body)\n- Update image tag: POST /deploy/{slug} with {\"image\": \"myapp:v2\"}\n- Upload new image: POST /deploy/{slug} with multipart form or raw tar body\nAll /apps/{appId} endpoints also accept slug instead of ID.\n\n## App Status\nApp responses include a 'status' field synced with Docker every 30s. Values: running, stopped, paused, restarting, starting. App list responses also include 'cpu_usage' (percent) and 'memory_usage' (MB) from the latest stats sample.\n\n## Logs\nGET /apps/{id}/logs returns JSON array: [{time, stream, text}]. Timestamps are ISO 8601. Stream is 'stdout' or 'stderr'. For real-time streaming, use WebSocket at /apps/{id}/logs/stream.\n\n## Terminal / Exec\nWebSocket at /apps/{id}/exec provides an interactive shell. Send text for stdin, receive binary for stdout. Send JSON {type:'resize', cols:N, rows:N} to resize. Use ?cmd=/bin/bash to change shell.\n\n## Events & History\nGET /apps/{id}/events returns deployment events, errors, and status changes with timestamps.\nGET /apps/{id}/deployments returns deployment history.\n\n## Monitoring\nGET /apps/{id}/stats returns live CPU%, memory, network for a running container.\nGET /apps/{id}/stats/history?minutes=60 returns time-series stats (30s intervals, 24h retention).\n\n## Autoscale\nGET/PUT /apps/{id}/autoscale to configure auto-scaling and sleep-on-idle.\n\n## Persistent Volumes\nAttach persistent storage to apps — data survives redeploys and container restarts.\n- GET /apps/{id}/volumes — list volumes\n- POST /apps/{id}/volumes — create and attach: {mount_path: '/data', size_mb: 1024}\n- DELETE /apps/{id}/volumes?mount_path=/data — detach (data preserved, Docker volume NOT deleted)\n- After add/remove, REDEPLOY to apply. Volume is mounted at mount_path inside the container.\n- Volumes are Docker named volumes (dockup-vol-{appId}-{n}) with dockup labels.\n- Use for databases (SQLite, etc.), uploads, config files — anything that must persist.\n\n## Templates\nCommon images: wordpress:latest, nginx:alpine, node:22-alpine, python:3.12-slim, postgres:17-alpine, redis:7-alpine, mysql:8.0, mongo:7, ghost:5-alpine, caddy:alpine, filebrowser/filebrowser:latest, php:8.3-apache.",
    "version": "0.2.0"
  },
  "servers": [{"url": "/api/v1"}],
  "security": [{"bearerAuth": []}],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "JWT token from /auth/login or API key starting with dkp_"
      }
    }
  },
  "paths": {
    "/auth/register": {
      "post": {
        "summary": "Register new account",
        "tags": ["Auth"],
        "security": [],
        "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["email","password","name"], "properties": {"email": {"type": "string"}, "password": {"type": "string", "minLength": 8}, "name": {"type": "string"}}}}}},
        "responses": {"201": {"description": "Returns user and JWT token"}}
      }
    },
    "/auth/login": {
      "post": {
        "summary": "Login and get JWT token",
        "tags": ["Auth"],
        "security": [],
        "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["email","password"], "properties": {"email": {"type": "string"}, "password": {"type": "string"}}}}}},
        "responses": {"200": {"description": "Returns user and JWT token"}}
      }
    },
    "/auth/me": {
      "get": {
        "summary": "Get current user profile",
        "tags": ["Auth"],
        "responses": {"200": {"description": "User object"}}
      }
    },
    "/system/status": {
      "get": {
        "summary": "System health check (public)",
        "tags": ["System"],
        "security": [],
        "responses": {"200": {"description": "Returns status, version, uptime, base_domain"}}
      }
    },
    "/system/dashboard": {
      "get": {
        "summary": "Dashboard stats — CPU/memory allocated, running/stopped apps with real Docker state",
        "tags": ["System"],
        "responses": {"200": {"description": "Returns system info, usage stats, tier, and app list with normalized status"}}
      }
    },
    "/system/resources": {
      "get": {
        "summary": "System resource overview — CPU cores, memory, container counts",
        "tags": ["System"],
        "responses": {"200": {"description": "Returns cpu_cores, goroutines, memory, containers_running/stopped/total, uptime"}}
      }
    },
    "/system/containers": {
      "get": {
        "summary": "List all Docker containers (admin)",
        "tags": ["System"],
        "responses": {"200": {"description": "Array of {id, name, image, state, status, ip, created}"}}
      }
    },
    "/projects": {
      "get": {
        "summary": "List all projects for current user",
        "tags": ["Projects"],
        "responses": {"200": {"description": "Array of projects"}}
      },
      "post": {
        "summary": "Create a new project",
        "tags": ["Projects"],
        "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["name"], "properties": {"name": {"type": "string"}, "slug": {"type": "string", "description": "URL-safe identifier, auto-generated from name if omitted"}}}}}},
        "responses": {"201": {"description": "Created project"}}
      }
    },
    "/projects/{id}": {
      "get": {
        "summary": "Get project by ID",
        "tags": ["Projects"],
        "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}],
        "responses": {"200": {"description": "Project object"}}
      },
      "delete": {
        "summary": "Delete project and all its apps",
        "tags": ["Projects"],
        "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}],
        "responses": {"200": {"description": "Deleted"}}
      }
    },
    "/projects/{projectId}/apps": {
      "get": {
        "summary": "List apps in a project (includes status, cpu_usage, memory_usage)",
        "description": "Returns enriched app list with real Docker status (running/stopped/paused/restarting/starting) and latest resource consumption (cpu_usage as %, memory_usage in MB).",
        "tags": ["Apps"],
        "parameters": [{"name": "projectId", "in": "path", "required": true, "schema": {"type": "string"}}],
        "responses": {"200": {"description": "Array of apps with status, cpu_usage, memory_usage fields"}}
      },
      "post": {
        "summary": "Create a new app in a project",
        "description": "Creates an app. Set 'image' to a Docker image (e.g. nginx:alpine). The app gets a public URL at https://{slug}-app.{base_domain} after deployment.",
        "tags": ["Apps"],
        "parameters": [{"name": "projectId", "in": "path", "required": true, "schema": {"type": "string"}}],
        "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["name","image"], "properties": {
          "name": {"type": "string", "description": "Display name"},
          "slug": {"type": "string", "description": "URL slug, used in subdomain. Auto-generated from name if omitted"},
          "image": {"type": "string", "description": "Docker image (e.g. nginx:alpine, node:22-alpine, wordpress:latest)"},
          "internal_port": {"type": "integer", "default": 8080, "description": "Port the container listens on"},
          "env_vars": {"type": "object", "description": "Environment variables as key-value pairs"},
          "command": {"type": "string", "description": "Override container command"},
          "cpu_limit": {"type": "number", "default": 0.5, "description": "CPU cores limit"},
          "memory_limit": {"type": "integer", "default": 512, "description": "Memory limit in MB"},
          "gpu_enabled": {"type": "boolean", "default": false},
          "git_repo_url": {"type": "string", "description": "Git repo (owner/repo for GitHub, or full URL)"},
          "git_branch": {"type": "string", "default": "main"},
          "git_provider": {"type": "string", "enum": ["github", "custom"]}
        }}}}},
        "responses": {"201": {"description": "Created app object"}, "409": {"description": "Slug already taken"}}
      }
    },
    "/apps/{appId}": {
      "get": {
        "summary": "Get app details, container state, status, and live resource usage",
        "tags": ["Apps"],
        "parameters": [{"name": "appId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "App ID or slug"}],
        "responses": {"200": {"description": "Returns {app (with status, cpu_usage, memory_usage), container_state}"}}
      },
      "put": {
        "summary": "Update app config (image, env vars, resources — resources applied live)",
        "tags": ["Apps"],
        "parameters": [{"name": "appId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "App ID or slug"}],
        "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {
          "name": {"type": "string"},
          "image": {"type": "string"},
          "internal_port": {"type": "integer"},
          "env_vars": {"type": "object"},
          "cpu_limit": {"type": "number"},
          "memory_limit": {"type": "integer"}
        }}}}},
        "responses": {"200": {"description": "Updated app"}}
      },
      "delete": {
        "summary": "Delete app (stops and removes container)",
        "tags": ["Apps"],
        "parameters": [{"name": "appId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "App ID or slug"}],
        "responses": {"200": {"description": "Deleted"}}
      }
    },
    "/deploy/{slug}": {
      "post": {
        "summary": "CI/CD deploy by slug — the primary way to update an app",
        "description": "Looks up app by slug (the stable, human-readable identifier) and deploys it. Three modes:\n\n1. Redeploy same image: POST with empty body — pulls latest tag and redeploys.\n2. Update image tag: POST with JSON {image: myapp:v2} — changes image and deploys.\n3. Upload image: POST with multipart form (field image) or raw tar body — loads Docker image and deploys.\n\nReturns the app object (with status, resources), deployment info, and slug.\n\nExamples:\n  curl -X POST -H 'Authorization: Bearer dkp_...' .../api/v1/deploy/my-app\n  curl -X POST -d '{image:v2}' -H 'Authorization: Bearer dkp_...' .../api/v1/deploy/my-app\n  docker save myapp:latest | curl -X POST --data-binary @- -H 'Content-Type: application/octet-stream' -H 'Authorization: Bearer dkp_...' .../api/v1/deploy/my-app",
        "tags": ["Deploy"],
        "parameters": [{"name": "slug", "in": "path", "required": true, "schema": {"type": "string"}, "description": "App slug (the subdomain identifier, e.g. 'my-app')"}],
        "requestBody": {"required": false, "content": {
          "application/json": {"schema": {"type": "object", "properties": {"image": {"type": "string", "description": "New Docker image tag (e.g. myapp:v2)"}}}},
          "multipart/form-data": {"schema": {"type": "object", "properties": {"image": {"type": "string", "format": "binary"}}}},
          "application/octet-stream": {"schema": {"type": "string", "format": "binary"}}
        }},
        "responses": {
          "202": {"description": "Deployment started. Returns {app, deployment, slug, message}"},
          "404": {"description": "No app with this slug"}
        }
      }
    },
    "/apps/{appId}/upload-image": {
      "post": {
        "summary": "Upload a Docker image tarball and deploy",
        "description": "Accepts a Docker image tar file (from 'docker save'), loads it into Docker, and deploys the app. Send as multipart form with field 'image' or raw tar body. Max 2GB.\n\nExample: docker save myapp:latest | gzip > myapp.tar.gz\ncurl -X POST -F 'image=@myapp.tar.gz' -H 'Authorization: Bearer dkp_...' https://dockup.cc/api/v1/apps/{appId}/upload-image\n\nOr: curl -X POST --data-binary @myapp.tar -H 'Authorization: Bearer dkp_...' https://dockup.cc/api/v1/apps/{appId}/upload-image",
        "tags": ["Apps"],
        "parameters": [{"name": "appId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "App ID or slug"}],
        "requestBody": {"required": true, "content": {
          "multipart/form-data": {"schema": {"type": "object", "properties": {"image": {"type": "string", "format": "binary"}}}},
          "application/octet-stream": {"schema": {"type": "string", "format": "binary"}}
        }},
        "responses": {"200": {"description": "Image loaded and deployment started. Returns loaded_image, tagged_as, deployment object."}}
      }
    },
    "/apps/{appId}/deploy": {
      "post": {
        "summary": "Deploy app (pull image, create container, start)",
        "description": "Pulls the Docker image, creates a new container with the configured resources and security settings, starts it, and registers a public URL. For git repos, clones and auto-builds. Runs asynchronously — returns immediately with deployment ID.",
        "tags": ["Apps"],
        "parameters": [{"name": "appId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "App ID or slug"}],
        "responses": {"202": {"description": "Deployment started, returns deployment object with status 'pending'"}}
      }
    },
    "/apps/{appId}/start": {
      "post": {
        "summary": "Start a stopped app",
        "tags": ["Apps"],
        "parameters": [{"name": "appId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "App ID or slug"}],
        "responses": {"200": {"description": "Started"}}
      }
    },
    "/apps/{appId}/stop": {
      "post": {
        "summary": "Stop a running app",
        "tags": ["Apps"],
        "parameters": [{"name": "appId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "App ID or slug"}],
        "responses": {"200": {"description": "Stopped"}}
      }
    },
    "/apps/{appId}/restart": {
      "post": {
        "summary": "Restart an app",
        "tags": ["Apps"],
        "parameters": [{"name": "appId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "App ID or slug"}],
        "responses": {"200": {"description": "Restarted"}}
      }
    },
    "/apps/{appId}/logs": {
      "get": {
        "summary": "Get recent container logs as JSON with timestamps",
        "description": "Returns JSON array of log entries. Each entry has 'time' (ISO 8601 timestamp), 'stream' (stdout/stderr), and 'text' (log line content).",
        "tags": ["Apps"],
        "parameters": [
          {"name": "appId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "App ID or slug"},
          {"name": "tail", "in": "query", "schema": {"type": "string", "default": "100"}, "description": "Number of recent lines to return"}
        ],
        "responses": {"200": {"description": "JSON array of {time, stream, text}", "content": {"application/json": {"schema": {"type": "array", "items": {"type": "object", "properties": {"time": {"type": "string"}, "stream": {"type": "string"}, "text": {"type": "string"}}}}}}}}
      }
    },
    "/apps/{appId}/logs/stream": {
      "get": {
        "summary": "Stream logs via WebSocket (real-time)",
        "description": "WebSocket endpoint. Sends JSON messages {stream, line} as logs arrive. Pass auth as ?token= query param. Use ?tail=N to include recent history.",
        "tags": ["Apps", "WebSocket"],
        "parameters": [
          {"name": "appId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "App ID or slug"},
          {"name": "token", "in": "query", "required": true, "schema": {"type": "string"}, "description": "JWT or API key"},
          {"name": "tail", "in": "query", "schema": {"type": "string", "default": "100"}}
        ],
        "responses": {"101": {"description": "WebSocket upgrade — sends JSON {stream, line} messages"}}
      }
    },
    "/apps/{appId}/exec": {
      "get": {
        "summary": "Interactive terminal via WebSocket",
        "description": "WebSocket endpoint for interactive shell access. Send text/binary for stdin, receive binary for stdout. Send JSON {type:'resize', cols:N, rows:N} to resize terminal. Pass auth as ?token= query param.",
        "tags": ["Apps", "WebSocket"],
        "parameters": [
          {"name": "appId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "App ID or slug"},
          {"name": "token", "in": "query", "required": true, "schema": {"type": "string"}, "description": "JWT or API key"},
          {"name": "cmd", "in": "query", "schema": {"type": "string", "default": "/bin/sh"}, "description": "Shell command to execute"}
        ],
        "responses": {"101": {"description": "WebSocket upgrade — bidirectional stdin/stdout stream"}}
      }
    },
    "/apps/{appId}/stats": {
      "get": {
        "summary": "Get live container resource usage",
        "tags": ["Apps"],
        "parameters": [{"name": "appId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "App ID or slug"}],
        "responses": {"200": {"description": "Returns {cpu_percent, memory_mb, memory_max_mb, net_rx_bytes, net_tx_bytes}"}}
      }
    },
    "/apps/{appId}/stats/history": {
      "get": {
        "summary": "Get stats time-series (30s intervals, 24h retention)",
        "tags": ["Apps"],
        "parameters": [
          {"name": "appId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "App ID or slug"},
          {"name": "minutes", "in": "query", "schema": {"type": "integer", "default": 60}, "description": "How many minutes of history (max 1440 = 24h)"}
        ],
        "responses": {"200": {"description": "Array of {cpu_pct, memory_mb, net_rx_mb, net_tx_mb, created_at}"}}
      }
    },
    "/apps/{appId}/events": {
      "get": {
        "summary": "Get app events (deploy, error, status changes)",
        "tags": ["Apps"],
        "parameters": [{"name": "appId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "App ID or slug"}],
        "responses": {"200": {"description": "Array of {id, type, status, message, details, created_at}"}}
      }
    },
    "/apps/{appId}/deployments": {
      "get": {
        "summary": "List deployment history",
        "tags": ["Apps"],
        "parameters": [{"name": "appId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "App ID or slug"}],
        "responses": {"200": {"description": "Array of {id, image, status, container_id, started_at, finished_at, created_at}"}}
      }
    },
    "/apps/{appId}/autoscale": {
      "get": {
        "summary": "Get autoscale configuration",
        "tags": ["Apps"],
        "parameters": [{"name": "appId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "App ID or slug"}],
        "responses": {"200": {"description": "Autoscale config: enabled, cpu_threshold, sleep_after_minutes, etc."}}
      },
      "put": {
        "summary": "Update autoscale configuration",
        "tags": ["Apps"],
        "parameters": [{"name": "appId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "App ID or slug"}],
        "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {
          "enabled": {"type": "boolean"},
          "cpu_threshold": {"type": "number", "description": "CPU% threshold to trigger scale-up"},
          "sleep_after_minutes": {"type": "integer", "description": "Minutes of inactivity before sleeping (0 = disabled)"}
        }}}}},
        "responses": {"200": {"description": "Updated"}}
      }
    },
    "/apps/{appId}/volumes": {
      "get": {
        "summary": "List persistent volumes attached to an app",
        "description": "Returns array of volumes. Each volume has name (Docker volume name), mount_path (path inside container), and size_mb (requested size, informational).",
        "tags": ["Volumes"],
        "parameters": [{"name": "appId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "App ID or slug"}],
        "responses": {"200": {"description": "Array of {name, mount_path, size_mb}"}}
      },
      "post": {
        "summary": "Create and attach a persistent volume",
        "description": "Creates a Docker named volume and attaches it to the app. Redeploy required to mount. Volume name auto-generated (dockup-vol-{appId}-{n}). Data survives redeploys and container restarts.",
        "tags": ["Volumes"],
        "parameters": [{"name": "appId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "App ID or slug"}],
        "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["mount_path"], "properties": {
          "mount_path": {"type": "string", "description": "Path inside container, e.g. /data"},
          "size_mb": {"type": "integer", "description": "Requested size in MB (informational)", "default": 1024}
        }}}}},
        "responses": {"201": {"description": "Volume created: {name, mount_path, size_mb}. Redeploy to mount."}}
      },
      "delete": {
        "summary": "Detach a volume from an app (data preserved)",
        "description": "Removes volume from app config. Docker volume is NOT deleted — data is preserved. Can re-attach later. Redeploy to apply.",
        "tags": ["Volumes"],
        "parameters": [
          {"name": "appId", "in": "path", "required": true, "schema": {"type": "string"}, "description": "App ID or slug"},
          {"name": "mount_path", "in": "query", "required": true, "schema": {"type": "string"}, "description": "Mount path to detach, e.g. /data"}
        ],
        "responses": {"200": {"description": "Volume detached, data preserved"}}
      }
    },
    "/api-keys": {
      "get": {
        "summary": "List API keys",
        "tags": ["API Keys"],
        "responses": {"200": {"description": "Array of API keys (prefix only, not full key)"}}
      },
      "post": {
        "summary": "Create API key (shown once)",
        "tags": ["API Keys"],
        "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"name": {"type": "string", "default": "default"}}}}}},
        "responses": {"201": {"description": "Returns id, name, and full key (starts with dkp_). Save it — won't be shown again."}}
      }
    },
    "/registries": {
      "get": {
        "summary": "List configured Docker registries",
        "tags": ["Registries"],
        "responses": {"200": {"description": "Array of registries"}}
      },
      "post": {
        "summary": "Add a private Docker registry",
        "tags": ["Registries"],
        "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["name","url"], "properties": {"name": {"type": "string"}, "url": {"type": "string"}, "username": {"type": "string"}, "password": {"type": "string"}}}}}},
        "responses": {"201": {"description": "Created registry"}}
      }
    },
    "/git/repos": {
      "get": {
        "summary": "List git repos from connected OAuth accounts",
        "tags": ["Git"],
        "parameters": [{"name": "provider", "in": "query", "schema": {"type": "string"}, "description": "Filter by provider (github)"}],
        "responses": {"200": {"description": "Array of repos with full_name, default_branch, provider"}}
      }
    },
    "/git/repos/{owner}/{repo}/branches": {
      "get": {
        "summary": "List branches for a repo",
        "tags": ["Git"],
        "parameters": [
          {"name": "owner", "in": "path", "required": true, "schema": {"type": "string"}},
          {"name": "repo", "in": "path", "required": true, "schema": {"type": "string"}},
          {"name": "provider", "in": "query", "schema": {"type": "string", "default": "github"}}
        ],
        "responses": {"200": {"description": "Array of branches"}}
      }
    },
    "/auth/oauth/accounts": {
      "get": {
        "summary": "List connected OAuth accounts (GitHub, Google)",
        "tags": ["Auth"],
        "responses": {"200": {"description": "Array of connected OAuth accounts with provider and username"}}
      }
    }
  }
}