================================================================================ OVERSKILL API - FULL AGENT DOCUMENTATION Version: 1.5 | Updated: 2026-08-29 ================================================================================ OVERVIEW -------- Overskill is a platform for generating, deploying, and managing full-stack web apps via API. Submit a plain-English brief, inspect the generated app, and iterate before publishing. The platform provides frontend generation, database and authentication capabilities, and deployment tools. An app's implemented features depend on its brief and configuration. API and MCP generation require authentication, builder access, and sufficient credits; connected services may require separate setup and authorization. Review generated behavior and permissions before publishing. Build briefs, template evaluation, comparisons, and launch checks: https://www.overskill.com/learn Current plans, credits, and access details: https://www.overskill.com/pricing USER-FACING HELP (for the people your agent serves) --------------------------------------------------- Overskill ships 40+ help articles that explain concepts in plain language — credits, refunds, preview vs live, custom domains, payments, etc. Useful when your agent's user asks a non-API question. Browse: https://www.overskill.com/help/categories Markdown: https://www.overskill.com/help/articles/.md Search: https://www.overskill.com/help/articles?q= The .md suffix returns raw markdown (Content-Type: text/markdown) with no HTML chrome — ideal for agent consumption. AUTHENTICATION -------------- All requests require authentication. Two methods are supported: 1. API Key (recommended): X-API-Key: os_your_key_here 2. OAuth 2.0 Bearer Token: Authorization: Bearer Keys start with "os_". Get yours at: https://www.overskill.com/account (Team Settings > API Keys) OAuth discovery: GET https://www.overskill.com/.well-known/oauth-authorization-server Client ID Metadata Documents (CIMD) are supported and preferred: client_id_metadata_document_supported: true Authorize with your https metadata-document URL as client_id — we fetch it, check that it names itself, and match your declared redirect_uris. Nothing to register, nothing to store. Dynamic client registration (RFC 7591 public PKCE) is also live: registration_endpoint: https://www.overskill.com/oauth/register We mint a public PKCE client whatever token_endpoint_auth_method you ask for, and echo what you actually got (RFC 7591 3.2.1). MCP hosts (Cursor, Claude, ChatGPT, …) connect with one of those two automatically; both end in the same consent screen and PKCE (S256). BASE URL -------- https://www.overskill.com/api/v1 Always use the www host. The apex domain (https://overskill.com/...) 301/307-redirects to www; clients that don't follow redirects get an empty redirect response instead of the API response. URLs returned BY the API (claim/editor/upgrade links) may use the apex host — those are browser-safe. ================================================================================ ENDPOINTS ================================================================================ -------------------------------------------------------------------------------- POST /api/v1/generation_queue Generate a new app or iterate on an existing one. -------------------------------------------------------------------------------- Request body: { "prompt": "Build a task management app" (required), "app_id": "QjqEkj" (optional - update existing app), "name": "My App" (optional - app name), "callback_url": "https://..." (optional - webhook on completion), "ai_model": "claude-opus-4-8" (optional - default: claude-opus-4-8), "metadata": {} (optional - custom keys passed through to callbacks) } Response 202 Accepted: { "job_id": "abc123", "app_id": "QjqEkj", "message_id": 456, "status": "queued", "estimated_time_seconds": 45, "webhook_events": ["app.generation.started", "app.generation.completed", "app.generation.failed"], "status_url": "https://www.overskill.com/api/v1/generation_queue/abc123", "app_url": "https://www.overskill.com/account/apps/QjqEkj/edit" } Errors: 400 (no prompt), 401 (bad key), 403 (no generation:queue permission), 429 (rate limited) -------------------------------------------------------------------------------- GET /api/v1/generation_queue/:id Check generation status by job ID or message ID. -------------------------------------------------------------------------------- Response 200: { "job_id": "abc123", "app_id": "QjqEkj", "status": "processing" | "completed" | "failed" | "cancelled", "progress": 10 | 25 | 50 | 75 | 100, "message": "Generating your app...", "app": { "id": "QjqEkj", "name": "Task Manager", "status": "generated", "preview_url": "https://preview-QjqEkj.overskill.app", "production_url": null, "created_at": "2026-02-18T12:00:00Z" }, "started_at": "2026-02-18T12:00:00Z", "completed_at": "2026-02-18T12:00:45Z" } Status values: queued, processing, completed, failed, cancelled Progress values: 5 (queued), 10 (executing), 25 (planning), 50 (generating), 75 (processing), 100 (complete) NOTE: The "progress" field uses coarse checkpoints and may stay at 10 for most of the build. The "message" field provides more useful real-time status text describing what the AI is currently doing. Prefer "message" for user-facing progress indicators. -------------------------------------------------------------------------------- POST /api/v1/managed_apps Create a new app (optionally queue generation). -------------------------------------------------------------------------------- Request body: { "name": "My App" (optional - default: "API Created App"), "prompt": "Build a..." (optional - triggers generation if provided), "ai_model": "claude-opus-4-8" (optional), "visibility": "public" | "login_required" (optional - default: "public"), "auto_generate": true | false (optional - default: true if prompt provided) } Response 201: { "app": { "id": "QjqEkj", "name": "My App", "status": "generating", "preview_url": null, "production_url": null, "created_at": "2026-02-18T12:00:00Z" }, "message": "App created and generation queued" } -------------------------------------------------------------------------------- GET /api/v1/managed_apps List all apps for your account. -------------------------------------------------------------------------------- Query params: status - filter by status (draft, generating, generated, published, failed) name - search by name (partial match) page - page number (default: 1) per_page - results per page (default: 25, max: 100) Response 200: { "apps": [ { "id": "QjqEkj", "name": "My App", "description": "...", "status": "generated", "visibility": "public", "ai_model": "claude-opus-4-8", "preview_url": "https://preview-QjqEkj.overskill.app", "production_url": "https://QjqEkj.overskill.app", "published_at": "2026-02-18T...", "created_at": "2026-02-18T...", "updated_at": "2026-02-18T..." } ], "meta": { "current_page": 1, "total_pages": 3, "total_count": 52, "per_page": 25 } } -------------------------------------------------------------------------------- GET /api/v1/managed_apps/:id Get detailed info about a single app. -------------------------------------------------------------------------------- Response 200: { "app": { "id": "QjqEkj", "name": "My App", "description": "...", "status": "generated", "visibility": "public", "ai_model": "claude-opus-4-8", "preview_url": "...", "production_url": "...", "creator": { "id": 123, "email": "user@example.com" }, "versions_count": 5, "files_count": 24, "users_count": 12, "urls": { "editor": "https://www.overskill.com/account/apps/QjqEkj/edit", "preview": "...", "production": "..." } } } -------------------------------------------------------------------------------- GET /api/v1/managed_apps/:id/status Deployment and generation status with live URLs. -------------------------------------------------------------------------------- Response 200: { "app_id": "QjqEkj", "name": "My App", "status": "generated", "deployment": { "id": 456, "environment": "production", "status": "deployed", "deployed_at": "2026-02-18T..." }, "generation": { "status": "complete", "started_at": "2026-02-18T...", "completed_at": "2026-02-18T..." }, "urls": { "preview": "https://preview-QjqEkj.overskill.app", "production": "https://QjqEkj.overskill.app", "editor": "https://www.overskill.com/account/apps/QjqEkj/edit" } } -------------------------------------------------------------------------------- GET /api/v1/managed_apps/:id/files Get all source files for an app. -------------------------------------------------------------------------------- Query params: path - filter files by path (e.g., "src/pages" returns files containing that path) Response 200: { "app_id": "QjqEkj", "files": [ { "path": "src/App.tsx", "content": "import React from 'react'...", "size": 1234, "updated_at": "2026-02-18T12:00:00Z" } ], "count": 24 } Per-app manifest (AGENTS.md): Every app carries a generated AGENTS.md at its repo root — the agent entry point for operating that one app. Fetch it with the files API: GET /api/v1/managed_apps/:id/files?path=AGENTS.md It is synthesized from the app's LIVE config and regenerated on every deploy and config change, so it never drifts from what's running. It documents the app's ID and live URLs, the deploy loop (deploy / status / errors below), environment variable KEYS (names only — never secret values), connected integrations, OAuth providers, custom domains, custom endpoints, scheduled tasks, and the data model — listing only capabilities actually enabled for the app. Companion files CLAUDE.md (pointer) and SKILL.md (operator skill) sit beside it; all three ship in the GitHub export, so a cloned repo is self-describing. (Rolling out behind a flag — if an app has no AGENTS.md yet, use the deploy loop documented here directly; it's identical.) -------------------------------------------------------------------------------- GET /api/v1/managed_apps/:id/errors Get actionable build errors for an app (self-heal contract). -------------------------------------------------------------------------------- Read-only. Wraps the platform's build-error telemetry so an external agent can fix-and-redeploy instead of retrying blind. Response 200 (clean app): { "recoverable": true, "errors": [] } Response 200 (build error recorded): { "recoverable": true, "errors": [ { "message": "Cannot resolve module 'fs'", "error_type": "build_error", "file": "src/lib/storage.ts", "suggested_action": "A module import can't be resolved. Add the missing dependency or fix the import path, then redeploy.", "fix_url": "https://www.overskill.com/account/apps/QjqEkj/edit", "recoverable": true, "occurred_at": "2026-06-08T12:00:00Z" } ] } Use each error's suggested_action to fix the offending file (via the files API or PATCH), then POST .../deploy again. -------------------------------------------------------------------------------- PATCH /api/v1/managed_apps/:id Update app settings. -------------------------------------------------------------------------------- Request body: { "name": "New Name" (optional), "description": "Updated desc" (optional), "visibility": "public" | "login_required" (optional), "ai_model": "claude-opus-4-8" (optional) } Response 200: { "app": { ...updated fields } } Visibility values: "public" - Anyone can access, no login required (default) "login_required" - Users must log in with Overskill before accessing NOTE: When visibility is "login_required", both preview and production URLs will show an Overskill login page. Users must authenticate before they can see the app. Changing visibility takes effect immediately (no redeployment needed). -------------------------------------------------------------------------------- DELETE /api/v1/managed_apps/:id Delete an app and all its resources. -------------------------------------------------------------------------------- Response 200: { "success": true, "message": "App deleted" } -------------------------------------------------------------------------------- POST /api/v1/managed_apps/:id/deploy Deploy an app to production. -------------------------------------------------------------------------------- Response 202: { "deployment_id": 789, "status": "queued", "message": "Production deployment queued" } Response 422 (guarded — app is not deployable): Deploy is guarded. A draft or un-built app (no dist/ bundle) is refused with 422 instead of being queued, so you never confidently ship a broken app: { "error": "App is not deployable", "reason": "No built files found (expected a dist/ bundle) — the app must be built before it can deploy.", "recoverable": true, "suggested_action": "Build the app (generate content or run a preview build so a dist/ bundle exists), then retry POST /api/v1/managed_apps/:id/deploy.", "fix_url": "https://www.overskill.com/account/apps/QjqEkj/edit" } On a 422, generate/build the app (e.g. POST /api/v1/generation_queue) until a dist/ bundle exists, then retry the deploy. Note: The deployment is created with internal status "pending". The response returns "queued" for simplicity. When checking deployment status via GET .../status, you will see the actual status: pending, building, deploying, deployed, or failed. If a build fails, fetch GET /api/v1/managed_apps/:id/errors for an actionable, self-heal-able error list. -------------------------------------------------------------------------------- POST /api/v1/managed_apps/:id/rollback Rollback to a previous version. -------------------------------------------------------------------------------- Request body: { "version_id": 123 } (optional - defaults to previous version) Response 200: { "message": "Rollback initiated", "version_id": 123 } -------------------------------------------------------------------------------- GET /api/v1/managed_apps/:id/analytics App analytics summary. -------------------------------------------------------------------------------- Query params: period - time period (7d, 30d, 90d; default: 7d) Response 200: { "app_id": "QjqEkj", "period": "7d", "metrics": { "page_views": 1234, "unique_visitors": 456, "sessions": 789 } } ================================================================================ WEBHOOK MANAGEMENT ================================================================================ -------------------------------------------------------------------------------- GET /api/v1/webhooks - List webhook subscriptions POST /api/v1/webhooks - Create webhook subscription GET /api/v1/webhooks/:id - Get webhook details PATCH /api/v1/webhooks/:id - Update webhook DELETE /api/v1/webhooks/:id - Delete webhook POST /api/v1/webhooks/:id/test - Send test event GET /api/v1/webhooks/:id/deliveries - View delivery history -------------------------------------------------------------------------------- Create webhook body: { "url": "https://your-server.com/webhook", "events": ["app.generation.completed", "app.deployment.completed"], "name": "My Webhook" (optional - default: "API Webhook"), "secret": "your_webhook_secret" (optional - used for signature verification), "description": "..." (optional) } Webhook events: app.created, app.updated, app.deleted app.generation.started, app.generation.completed, app.generation.failed app.generation.progress app.deployment.started, app.deployment.completed ================================================================================ MCP (MODEL CONTEXT PROTOCOL) ================================================================================ Two surfaces. 1) Hosted MCP (JSON-RPC / Streamable HTTP) — what Grok Bot, OpenClaw, Hermes Agent, Cursor, Claude, ChatGPT, and any MCP host speak. Connect URL: https://mcp.overskill.com/mcp Per-host add flow (verified against each host's docs): https://www.overskill.com/connect Sign-in is OAuth. CIMD (client_id = your metadata-document URL) or DCR: POST https://www.overskill.com/oauth/register — both advertised on /.well-known/oauth-authorization-server (client_id_metadata_document_supported / registration_endpoint). Tools: create_app / create_landing_page / create_presentation / create_internal_tool params: { "prompt": "...", "name": "..." (optional title, max 50), "model": "...", "thinking_level": "...", "attachment": "..." } Or put "App name: Offer Lab" on its own first line of prompt. Returns structuredContent: build_id, status, preview_url, editor_url. Poll get_build until status is ready. update_app params: { "build_id": "...", "prompt": "..." } list_apps params: { "query": "...", "limit": 1-50, "cursor": "" } Pages newest-first. Pass cursor from the previous next_cursor. get_app / get_build params: { "build_id": "..." } get_build elapsed_seconds is the current generate or update turn, not the app's lifetime. Ready payload includes screenshot_url, generation.ready, and files [{ name, mtime }] when that data exists. publish_app params: { "build_id": "..." } — confirm with the user first list_generation_models render_build_widget get_balance params: {} — balance, effective plan, and the packs that add to it fetch_credit_purchase_link params: { "pack_id": "...", "idempotency_key": "..." } Returns { url, pack, requires_human: true }. Hand the URL to the person and stop — nothing is charged and no agent can complete it. Reuse idempotency_key on a retry to get the same link back. 2) REST MCP (API key) — NOT the MCP wire protocol. File/entity tools live here only. POST /api/v1/mcp/execute_tool - Execute a REST MCP tool GET /api/v1/mcp/tools - List REST MCP tools Request body for execute_tool: { "tool": "overskill-generate-app", "app_id": "QjqEkj" (required for most tools, NOT needed for generate-app), "params": { ...tool-specific parameters } } REST tools: App Generation (no app_id required): overskill-generate-app - Create a new app from a prompt params: { "name": "My App", "prompt": "Build a...", "description": "...", "model": "..." } Returns: { "success": true, "app_id": "...", "status_url": "...", "preview_url": null } File Operations (require app_id): overskill-view-file - View file contents params: { "path": "src/App.tsx" } overskill-write-file - Create or update a file (triggers preview rebuild) params: { "path": "src/App.tsx", "content": "..." } overskill-list-files - List files matching a pattern params: { "pattern": "src/**/*.tsx" } overskill-delete-file - Delete a file (triggers preview rebuild) params: { "path": "src/old-file.tsx" } Entity Operations (require app_id): overskill-create-entity - Create a database table params: { "name": "tasks", "columns": [...], "user_scoping": "user_scoped" } overskill-query-data - Query records params: { "entity": "tasks", "filters": {...}, "order_by": "...", "limit": 10 } overskill-insert-data - Insert a record params: { "entity": "tasks", "data": {...} } overskill-update-data - Update a record params: { "entity": "tasks", "id": "...", "data": {...} } overskill-delete-data - Delete a record params: { "entity": "tasks", "id": "..." } -------------------------------------------------------------------------------- DELETE /api/v1/generation_queue/:id Cancel a queued or in-progress generation. -------------------------------------------------------------------------------- Response 200: { "cancelled": true, "job_id": "abc123" } -------------------------------------------------------------------------------- GET /api/v1/generation_queue/:id/stream Stream generation progress as Server-Sent Events (alternative to polling). -------------------------------------------------------------------------------- Emits the same status object as GET /api/v1/generation_queue/:id as SSE events until the job reaches a terminal state (completed/failed/cancelled). -------------------------------------------------------------------------------- GET /api/v1/capabilities What can this account do? Plan, allowed/blocked actions, limits. -------------------------------------------------------------------------------- Response 200: { "plan": "free" | "pro" | "team" | "enterprise", "can": ["generate_apps", "deploy_preview", ...], "cannot": [ { "action": "deploy_production", "reason": "Pro plan required", "unlock_url": "https://overskill.com/account/billing/upgrade?token=&capability=...", "required_tier": "pro", "upgrade_token": "", "api_endpoint": "POST /api/v1/account/upgrade" } ], "limits": { "apps_count": 3, "apps_used": 1, "generations_remaining": 10, "storage_gb": 1.0, "custom_domains_per_app": 0 }, "account": { "team_id": 258, "plan": "free", "email": "...", "credit_balance": 1000 } } -------------------------------------------------------------------------------- GET /api/v1/usage Credit balance, usage breakdown, API-key stats. -------------------------------------------------------------------------------- Response 200: { "team_id": 258, "period": "30d", "credits": { "balance": 1500, "used_in_period": 350, "added_in_period": 500, "breakdown": { "generation": 340, "deployment": 0, "integration": 10, "workflow": 0, "storage": 0 } }, "generations": { "total": 42, "apps_created": 8, "apps_deployed": 3 }, "api_key": { "key_name": "...", "key_prefix": "os_...", "total_requests": 120, "last_used_at": "..." }, "subscription": { "tier": "pro", "credit_tier": "tier_500" }, "next_steps": [] } ================================================================================ NO UNAUTHENTICATED BUILD (retired Sep 2026) ================================================================================ POST /api/v1/anonymous_builds — the old Bolt-style "build now, claim later" lane — is OFF and returns 404. There is no unauthenticated build path on any surface: /mcp requires OAuth, POST /api/v1/mcp/execute_tool requires an API key, and POST /build/anon requires signup before it builds. Nothing generates before an identity exists, because generation spends real credits. If the human has no Overskill account yet, do not try to build for them. Hand them a prefilled (unsent) homepage link and let them start it themselves: https://www.overskill.com/?prompt=URL_ENCODED_PROMPT The prompt arrives already typed in the homepage composer; they sign in (or sign up) and press build. To act on their behalf afterwards, get authenticated first — see AUTHENTICATION above, then POST /api/v1/generation_queue. Already holding a trial from before the retirement? The two read/claim endpoints still work for the remainder of its 48-hour window: GET /api/v1/anonymous_builds/:id — poll status (no auth) POST /api/v1/anonymous_builds/:id/claim — claim into your team (X-API-Key or Bearer; body { "claim_token": "..." }) Nothing mints a new one. ================================================================================ CALLBACK URL PATTERN ================================================================================ When queuing generation with callback_url, Overskill POSTs to your URL on completion or failure. Callback payload: { "event": "app.generation.completed" | "app.generation.failed", "app_id": "QjqEkj", "app_name": "My App", "status": "generated" | "failed", "urls": { "preview": "https://preview-QjqEkj.overskill.app", "production": null, "editor": "https://www.overskill.com/account/apps/QjqEkj/edit" }, "generation": { "message_id": 456, "started_at": "2026-02-18T12:00:00Z", "completed_at": "2026-02-18T12:00:45Z" }, "credits_used": 12.5, "tokens_used": { "input": 10000, "output": 5000 }, "metadata": { ...your custom metadata keys passed through } } Notes: - "credits_used" and "tokens_used" are only present when generation metrics are available (may be absent on some failures) - "metadata" includes your custom keys; internal keys like "api_key_id" may also appear — filter by your known keys Signature verification — TWO DIFFERENT SCHEMES: 1. REGISTERED WEBHOOKS (subscriptions created via POST /api/v1/webhooks, where you supplied your own `secret`): Header: X-Overskill-Signature: t=,v1= Header: X-Overskill-Timestamp: To verify: 1. Extract timestamp (t=) and signature (v1=) from X-Overskill-Signature 2. Compute: HMAC-SHA256(your_webhook_secret, ".") 3. Compare computed hex digest with the v1 value 4. Optionally check timestamp is within an acceptable window (e.g., 5 minutes) 2. CALLBACK_URL DELIVERIES (the per-request callback_url param above): Header: X-OverSkill-Signature: sha256= Signed with a PLATFORM-held key — there is currently no per-caller secret, so these cannot be verified cryptographically by the recipient. Treat callback payloads as untrusted hints: on receipt, confirm state with GET /api/v1/generation_queue/:id before acting. ================================================================================ POLLING PATTERN ================================================================================ 1. POST /api/v1/generation_queue -> get job_id and app_id 2. Wait 3 seconds 3. GET /api/v1/generation_queue/:job_id -> check status 4. If status == "processing", go to step 2 5. If status == "completed", use app.preview_url 6. If status == "failed", report error Typical generation time: 30-60 seconds. ================================================================================ RATE LIMITS ================================================================================ Recommended client-side limits (plan your integration with these for forward compatibility as server-side enforcement is being rolled out progressively): Generation (POST /generation_queue): 20 per hour per API key API reads (GET): 300 per minute per API key API writes (POST/PATCH/DELETE): 60 per minute per API key Deployments: 10 per hour per API key ================================================================================ ERROR FORMAT ================================================================================ All errors return JSON: { "error": "Error type", "message": "Human-readable description" } HTTP status codes: 400 - Bad request (invalid parameters) 401 - Unauthorized (missing/invalid API key) 403 - Forbidden (insufficient permissions) 404 - Not found 422 - Validation failed (with "errors" array) 429 - Rate limit exceeded 500 - Server error ================================================================================ API KEY PERMISSIONS ================================================================================ read_only: apps:read, users:read, analytics:read, webhooks:read read_write: + apps:write, users:write, generation:queue full_access: + apps:delete, users:delete, webhooks:write generation:queue permission is required for POST /generation_queue — it is granted at read_write scope and above. webhooks:write (creating / updating webhook subscriptions) requires full_access. ================================================================================ CUSTOM DOMAINS ================================================================================ Custom domains let you serve an Overskill app from your own domain (e.g., app.yourdomain.com) instead of {slug}.overskill.app. Manage via API or dashboard (Settings → Publish → Custom Domain). -------------------------------------------------------------------------------- GET /api/v1/managed_apps/:app_id/custom_domains List all custom domains for an app. -------------------------------------------------------------------------------- Response 200: { "custom_domains": [ { "id": 123, "domain": "app.yourdomain.com", "url": "https://app.yourdomain.com", "verification_status": "pending" | "verified" | "failed", "ssl_status": "pending" | "initializing" | "active" | "failed", "primary": false, "active": true, "operational": false, "dns_records_complete": true, // false while apex TXT records are still generating "created_at": "2026-02-28T..." } ], "cname_target": "proxy.overskill.app", "can_add_more": true } -------------------------------------------------------------------------------- POST /api/v1/managed_apps/:app_id/custom_domains Add a custom domain to an app. -------------------------------------------------------------------------------- Request body: { "domain": "app.yourdomain.com" } ⚠️ TWO-PHASE DNS RECORDS — READ THIS OR SSL WILL NEVER ISSUE ⚠️ `dns_records` in the create response is populated in TWO phases: Phase 1 (this response, synchronous): the CNAME record(s). Phase 2 (asynchronous, seconds later): for APEX/root domains ONLY, TWO additional TXT validation records that Cloudflare generates AFTER the hostname is created: - _cf-custom-hostname (domain-ownership DCV) - _acme-challenge (ACME / SSL certificate DCV) The apex TXT records are NOT in this create response yet. If you configure ONLY the CNAMEs you get back here, an apex domain's SSL stays stuck in `pending_validation` forever. You MUST poll GET .../custom_domains/:id (or POST .../refresh) and add each TXT record as it appears. Use the machine-readable `dns_records_complete` boolean to know when every record is present: - false → keep polling; more records (TXT) are still being generated. - true → `dns_records` now contains every record you need to add. Subdomains (e.g. app.yourdomain.com) only need a single CNAME, so their create response already has `dns_records_complete: true`. Response 201 (subdomain — complete immediately): { "custom_domain": { "id": 123, "domain": "app.yourdomain.com", "url": "https://app.yourdomain.com", "verification_status": "pending", "ssl_status": "pending", "operational": false, "is_apex": false, "dns_records_complete": true, "dns_records": [ { "type": "CNAME", "name": "app", "value": "proxy.overskill.app", "ttl": 300 } ] }, "dns_records": [ { "type": "CNAME", "name": "app", "value": "proxy.overskill.app", "ttl": 300 } ], "dns_records_complete": true, "ssl_validation": { "status": "records_ready", "dns_records_complete": true, "txt_records_pending": false, "poll_url": "https://www.overskill.com/api/v1/managed_apps/QjqEkj/custom_domains/123", "message": "All DNS records (including any required TXT validation records) are ready. Add every record listed in `dns_records` at your DNS host." }, "cname_target": "proxy.overskill.app", "poll_url": "https://www.overskill.com/api/v1/managed_apps/QjqEkj/custom_domains/123" } Response 201 (apex/root — TXT records still pending): { "custom_domain": { "id": 124, "domain": "yourdomain.com", "verification_status": "pending", "ssl_status": "pending", "operational": false, "is_apex": true, "dns_records_complete": false, // ← keep polling "dns_records": [ { "type": "CNAME", "name": "@", "value": "proxy.overskill.app", "ttl": 300, "recommended": true }, { "type": "CNAME", "name": "www", "value": "proxy.overskill.app", "ttl": 300, "recommended": true } // TXT records NOT here yet — they appear on a later poll ] }, "dns_records_complete": false, "ssl_validation": { "status": "pending", "dns_records_complete": false, "txt_records_pending": true, "poll_url": "https://www.overskill.com/api/v1/managed_apps/QjqEkj/custom_domains/124", "message": "This domain also requires TWO TXT validation records ... poll GET ... until `dns_records` includes the TXT records (dns_records_complete becomes true), then add ALL records (CNAMEs + both TXT). SSL will NOT issue until the TXT records are present." }, "next_steps": [ { "action": "configure_dns", "instructions": [ ...CNAMEs... ] }, { "action": "wait_for_txt_records", "completion_signal": "dns_records_complete=true", "poll_url": "...", "poll_interval_seconds": 30, "warning": "Setting only the CNAMEs will NOT provision SSL for an apex domain." }, { "action": "verify_domain", "completion_signal": "operational=true (dns_records_complete=true first for apex domains)" } ], "cname_target": "proxy.overskill.app", "poll_url": "https://www.overskill.com/api/v1/managed_apps/QjqEkj/custom_domains/124" } After creating, configure DNS at the registrar: Subdomains (e.g., app.yourdomain.com): Type: CNAME Name: app (or whatever subdomain they chose) Value: proxy.overskill.app TTL: 300 (or Auto) Apex/root domains (e.g., yourdomain.com) — add EVERY record in `dns_records` once `dns_records_complete` is true: Type: CNAME Name: @ Value: proxy.overskill.app (DNS-only / grey cloud if your zone is on Cloudflare) Type: CNAME Name: www Value: proxy.overskill.app Type: TXT Name: _cf-custom-hostname Value: (domain ownership) Type: TXT Name: _acme-challenge Value: (SSL certificate) (Fallback A records are also returned for hosts that can't CNAME at apex — only use them when your zone is NOT on Cloudflare; CF zones must use the CNAME.) DNS changes typically propagate in 5-15 minutes (up to 48 hours). -------------------------------------------------------------------------------- GET /api/v1/managed_apps/:app_id/custom_domains/:id Get detailed domain info including DNS records and errors. -------------------------------------------------------------------------------- Response 200 (apex domain, TXT records now generated — dns_records_complete true): { "custom_domain": { "id": 124, "domain": "yourdomain.com", "url": "https://yourdomain.com", "verification_status": "verified", "ssl_status": "active", "primary": true, "active": true, "operational": true, "is_apex": true, "dns_records_complete": true, "dns_records": [ { "type": "CNAME", "name": "@", "value": "proxy.overskill.app", "ttl": 300 }, { "type": "CNAME", "name": "www", "value": "proxy.overskill.app", "ttl": 300 }, { "type": "TXT", "name": "_cf-custom-hostname", "value": "", "ttl": 300 }, { "type": "TXT", "name": "_acme-challenge", "value": "", "ttl": 300 } ], "verified_at": "2026-02-28T...", "ssl_active_at": "2026-02-28T...", "verification_error": null, "ssl_error": null, "created_at": "2026-02-28T..." } } NOTE: `dns_records` is ALWAYS an array. For a subdomain it holds one CNAME; for an apex domain it grows from CNAMEs → CNAMEs + TXT records as Cloudflare generates the TXT validation records. Poll this endpoint until `dns_records_complete` is true, adding each record as it appears. -------------------------------------------------------------------------------- DELETE /api/v1/managed_apps/:app_id/custom_domains/:id Remove a custom domain. Cleans up Cloudflare hostname automatically. -------------------------------------------------------------------------------- Response 200: { "success": true, "message": "Domain app.yourdomain.com removed" } -------------------------------------------------------------------------------- POST /api/v1/managed_apps/:app_id/custom_domains/:id/set_primary Set a domain as the app's primary custom domain. -------------------------------------------------------------------------------- Response 200: { "custom_domain": { ...updated domain }, "message": "app.yourdomain.com set as primary domain" } -------------------------------------------------------------------------------- POST /api/v1/managed_apps/:app_id/custom_domains/:id/check_status Re-check DNS verification and SSL provisioning status with Cloudflare. -------------------------------------------------------------------------------- Response 200: { "custom_domain": { ...updated domain }, "cloudflare_status": "active", "ssl_status": "active" } Use this to poll after DNS configuration. SSL certificates are provisioned automatically via Cloudflare SSL for SaaS once DNS propagation is confirmed. -------------------------------------------------------------------------------- POST /api/v1/managed_apps/:app_id/custom_domains/:id/refresh Trigger a fresh DNS/SSL verification cycle. -------------------------------------------------------------------------------- Response 200: { "custom_domain": { ...refreshed domain }, "message": "Verification refreshed for app.yourdomain.com" } DOMAIN STATUS FLOW ------------------ pending → verified (DNS confirmed) → active (SSL provisioned + operational) "operational" is true when: verification_status == "verified" AND ssl_status == "active" AGENT WORKFLOW (subdomain) -------------------------- 1. POST .../custom_domains { "domain": "app.example.com" } → dns_records_complete is true immediately. 2. Tell user to configure DNS: CNAME app → proxy.overskill.app 3. Wait for DNS propagation (5-15 minutes) 4. POST .../custom_domains/:id/check_status (poll every 30 seconds) 5. When operational == true, domain is live AGENT WORKFLOW (apex/root — MUST poll until dns_records_complete) ---------------------------------------------------------------- 1. POST .../custom_domains { "domain": "example.com" } → dns_records_complete is FALSE. dns_records has only CNAMEs so far. 2. POLL GET .../custom_domains/:id (or POST .../refresh) every 30 seconds until dns_records_complete == true. The TXT records (_cf-custom-hostname ownership, then _acme-challenge SSL) appear in WAVES — add each record to the registrar as soon as it shows up in dns_records; ownership must be added + verified before the ACME TXT appears. 3. Once dns_records_complete == true, ensure EVERY record in dns_records is set at the registrar: both CNAMEs (@ and www) + both TXT records. (Cloudflare-hosted zones: set our records DNS-only / grey cloud.) 4. Keep polling GET .../:id until operational == true. Do NOT report the domain as connected while dns_records_complete is false — the CNAMEs alone will leave SSL stuck in pending_validation forever. Worked poll loop (pseudocode): resp = POST /api/v1/managed_apps/:app_id/custom_domains {domain} loop: d = GET /api/v1/managed_apps/:app_id/custom_domains/:id .custom_domain set every record in d.dns_records at the registrar (idempotent) break if d.operational == true # done — SSL live # d.dns_records_complete flips true once both TXT records exist sleep 30 # If it stalls, POST .../refresh to restart Cloudflare DCV, then keep polling. AUTOMATIC DNS SETUP (Dashboard Only) ------------------------------------- For supported registrars (GoDaddy, Namecheap, Google Domains, etc.), users can click "Set Up Automatically" in the dashboard to configure DNS in one click via Entri. This is only available in the dashboard UI, not via API. LIMITS ------ Custom domains are plan-gated, per app: free: 0 | pro: 1 | team: 5 | enterprise: unlimited GET /api/v1/capabilities reports your custom_domains_per_app limit; the domains list response includes can_add_more. ================================================================================ NOTIFY OPERATOR — ESCALATING TO A HUMAN ================================================================================ -------------------------------------------------------------------------------- POST /api/v1/notify_operator File an escalation and get a magic link your operator can act on. -------------------------------------------------------------------------------- Use this whenever you hit something you cannot resolve yourself and only a human can decide or unblock: out of credits, a plan limit, a domain that will not verify, an ambiguous product decision, a third-party account you have no access to. Request: { "issue": "Unable to deploy — the team is out of credits", "message": "(alias for issue — send either, not both)", "context": { "blocked_by": "INSUFFICIENT_CREDITS" }, "action_label": "Add credits", "managed_app_id": "QjqEkj" } issue REQUIRED, 1-1000 characters. `message` is an accepted alias. context optional object, any JSON. Shown to the operator. action_label optional button text. Default: "Review Request". managed_app_id optional; associates one of YOUR apps. Response 200: { "result": { "operator_action_url": "https://www.overskill.com/operator/action?token=...", "operator_action_label": "Add credits", "agent_script": "Tell your operator: Unable to deploy — the team is out of credits. Here's a direct action link: https://www.overskill.com/operator/action?token=...", "notification_id": "QjqEkj", "expires_at": "2026-08-18T14:23:45Z" }, "next_steps": [ "Share operator_action_url with your operator via Slack, email, or UI", "Use agent_script as a ready-made message template", "Link is valid for 7 days", "Operator can view issue details and take action without logging in" ] } Response 400 (neither issue nor message given, or issue over 1000 chars): { "error": "Bad Request", "message": "issue is required", "next_steps": [...] } Response 429 (rate limit): { "error": "Too Many Requests", "message": "Rate limit exceeded: 10 ...", "next_steps": [...] } WE DO NOT DELIVER THE NOTIFICATION ---------------------------------- Overskill sends no email, no SMS and no push for this. YOU are the delivery channel. Hand `agent_script` to your human, or post `operator_action_url` wherever they already are (Slack, your own UI, the chat you're in). The operator IS the account owner — they're already talking to you, and that conversation is the notification. What the operator sees when they open the link (no login required): - the issue text - the context object, formatted - the team, and the app if you associated one - a button to mark it resolved (with optional notes) The link grants ONLY that: viewing this one escalation and resolving it. It does not sign anyone in, expose any other record, or change the app, domain, team, or billing. EXPIRY ------ 7 days. After that the page shows a terminal "expired" state and the token stops verifying. File a new request to get a fresh link. `expires_at` in the response is the exact deadline (ISO 8601, UTC). RATE LIMIT ---------- 10 requests per hour PER TEAM — not per API key. A team holding several keys still gets ten. Over the limit returns 429; reuse the operator_action_url from your most recent notification instead, it stays valid for 7 days. SECURITY -------- NEVER put API keys, passwords, tokens or other secrets in `issue` or `context`. They are stored as sent and rendered to the operator in plaintext. -------------------------------------------------------------------------------- POST /api/v1/managed_apps/:app_id/custom_domains/:id/notify_operator The domain-specific flavour. Same response contract. -------------------------------------------------------------------------------- Pre-fills `issue` and `context` from the domain — verification and SSL status, every required DNS record, apex/Cloudflare fallback guidance, and a troubleshooting checklist — then returns the SAME flat contract as above. It also files an escalated support request with the Overskill team, because broken domains are the one case with a real human queue behind them. The support ticket id is in the notification's `context`. If the domain is already verified with SSL active, nothing is filed and the response is instead: { "custom_domain": {...}, "status": "already_operational", "message": "..." } ================================================================================ LINKS ================================================================================ Skill file: https://www.overskill.com/SKILL.md Full docs: https://www.overskill.com/llms-full.txt API docs: https://www.overskill.com/developers Dashboard: https://www.overskill.com/account Webhook docs: https://www.overskill.com/developers/webhooks Zapier: https://www.overskill.com/developers/zapier Skill install: https://www.overskill.com/developers/skill