{"openapi":"3.1.0","info":{"title":"Percher API","version":"1.0.0","description":"AI-native hosting platform. Deploy apps via CLI, MCP, or API — live at name.percher.run.","contact":{"url":"https://percher.app"}},"servers":[{"url":"https://api.percher.run","description":"Production"},{"url":"http://localhost:3000","description":"Local development"}],"components":{"securitySchemes":{"bearer":{"type":"http","scheme":"bearer","description":"API token from `percher login`"},"internalSecret":{"type":"apiKey","in":"header","name":"X-Internal-Secret","description":"Internal service secret"}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"requestId":{"type":"string"}},"required":["code","message"]}}},"App":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"runtime":{"type":"string"},"framework":{"type":"string","nullable":true},"status":{"type":"string","enum":["provisioning","live","crashed","suspended"]},"url":{"type":"string","format":"uri"},"dataMode":{"type":"string","enum":["none","pocketbase","convex","supabase"]},"createdAt":{"type":"string","format":"date-time"}}},"Deployment":{"type":"object","properties":{"id":{"type":"string"},"appId":{"type":"string"},"status":{"type":"string","enum":["queued","building","deploying","live","failed"]},"type":{"type":"string","enum":["live","preview"]},"previewSlug":{"type":"string","nullable":true},"buildLog":{"type":"string","nullable":true},"errorMessage":{"type":"string","nullable":true},"errorCode":{"type":"string","nullable":true,"enum":["WORKER_UNAVAILABLE","WORKER_UNAVAILABLE_PERSISTENT","BUILD_TIMEOUT","BUILD_FAILED","WORKER_ERROR","DEPLOY_STALLED"]},"containerId":{"type":"string","nullable":true},"commitSha":{"type":"string","nullable":true},"expiresAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"},"finishedAt":{"type":"string","format":"date-time","nullable":true}}},"Version":{"type":"object","properties":{"sha":{"type":"string"},"message":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}}}}},"paths":{"/health":{"get":{"tags":["System"],"summary":"Health check","responses":{"200":{"description":"OK"}}}},"/metrics":{"get":{"tags":["System"],"summary":"Prometheus metrics","description":"Returns metrics in Prometheus exposition format. May require X-Internal-Secret header.","responses":{"200":{"description":"Prometheus metrics","content":{"text/plain":{}}},"401":{"description":"Unauthorized"}}}},"/catalog/templates":{"get":{"tags":["Catalog"],"summary":"List available scaffolding templates","description":"Returns TemplateMetadata entries for every bundled template. No authentication required.","responses":{"200":{"description":"Array of TemplateMetadata","content":{"application/json":{}}}}}},"/catalog/integrations":{"get":{"tags":["Catalog"],"summary":"List first-party integration guides","description":"Returns IntegrationGuide entries (Stripe, Resend, OpenAI, Anthropic, Supabase, etc.) with the env vars each service needs and setup URLs. No authentication required. Not a plugin/marketplace — just content the dashboard renders alongside the env var editor.","responses":{"200":{"description":"Array of IntegrationGuide","content":{"application/json":{}}}}}},"/auth/device/code":{"post":{"tags":["Auth"],"summary":"Start device code flow","responses":{"200":{"description":"Device code issued","content":{"application/json":{"schema":{"type":"object","properties":{"deviceCode":{"type":"string"},"userCode":{"type":"string"},"expiresIn":{"type":"number"}}}}}}}}},"/auth/device/token":{"post":{"tags":["Auth"],"summary":"Exchange device code for token","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"deviceCode":{"type":"string"}},"required":["deviceCode"]}}}},"responses":{"200":{"description":"Token issued"},"410":{"description":"Code expired or consumed"},"428":{"description":"Authorization pending"}}}},"/auth/whoami":{"get":{"tags":["Auth"],"summary":"Get current user","security":[{"bearer":[]}],"responses":{"200":{"description":"Current user info"}}}},"/apps/{appRef}/versions":{"get":{"tags":["Versions"],"summary":"List deploy history","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deploy history (commits)","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Version"}}}}}}}},"/apps/{appRef}/rollback":{"post":{"tags":["Versions"],"summary":"Rollback to previous version","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"commitSha":{"type":"string","minLength":1}},"required":["commitSha"]}}}},"responses":{"202":{"description":"Rollback queued"},"404":{"description":"Commit not found"}}}},"/apps/{appRef}/versions/{sha}/build-log":{"get":{"tags":["Versions"],"summary":"Get build log for specific version","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}},{"name":"sha","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Build log with status"},"404":{"description":"Not found"}}}},"/apps/{appRef}/versions/{sha}/config":{"get":{"tags":["Versions"],"summary":"Get percher.toml for specific version","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}},{"name":"sha","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"percher.toml as plain text"},"404":{"description":"Not found"}}}},"/apps/{appRef}/versions/{sha}/manifest":{"get":{"tags":["Versions"],"summary":"Get deploy manifest for specific version","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}},{"name":"sha","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Manifest JSON"},"404":{"description":"Not found"}}}},"/account/export":{"get":{"tags":["Account"],"summary":"Download all stored user data (GDPR Article 20)","description":"Returns a JSON blob with the user record, API tokens (metadata only), alert preferences, shared access grants (outgoing + incoming), and all owned apps with deploy history, env var keys, and custom domains. Env var values are excluded (encrypted at rest). Build logs are excluded due to size — fetch via the logs API if needed.","security":[{"bearer":[]}],"responses":{"200":{"description":"Export JSON","content":{"application/json":{}}},"401":{"description":"Unauthorized"}}}},"/account":{"delete":{"tags":["Account"],"summary":"Delete the user account and all owned data","description":"Permanently deletes the user row. Cascades to apps, deploys, env vars, custom domains, API tokens, alert preferences, and shared access grants. Requires `?confirm=<your-email>` to avoid accidental triggers.","security":[{"bearer":[]}],"parameters":[{"name":"confirm","in":"query","required":true,"schema":{"type":"string"},"description":"Must match the authenticated user's email address."}],"responses":{"204":{"description":"Account deleted"},"400":{"description":"Missing or mismatched confirmation"},"401":{"description":"Unauthorized"}}}},"/alerts/preferences":{"get":{"tags":["Alerts"],"summary":"Get the user's alert preferences","description":"Returns boolean flags for email channels (crash, deploy-fail, domain expiry, weekly summary) plus the current webhook URL and a `hasWebhookSecret` boolean. The signing secret itself is never returned here — only on PUT when a new URL is saved.","security":[{"bearer":[]}],"responses":{"200":{"description":"Alert preferences (secret omitted)","content":{"application/json":{}}},"401":{"description":"Unauthorized"}}},"put":{"tags":["Alerts"],"summary":"Update alert preferences","description":"Upserts the user's preferences. If `webhookUrl` is set (or changed), a new HMAC-SHA256 signing secret is generated and returned once in the response as `webhookSecret`. It is never shown again — the user must copy it into their receiver. Subsequent saves without a URL change keep the existing secret; saving a different URL rotates it; clearing the URL clears the secret.\n\nWebhook payload format (POST to the configured URL):\n- Body: `{ type, id, timestamp, data }` (JSON)\n- Headers: `X-Percher-Event`, `X-Percher-Delivery`, `X-Percher-Timestamp`, `X-Percher-Signature: sha256=<hex>`\n- Signature = HMAC-SHA256(secret, `${timestamp}.${body}`)\n- Event types: `deploy.failed`, `app.crashed`, `app.unhealthy`, `app.recovered`, `domain.expiring`","security":[{"bearer":[]}],"requestBody":{"content":{"application/json":{}}},"responses":{"200":{"description":"Updated preferences. If a new secret was generated, it is included as `webhookSecret`.","content":{"application/json":{}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"}}}},"/apps":{"get":{"tags":["Apps"],"summary":"List apps","security":[{"bearer":[]}],"responses":{"200":{"description":"List of apps","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/App"}}}}}}},"post":{"tags":["Apps"],"summary":"Create app","security":[{"bearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","pattern":"^[a-z][a-z0-9-]{2,39}$","description":"3-40 chars, lowercase, start with letter"},"runtime":{"type":"string"},"framework":{"type":"string"}},"required":["name","runtime"]}}}},"responses":{"201":{"description":"App created"},"400":{"description":"Invalid name or name blocked"},"403":{"description":"App limit reached or account suspended"},"409":{"description":"Name already taken"}}}},"/apps/{appRef}":{"get":{"tags":["Apps"],"summary":"Get app details","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"App details"},"404":{"description":"App not found"}}},"delete":{"tags":["Apps"],"summary":"Delete app","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"App deleted"},"409":{"description":"Deploy in progress"}}}},"/apps/{appRef}/rename":{"post":{"tags":["Apps"],"summary":"Rename app","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}}}},"responses":{"200":{"description":"App renamed"},"400":{"description":"Invalid name"},"409":{"description":"Name taken or reserved"}}}},"/apps/{appRef}/unsuspend":{"post":{"tags":["Apps"],"summary":"Unsuspend app","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"App unsuspended"},"403":{"description":"App was admin-suspended"}}}},"/apps/{appRef}/uptime":{"get":{"tags":["Apps"],"summary":"Get uptime stats","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Uptime rollup and recent probe history"}}}},"/apps/{appRef}/requests":{"get":{"tags":["Apps"],"summary":"Get request analytics","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Request counts and error rates"}}}},"/apps/{appRef}/crash-report":{"get":{"tags":["Crash Reports"],"summary":"Get latest crash report","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Latest crash report"}}}},"/apps/{appRef}/crash-reports":{"get":{"tags":["Crash Reports"],"summary":"List recent crash reports","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Array of recent crash reports (up to 10)"}}}},"/apps/{appRef}/crash-analysis":{"get":{"tags":["Crash Reports"],"summary":"Get AI crash analysis for latest crash","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Crash analysis result"}}}},"/apps/{appRef}/diagnostics":{"get":{"tags":["Diagnostics"],"summary":"Get app diagnostics","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Comprehensive diagnostics"}}}},"/apps/{appRef}/logs":{"get":{"tags":["Logs"],"summary":"Get app logs","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}},{"name":"lines","in":"query","schema":{"type":"integer","default":200}},{"name":"level","in":"query","schema":{"type":"string","enum":["debug","info","warn","error","fatal"]}},{"name":"source","in":"query","schema":{"type":"string","enum":["build","runtime","crash","system"]}},{"name":"search","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Log lines"}}}},"/apps/{appRef}/logs/stream":{"get":{"tags":["Logs"],"summary":"Stream live logs (SSE)","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Server-Sent Events stream"}}}},"/apps/{appRef}/deploys":{"post":{"tags":["Deploys"],"summary":"Deploy app","description":"Upload a tarball to deploy. Returns 202 and enqueues the build.","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}},{"name":"type","in":"query","schema":{"type":"string","enum":["live","preview"],"default":"live"}}],"requestBody":{"required":true,"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"responses":{"202":{"description":"Deploy queued"},"413":{"description":"Tarball too large (max 500 MB)"},"429":{"description":"Deploy rate limit exceeded"}}},"get":{"tags":["Deploys"],"summary":"List recent deployments","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","default":20,"maximum":100}},{"name":"type","in":"query","schema":{"type":"string","enum":["live","preview"]}}],"responses":{"200":{"description":"Array of deployments","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Deployment"}}}}}}}},"/apps/{appRef}/deploys/{deployId}":{"get":{"tags":["Deploys"],"summary":"Get deployment status","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}},{"name":"deployId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deployment details"}}}},"/apps/{appRef}/deploys/{deployId}/build-log":{"get":{"tags":["Deploys"],"summary":"Get raw build log for deployment","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}},{"name":"deployId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Build log as plain text"},"404":{"description":"Deployment not found"}}}},"/apps/{appRef}/deploys/{deployId}/source":{"get":{"tags":["Deploys"],"summary":"Download source tarball for deployment","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}},{"name":"deployId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Source tarball (application/x-gzip)"},"404":{"description":"Deployment not found"},"410":{"description":"Tarball retention-evicted"}}}},"/apps/{appRef}/deploys/{deployId}/events":{"get":{"tags":["Deploys"],"summary":"Get deploy stage timeline","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}},{"name":"deployId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Array of deploy events"}}}},"/apps/{appRef}/deploys/{deployId}/promote":{"post":{"tags":["Deploys"],"summary":"Promote preview to live","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}},{"name":"deployId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Preview promoted"},"400":{"description":"Not promotable or expired"}}}},"/apps/{appRef}/deploys/{deployId}/discard":{"post":{"tags":["Deploys"],"summary":"Discard preview","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}},{"name":"deployId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Preview discarded"}}}},"/apps/{appRef}/deploys/{deployId}/rerun":{"post":{"tags":["Deploys"],"summary":"Re-run a deployment from existing tarball","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}},{"name":"deployId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"201":{"description":"Rerun queued"},"404":{"description":"Deployment not found"},"410":{"description":"Tarball retention-evicted"}}}},"/apps/{appRef}/data/status":{"get":{"tags":["Data"],"summary":"Get data backend status","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Data mode, status, and URLs"}}}},"/apps/{appRef}/data/collections":{"get":{"tags":["Data"],"summary":"List PocketBase collections","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Array of collections"}}}},"/apps/{appRef}/data/collections/{collection}/records":{"get":{"tags":["Data"],"summary":"List records in collection","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}},{"name":"collection","in":"path","required":true,"schema":{"type":"string"}},{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"perPage","in":"query","schema":{"type":"integer","default":20,"maximum":100}}],"responses":{"200":{"description":"Paginated records"}}}},"/apps/{appRef}/data/stats":{"get":{"tags":["Data"],"summary":"Get PocketBase statistics","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Collection count and total records"}}}},"/apps/{appRef}/data/admin-link":{"get":{"tags":["Data"],"summary":"Get PocketBase admin link","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Admin URL and availability"}}}},"/apps/{appRef}/data/reset-superuser":{"post":{"tags":["Data"],"summary":"Rotate the PocketBase superuser password","description":"Generates a new PB superuser password, re-injects it as the encrypted `POCKETBASE_ADMIN_PASSWORD` env var, and updates the latest deployment's stored credential so subsequent /apps/{appRef}/data/* calls keep working. By default the response only contains the rotation confirmation and email — the plaintext password never appears in the response body. Pass `?reveal=true` to additionally return the plaintext once (same one-time-show pattern as token creation). Audit log records method/path/status/userId only, never response bodies, so revealed plaintext does not land in /var/log.","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}},{"name":"reveal","in":"query","required":false,"schema":{"type":"boolean"},"description":"Set to `true` to include the plaintext password in the response. Default `false`."}],"responses":{"200":{"description":"Rotation succeeded. `password` is present only when `?reveal=true` was set.","content":{"application/json":{"schema":{"type":"object","required":["message","email"],"properties":{"message":{"type":"string"},"email":{"type":"string","format":"email"},"password":{"type":"string","description":"Plaintext PB superuser password. Present only when invoked with `?reveal=true`."}}}}}},"400":{"description":"App is not configured for PocketBase (NOT_POCKETBASE)"},"500":{"description":"Failed to create or reset superuser (PB_SUPERUSER_FAILED)"},"503":{"description":"PocketBase sidecar is not running (PB_NOT_RUNNING)"}}}},"/apps/{appRef}/pb-password-ack":{"post":{"tags":["Data"],"summary":"Acknowledge PocketBase admin password","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Password cleared"}}}},"/apps/{appRef}/deprovision-data":{"post":{"tags":["Data"],"summary":"Delete PocketBase data permanently","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"confirm":{"type":"boolean","const":true}},"required":["confirm"]}}}},"responses":{"204":{"description":"Data deprovisioned"}}}},"/apps/{appRef}/jobs":{"get":{"tags":["Jobs"],"summary":"List jobs for app","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","default":20,"maximum":100}}],"responses":{"200":{"description":"Array of job summaries"}}}},"/apps/{appRef}/jobs/{jobId}":{"get":{"tags":["Jobs"],"summary":"Get job details","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}},{"name":"jobId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Job details"},"404":{"description":"Not found"}}}},"/apps/{appRef}/env":{"get":{"tags":["Environment"],"summary":"List env vars (masked values)","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Masked env vars"}}},"put":{"tags":["Environment"],"summary":"Set env vars","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string","maxLength":65536},"maxProperties":100}}}},"responses":{"204":{"description":"Env vars updated"}}}},"/apps/{appRef}/env/{key}":{"delete":{"tags":["Environment"],"summary":"Delete env var","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}},{"name":"key","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Env var deleted"}}}},"/apps/{appRef}/domains":{"get":{"tags":["Domains"],"summary":"List custom domains","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Custom domains"}}},"post":{"tags":["Domains"],"summary":"Add custom domain","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"domain":{"type":"string"}},"required":["domain"]}}}},"responses":{"201":{"description":"Domain added with DNS instructions"}}}},"/apps/{appRef}/domains/verify":{"post":{"tags":["Domains"],"summary":"Verify domain DNS","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"domain":{"type":"string"}},"required":["domain"]}}}},"responses":{"200":{"description":"Verification result"}}}},"/apps/{appRef}/domains/{domain}":{"delete":{"tags":["Domains"],"summary":"Remove custom domain","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}},{"name":"domain","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Domain removed"}}}},"/admin/health":{"get":{"tags":["Admin"],"summary":"System health check with stats","security":[{"internalSecret":[]}],"responses":{"200":{"description":"Health status with component checks and stats"}}}},"/admin/jobs":{"get":{"tags":["Admin"],"summary":"List all recent jobs","security":[{"internalSecret":[]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":50}}],"responses":{"200":{"description":"Jobs with full payload"}}}},"/admin/jobs/stats":{"get":{"tags":["Admin"],"summary":"Job queue statistics","security":[{"internalSecret":[]}],"responses":{"200":{"description":"Counts by status"}}}},"/admin/jobs/{jobId}/retry":{"post":{"tags":["Admin"],"summary":"Retry failed/dead job","security":[{"internalSecret":[]}],"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Job re-enqueued"},"409":{"description":"Invalid state"}}}},"/admin/jobs/{jobId}/cancel":{"post":{"tags":["Admin"],"summary":"Cancel pending/failed job","security":[{"internalSecret":[]}],"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Job cancelled"},"409":{"description":"Invalid state"}}}},"/internal/users/create":{"post":{"tags":["Internal"],"summary":"Create user (dashboard → API)","security":[{"internalSecret":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"},"termsAccepted":{"type":"boolean"}},"required":["email"]}}}},"responses":{"200":{"description":"User created"},"409":{"description":"Email taken"}}}},"/internal/users/{userId}/accept-terms":{"post":{"tags":["Internal"],"summary":"Mark user as accepted terms","security":[{"internalSecret":[]}],"parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Terms accepted"}}}},"/internal/sessions/issue-token":{"post":{"tags":["Internal"],"summary":"Issue API token for user","security":[{"internalSecret":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string"},"name":{"type":"string"}},"required":["userId","name"]}}}},"responses":{"200":{"description":"Token issued"}}}},"/internal/sessions/revoke-token":{"post":{"tags":["Internal"],"summary":"Revoke API token","security":[{"internalSecret":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"tokenId":{"type":"string"}},"required":["tokenId"]}}}},"responses":{"204":{"description":"Token revoked"}}}},"/internal/device-codes/authorize":{"post":{"tags":["Internal"],"summary":"Authorize device code","security":[{"internalSecret":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"userCode":{"type":"string"},"userId":{"type":"string"}},"required":["userCode","userId"]}}}},"responses":{"204":{"description":"Authorized"},"404":{"description":"Code not found"}}}},"/abuse-reports":{"post":{"tags":["Abuse"],"summary":"Report abuse","description":"Public endpoint, rate-limited to 5/hour per IP.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"reason":{"type":"string","enum":["phishing","malware","illegal","spam","abuse","other"]},"details":{"type":"string","maxLength":2000},"reporterEmail":{"type":"string","format":"email"}},"required":["url","reason"]}}}},"responses":{"201":{"description":"Report received"},"429":{"description":"Rate limit exceeded"}}}},"/apps/{appRef}/restore":{"post":{"tags":["Restore"],"summary":"Start a restore job","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"backupId":{"type":"string"},"components":{"type":"array","items":{"type":"string"}}},"required":["backupId"]}}}},"responses":{"202":{"description":"Restore job started"},"409":{"description":"Restore already in progress"}}},"get":{"tags":["Restore"],"summary":"Get restore job status","security":[{"bearer":[]}],"parameters":[{"name":"appRef","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Current restore job status"}}}}}}