Skip to content

REST API Reference

wody edited this page May 23, 2026 · 24 revisions

REST API Reference

Complete endpoint catalog as of v0.10.0. All routes are defined as constants in shared/.../ApiPath.kt; copy them into your client to stay wire-compatible.

Authentication

All /api/* routes (except /api/auth/setup, /api/auth/setup/status, /api/auth/login, and /health) require Bearer authentication:

Authorization: Bearer <token>

Obtain a token with POST /api/auth/login. The same token works with both the header and the vibe_session cookie path.

Base URL

http://<host>:17880

For LAN deployments. HTTPS is the operator's responsibility (reverse proxy recommended for non-LAN exposure).


Auth

POST /api/auth/setup/status

Check whether an admin user exists. Used by clients to decide between login and setup screen.

curl http://localhost:17880/api/auth/setup/status
# → {"adminExists": true}

POST /api/auth/setup

Create the first admin user (only when no admin exists). Returns 409 Conflict if one already exists.

curl -X POST http://localhost:17880/api/auth/setup \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"ChangeMe123","deviceName":"my-laptop"}'
# → {"token":"...","deviceId":"...","serverName":"...","username":"admin"}

POST /api/auth/login

curl -X POST http://localhost:17880/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"ChangeMe123","deviceName":"my-android"}'
# → {"token":"...","deviceId":"...","serverName":"...","username":"admin"}

10 consecutive failures lock the account for 15 minutes (timing-safe).

POST /api/auth/password

Change password. Requires Bearer auth.

curl -X POST http://localhost:17880/api/auth/password \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"currentPassword":"old","newPassword":"new"}'
# → 204 No Content

Server status

GET /api/server/status

High-level summary.

curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:17880/api/server/status
{
  "serverName": "Vibe Coder Server",
  "serverVersion": "0.10.0",
  "osName": "Linux 6.x",
  "javaVersion": "17.0.19",
  "workspaceRoot": "/workspace",
  "projectCount": 3,
  "runningTaskCount": 0,
  "claudeAvailable": true,
  "androidSdkAvailable": true,
  "gitAvailable": true,
  "freeDiskSpaceBytes": 102400000000
}

GET /api/server/environment / GET /api/server/environment/check

Detailed per-component diagnostics.


Projects

GET /api/projects

curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:17880/api/projects
# → [{"id":"my-app","name":"My App","packageName":"...", ...}]

POST /api/projects/register

Empty project:

curl -X POST http://localhost:17880/api/projects/register \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "projectId": "my-app",
    "appName": "My App",
    "packageName": "com.siamakerlab.myapp"
  }'

Clone from git:

curl -X POST http://localhost:17880/api/projects/register \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "projectId": "my-app",
    "appName": "My App",
    "packageName": "com.siamakerlab.myapp",
    "sourceType": "clone",
    "cloneUrl": "https://github.com/owner/repo.git",
    "cloneBranch": "main"
  }'

Private SSH URL — first register a PAT or generate an SSH key via the Git integrations endpoints below.

Optional keystore field (JSON API only)

RegisterProjectRequestDto.keystore is accepted by the server but is not exposed in the admin SSR project-create form — it is reachable only via this JSON endpoint (curl / mobile client / direct API).

curl -X POST http://localhost:17880/api/projects/register \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "projectId": "my-app",
    "appName": "My App",
    "packageName": "com.siamakerlab.myapp",
    "keystore": {
      "alias": "myapp",
      "password": "at-least-6-chars",
      "dname": "CN=Jangwook Lee, OU=Mobile, O=Sia Makerlab, L=Jecheon, ST=Chungbuk, C=KR",
      "validityDays": 36500
    }
  }'

KeystoreRequestDto fields:

Field Type Required Default Notes
alias String Non-blank. Becomes keyAlias in the generated .properties.
password String Used for both storePassword and keyPassword. Minimum 6 chars.
dname String? Sia Makerlab default DName keytool -dname value.
validityDays Int 36500 (~100 yr) keytool -validity.

Output files (always outside the project folder, in the workspace sidecar):

<workspace>/.vibecoder/keystores/<projectId>/
  <projectId>.keystore               ← keytool -genkeypair output
  <projectId>-keystore.properties    ← Gradle-readable signing config

.properties contents:

storeFile=<workspace>/.vibecoder/keystores/<projectId>/<projectId>.keystore
storePassword=<plain-password>
keyAlias=<alias>
keyPassword=<plain-password>

Security caveat (current implementation). KeystoreGenerator invokes keytool -storepass <plain> -keypass <plain> — the password is therefore briefly visible in ps -ef / /proc/<pid>/cmdline on the host while the command runs (~1 s). Acceptable under vibe-coder's single-operator LAN threat model, but if you share the host with anyone, generate the keystore manually outside the container and only drop the .keystore file in. A future release will migrate to -storepass:env. Tracked in roadmap.

If keystore.password policy is stricter than the 6-char minimum, generate the keystore separately and place the files under <workspace>/.vibecoder/keystores/<projectId>/ yourself.

To skip keystore generation entirely, omit the field or send "keystore": null (current default).


Builds

POST /api/projects/{id}/build/debug

Queue a debug build. Returns immediately with a BuildDto; stream live output via /ws/projects/{id}/builds/{buildId}/logs.

GET /api/projects/{id}/builds

List recent builds.

POST /api/projects/{id}/builds/{buildId}/cancel

Send SIGTERM (then SIGKILL after 5 s) to the build subprocess.

GET /api/projects/{id}/artifacts/{artifactId}/download

Stream the produced APK.


Claude Console (per-project persistent session)

POST /api/projects/{id}/claude/console/prompt

curl -X POST http://localhost:17880/api/projects/my-app/claude/console/prompt \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"text":"Add a settings screen with a dark mode toggle"}'
# → 202 Accepted (response streamed over WebSocket)

POST /api/projects/{id}/claude/console/new

Tear down the current session and start fresh (--resume <id> is dropped).

GET /api/projects/{id}/claude/status

Model, plan, quota remaining (cached 60 s).


Environment setup (v0.10.0)

GET /api/env-setup/components

curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:17880/api/env-setup/components
{
  "components": [
    {"id":"java","displayName":"JDK 17","status":"INSTALLED","installable":false, ...},
    {"id":"android-sdk","displayName":"Android SDK","status":"MISSING","installable":true, ...},
    {"id":"claude-auth","displayName":"Claude 로그인","status":"INSTALLED","installable":false, ...}
  ]
}

POST /api/env-setup/install-all

Trigger sequential install of every installable component. Returns {"taskId":"..."}. Subscribe to /ws/env-setup/{taskId}/logs for live output.

POST /api/env-setup/{componentId}/install

Install a specific component (e.g. android-sdk).

Tracking install progress (client-side only)

The ComponentStateDto returned by GET /api/env-setup/components has no installingTaskId (or equivalent) field. Its full shape is:

id, displayName, description, sizeHint, status, message, installable

That means the wire does not tell you which component is currently installing. The contract is:

  1. Client calls POST .../install (or install-all).
  2. Server returns EnvSetupTaskDto{ taskId } once.
  3. Client stores the (componentId → taskId) mapping itself and subscribes to /ws/env-setup/{taskId}/logs.
  4. When WsFrame.Done arrives, the client drops the mapping and may refresh GET /components to confirm the new status.

The server has a lastTask[component] cache internally (used by the SSR template to render "View live progress →" links), but it is intentionally not exposed in any JSON response — different clients can be installing different components concurrently from different sessions, so per-client ownership of the taskId is the right model.

If your UI shows a per-component "installing…" spinner, do it from local state seeded by the install response, not by re-polling /components.


Claude authentication (v0.10.0)

POST /api/env-setup/claude-auth/upload (multipart)

Upload .credentials.json obtained on another machine.

curl -X POST http://localhost:17880/api/env-setup/claude-auth/upload \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@$HOME/.claude/.credentials.json"
# → {"targetPath":"/home/vibe/.claude/.credentials.json", ...}

POST /api/env-setup/claude-auth/api-key

curl -X POST http://localhost:17880/api/env-setup/claude-auth/api-key \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"apiKey":"sk-ant-..."}'
# → 204 No Content

DELETE /api/env-setup/claude-auth/api-key/delete

Remove the API key (also accepts POST for clients that lack DELETE).


Claude semi-automatic web OAuth (v0.10.0)

POST /api/env-setup/claude-login/start

Spawn claude auth login wrapped in script -q. Returns the initial state.

curl -X POST http://localhost:17880/api/env-setup/claude-login/start \
  -H "Authorization: Bearer $TOKEN"
{
  "id":"task-...",
  "state":"STARTING",
  "url":null,
  "startedAt":"2026-05-23T12:00:00Z",
  "updatedAt":"2026-05-23T12:00:00Z",
  "errorMessage":null,
  "lastLines":[]
}

Poll /api/env-setup/claude-login/status every 1 s until state becomes AWAITING_CODE (and url is set).

GET /api/env-setup/claude-login/status

Same shape as above. Returns 204 if no session active.

POST /api/env-setup/claude-login/submit

curl -X POST http://localhost:17880/api/env-setup/claude-login/submit \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"code":"abc...#xyz"}'

State → VERIFYING, then DONE or FAILED.

POST /api/env-setup/claude-login/cancel

Destroy the child process. State → CANCELED.


MCP catalog (v0.10.0)

GET /api/env-setup/mcp

curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:17880/api/env-setup/mcp
{
  "entries": [
    {
      "id":"github",
      "displayName":"GitHub",
      "pkg":"@modelcontextprotocol/server-github",
      "description":"...",
      "category":"Git 호스팅 (GitHub / GitLab / Gitea / Bitbucket)",
      "trust":"VERIFIED",
      "recommended":true,
      "homepage":null,
      "configFields":[
        {"key":"GITHUB_PERSONAL_ACCESS_TOKEN","label":"GitHub PAT","placeholder":"ghp_...","isSecret":true,"required":true,"help":"..."}
      ],
      "status":"NOT_INSTALLED",
      "configValues":{},
      "comingSoon":false
    },
    ...
  ]
}

McpConfigFieldDto reference

The configFields[] entries have a fixed shape with these 8 keys (no type, no default — those concepts don't exist in the catalog):

Key Type Required Default Purpose
key String Stored verbatim as the env var name in .mcp.json (e.g. GITHUB_PERSONAL_ACCESS_TOKEN).
label String Human-readable text for the form label.
placeholder String? null HTML placeholder attribute, e.g. "ghp_...".
isSecret Boolean false Render as <input type="password"> / masked field.
required Boolean true Server rejects install with 400 missing_config if a required field is empty.
help String? null Tooltip / sub-label describing the field.
isFile Boolean false v0.11.0+. true → render a file picker and call the multipart upload endpoint below.
acceptMime String? null When isFile=true, value for HTML <input accept="...">, e.g. ".json,application/json".

Notes:

  • A field's value type is always String at the wire level. Files go through the multipart upload endpoint and the server returns an absolute path which the client then sends back as the String value.
  • There is no default field in the wire. If a catalog entry needs a sensible default (e.g. --workspace-root /workspace for filesystem), it is baked into argsTemplate in McpCatalog.kt, not exposed to the client.

POST /api/env-setup/mcp/install

curl -X POST http://localhost:17880/api/env-setup/mcp/install \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "selections": {
      "filesystem": {},
      "github": {"GITHUB_PERSONAL_ACCESS_TOKEN":"ghp_..."},
      "context7": {}
    }
  }'
# → {"taskId":"..."}    (subscribe to /ws/env-setup/{taskId}/logs)

POST /api/env-setup/mcp/unregister

Removes entries from .mcp.json. The npm package itself stays on disk.

curl -X POST http://localhost:17880/api/env-setup/mcp/unregister \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"ids":["github","slack"]}'
# → 204 No Content

POST /api/env-setup/mcp/{mcpId}/file/{fieldKey} (multipart, v0.11.0+)

Upload a secret file (Service Account JSON, Apple .p8, OAuth client.json) for an MCP entry whose configFields[].isFile is true.

curl -X POST http://localhost:17880/api/env-setup/mcp/google-play-publisher/file/GOOGLE_PLAY_SERVICE_ACCOUNT_JSON \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@./play-sa.json"
# → {"path":"/home/vibe/.config/mcp-secrets/google-play-publisher-GOOGLE_PLAY_SERVICE_ACCOUNT_JSON.json"}

Then pass the returned path as the value for that config key in the subsequent POST /api/env-setup/mcp/install:

curl -X POST http://localhost:17880/api/env-setup/mcp/install \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "selections": {
      "google-play-publisher": {
        "GOOGLE_PLAY_SERVICE_ACCOUNT_JSON": "/home/vibe/.config/mcp-secrets/google-play-publisher-GOOGLE_PLAY_SERVICE_ACCOUNT_JSON.json",
        "GOOGLE_PLAY_PACKAGE_NAME": "com.siamakerlab.myapp"
      }
    }
  }'
  • File saved with 0600 permissions in ${CLAUDE_CONFIG_DIR}/mcp-secrets/.
  • 128 KB size cap (normal Service Account / .p8 is a few KB).
  • Filename sanitized — only [A-Za-z0-9._-] in the stored name.
  • Re-upload to replace (atomic move).

Git integrations (v0.10.0)

GET /api/settings/git-integrations

List registered PATs (masked) + SSH public key.

curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:17880/api/settings/git-integrations
{
  "tokens": [
    {"provider":"github","host":"github.com","username":"x-access-token","tokenMasked":"••••••••wxyz","createdAt":"2026-05-23T12:00:00Z","note":null}
  ],
  "sshPublicKey": "ssh-ed25519 AAAA... vibe-coder"
}

POST /api/settings/git-integrations

curl -X POST http://localhost:17880/api/settings/git-integrations \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "provider":"github",
    "host":"github.com",
    "username":"x-access-token",
    "token":"ghp_...",
    "note":"vibe-coder PAT, expires 2026-12"
  }'
# → 204 No Content

POST /api/settings/git-integrations/delete

curl -X POST http://localhost:17880/api/settings/git-integrations/delete \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"host":"github.com"}'

POST /api/settings/git-integrations/ssh-keygen

Generate ed25519 key pair if missing. Returns the full state including the public key for the user to copy.


Error response shape

{
  "code": "missing_clone_url",
  "message": "sourceType=clone 일 때 cloneUrl 이 필수입니다.",
  "detail": null
}

HTTP status codes: 400 bad input, 401 no/invalid token, 403 insufficient permission, 404 not found, 409 conflict, 413 payload too large, 502 upstream failed (git clone, npm install), 504 timeout.

Clone this wiki locally