{
  "openapi": "3.1.0",
  "info": {
    "title": "WebToAppConvert REST API",
    "version": "v1",
    "summary": "Trigger Android builds, manage apps, and integrate AI agents.",
    "description": "REST API for the WebToAppConvert platform. Authenticated via X-API-Key header. Available on paid and subscribed accounts only — generate a key from your dashboard at https://webtoappconvert.com/account.\n\nFull human-readable reference: https://webtoappconvert.com/docs/developer-api-reference\n\nDesigned for scripts, CI workflows, and AI agents (MCP).",
    "termsOfService": "https://webtoappconvert.com/terms",
    "contact": {
      "name": "WebToAppConvert API Support",
      "email": "support@webtoappconvert.com",
      "url": "https://webtoappconvert.com/contact"
    },
    "license": { "name": "Proprietary", "url": "https://webtoappconvert.com/terms" }
  },
  "servers": [
    {
      "url": "https://api.webtoappconvert.com/api/v1",
      "description": "Production"
    }
  ],
  "security": [{ "ApiKeyAuth": [] }],
  "tags": [
    { "name": "Account", "description": "Tier, credits, and limits" },
    { "name": "Apps", "description": "App configuration management" },
    { "name": "Builds", "description": "Trigger and download Android builds" },
    { "name": "Signing Keys", "description": "Read-only signing key listing" }
  ],
  "paths": {
    "/account": {
      "get": {
        "operationId": "getAccount",
        "summary": "Get account info",
        "description": "Returns the authenticated user's tier, credit balance, and tier limits. Always call this before triggering a build to verify the user has enough credits.",
        "tags": ["Account"],
        "responses": {
          "200": {
            "description": "OK",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Account" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/apps": {
      "get": {
        "operationId": "listApps",
        "summary": "List apps",
        "tags": ["Apps"],
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 } },
          { "name": "cursor", "in": "query", "schema": { "type": "string" }, "description": "siteId from a previous response's nextCursor" }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AppsListResponse" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "operationId": "createApp",
        "summary": "Create a new app",
        "description": "Creates a new app. Multipart body with a JSON `metadata` part and a binary `icon` file part. Both required.",
        "tags": ["Apps"],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": ["metadata", "icon"],
                "properties": {
                  "metadata": {
                    "type": "string",
                    "description": "JSON-encoded CreateAppMetadata payload",
                    "example": "{\"name\":\"My App\",\"packageName\":\"com.example.app\",\"url\":\"https://example.com\",\"primaryColor\":\"#22A11A\",\"primaryDarkColor\":\"#1A7A14\",\"accentColor\":\"#FFD500\",\"backgroundColor\":\"#000000\",\"version\":\"1.0.0\",\"versionCode\":\"1\",\"buildType\":\"debug\"}"
                  },
                  "icon": { "type": "string", "format": "binary", "description": "PNG/JPEG/WEBP, max 5 MB" }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateAppResponse" } } }
          },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/TierLimitExceeded" },
          "413": { "$ref": "#/components/responses/PayloadTooLarge" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/apps/{siteId}": {
      "parameters": [
        { "name": "siteId", "in": "path", "required": true, "schema": { "type": "string" } }
      ],
      "get": {
        "operationId": "getApp",
        "summary": "Get app config",
        "tags": ["Apps"],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/App" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "patch": {
        "operationId": "updateApp",
        "summary": "Update app metadata or replace assets",
        "description": "Accepts JSON for metadata-only updates, or multipart for combined metadata + asset replacement (icon, roundIcon, splash).",
        "tags": ["Apps"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": { "schema": { "$ref": "#/components/schemas/PatchAppMetadata" } },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "metadata": { "type": "string", "description": "Optional JSON-encoded PatchAppMetadata" },
                  "icon": { "type": "string", "format": "binary" },
                  "roundIcon": { "type": "string", "format": "binary" },
                  "splash": { "type": "string", "format": "binary" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PatchAppResponse" } } } },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "413": { "$ref": "#/components/responses/PayloadTooLarge" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/apps/{siteId}/duplicate": {
      "parameters": [
        { "name": "siteId", "in": "path", "required": true, "schema": { "type": "string" } }
      ],
      "post": {
        "operationId": "duplicateApp",
        "summary": "Duplicate an app",
        "description": "Copies all configuration, icon, splash, intro screens, and signing references to a new app. Build state resets. The new app's name gets a `(Copy)` suffix.",
        "tags": ["Apps"],
        "responses": {
          "201": {
            "description": "Created",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DuplicateAppResponse" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/TierLimitExceeded" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/builds": {
      "get": {
        "operationId": "listBuilds",
        "summary": "List builds",
        "tags": ["Builds"],
        "parameters": [
          { "name": "siteId", "in": "query", "schema": { "type": "string" }, "description": "Filter to one app" },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 } },
          { "name": "cursor", "in": "query", "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BuildsListResponse" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "operationId": "createBuild",
        "summary": "Trigger a build",
        "description": "Triggers a build. Credit cost: debug=10, starter=50, professional=100. Credits are deducted atomically; failed builds (infrastructure) are auto-refunded.",
        "tags": ["Builds"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": { "schema": { "$ref": "#/components/schemas/CreateBuildRequest" } }
          }
        },
        "responses": {
          "201": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateBuildResponse" } } } },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/InsufficientCredits" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "422": { "$ref": "#/components/responses/ValidationFailed" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/builds/{buildId}": {
      "parameters": [
        { "name": "buildId", "in": "path", "required": true, "schema": { "type": "string" } }
      ],
      "get": {
        "operationId": "getBuild",
        "summary": "Get build details",
        "description": "Full build doc with live wait estimate while pending or in progress.",
        "tags": ["Builds"],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Build" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/builds/{buildId}/download": {
      "parameters": [
        { "name": "buildId", "in": "path", "required": true, "schema": { "type": "string" } }
      ],
      "get": {
        "operationId": "getBuildDownload",
        "summary": "Get a download URL for a completed build",
        "description": "Returns a fresh download URL for the artifact ZIP. Only works when status is `completed` and the artifact hasn't been removed by retention policy.",
        "tags": ["Builds"],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BuildDownloadResponse" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "description": "Build not complete", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "410": { "description": "Artifact expired", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/signing-keys": {
      "get": {
        "operationId": "listSigningKeys",
        "summary": "List signing keys",
        "description": "Read-only — credentials are stripped server-side. Signing key creation/deletion is dashboard-only in this version.",
        "tags": ["Signing Keys"],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SigningKeysListResponse" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "API key issued from the dashboard. Format: `wac_` prefix + 40 URL-safe base64 chars."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable snake_case identifier — branch on this, not on `message`.",
                "examples": ["unauthorized", "insufficient_credits", "rate_limited", "validation_failed"]
              },
              "message": { "type": "string", "description": "Human-readable explanation. May change between releases." }
            },
            "additionalProperties": true
          }
        }
      },
      "Account": {
        "type": "object",
        "required": ["userId", "tier", "credits", "limits"],
        "properties": {
          "userId": { "type": "string" },
          "email": { "type": "string", "format": "email", "nullable": true },
          "tier": { "type": "string", "enum": ["free", "paid", "subscribed"] },
          "credits": {
            "type": "object",
            "properties": {
              "rollover": { "type": "integer", "description": "Bought credits — never expire" },
              "monthly": { "type": "integer", "description": "Subscription credits — reset on renewal" },
              "total": { "type": "integer" }
            }
          },
          "limits": {
            "type": "object",
            "properties": {
              "maxApps": { "type": "integer", "nullable": true, "description": "null = unlimited" },
              "maxSigningKeys": { "type": "integer", "nullable": true },
              "artifactRetentionDays": { "type": "integer", "nullable": true }
            }
          }
        }
      },
      "AppSummary": {
        "type": "object",
        "properties": {
          "siteId": { "type": "string" },
          "name": { "type": "string" },
          "url": { "type": "string", "format": "uri" },
          "packageName": { "type": "string" },
          "status": { "type": "string", "enum": ["ready", "archived", "deleted"] },
          "buildStatus": { "type": "string" },
          "iconImage": { "type": "string", "description": "Storage path, not a URL" },
          "updatedAt": { "type": "string", "format": "date-time", "nullable": true }
        }
      },
      "AppsListResponse": {
        "type": "object",
        "properties": {
          "apps": { "type": "array", "items": { "$ref": "#/components/schemas/AppSummary" } },
          "nextCursor": { "type": "string", "nullable": true }
        }
      },
      "App": {
        "type": "object",
        "description": "Full app config — same shape the dashboard reads. Internal fields stripped.",
        "additionalProperties": true,
        "properties": {
          "siteId": { "type": "string" },
          "userId": { "type": "string" },
          "status": { "type": "string" },
          "basic": { "type": "object" },
          "strings": { "type": "object" },
          "resources": { "type": "object" },
          "colors": { "type": "object" },
          "introScreens": { "type": "array" },
          "settings": { "type": "object" },
          "webview": { "type": "object" },
          "firebase": { "type": "object" },
          "monetization": { "type": "object" },
          "build": { "type": "object" },
          "createdAt": { "type": "string", "format": "date-time", "nullable": true },
          "updatedAt": { "type": "string", "format": "date-time", "nullable": true }
        }
      },
      "CreateAppMetadata": {
        "type": "object",
        "required": ["name", "packageName", "url", "primaryColor", "primaryDarkColor", "accentColor", "backgroundColor", "version", "versionCode", "buildType"],
        "properties": {
          "name": { "type": "string", "minLength": 2, "maxLength": 50 },
          "packageName": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9]*(\\.[a-z][a-z0-9]*)+$",
            "description": "Lowercase, dot-separated, ≥2 segments. No underscores."
          },
          "url": { "type": "string", "format": "uri" },
          "primaryColor": { "type": "string", "pattern": "^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$" },
          "primaryDarkColor": { "type": "string", "pattern": "^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$" },
          "accentColor": { "type": "string", "pattern": "^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$" },
          "backgroundColor": { "type": "string", "pattern": "^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$" },
          "version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" },
          "versionCode": { "type": "string", "pattern": "^[1-9]\\d*$" },
          "buildType": { "type": "string", "enum": ["debug", "starter", "professional", "customize"] }
        }
      },
      "PatchAppMetadata": {
        "type": "object",
        "description": "Partial update. Forbidden fields silently dropped: userId, id, createdAt, resources.*, build.signing, build.status.",
        "properties": {
          "basic": { "type": "object" },
          "strings": { "type": "object" },
          "colors": { "type": "object" },
          "webview": { "type": "object" },
          "settings": { "type": "object" },
          "monetization": { "type": "object" },
          "introScreens": { "type": "array" },
          "build": { "type": "object" }
        }
      },
      "PatchAppResponse": {
        "type": "object",
        "properties": {
          "siteId": { "type": "string" },
          "updated": { "type": "array", "items": { "type": "string" } },
          "uploadedAssets": { "type": "array", "items": { "type": "string" } }
        }
      },
      "CreateAppResponse": {
        "type": "object",
        "required": ["siteId"],
        "properties": {
          "siteId": { "type": "string" },
          "message": { "type": "string" }
        }
      },
      "DuplicateAppResponse": {
        "type": "object",
        "required": ["siteId"],
        "properties": {
          "siteId": { "type": "string" },
          "name": { "type": "string" },
          "message": { "type": "string" }
        }
      },
      "BuildSummary": {
        "type": "object",
        "properties": {
          "buildId": { "type": "string" },
          "siteId": { "type": "string" },
          "appName": { "type": "string" },
          "buildTier": { "type": "string", "enum": ["debug", "starter", "professional"] },
          "status": { "type": "string", "enum": ["pending", "queued", "in_progress", "completed", "failed"] },
          "createdAt": { "type": "string", "format": "date-time", "nullable": true },
          "completedAt": { "type": "string", "format": "date-time", "nullable": true },
          "artifactExpired": { "type": "boolean" }
        }
      },
      "BuildsListResponse": {
        "type": "object",
        "properties": {
          "builds": { "type": "array", "items": { "$ref": "#/components/schemas/BuildSummary" } },
          "nextCursor": { "type": "string", "nullable": true }
        }
      },
      "Build": {
        "type": "object",
        "properties": {
          "buildId": { "type": "string" },
          "siteId": { "type": "string" },
          "appName": { "type": "string" },
          "packageName": { "type": "string" },
          "versionName": { "type": "string" },
          "versionCode": { "type": "integer" },
          "buildTier": { "type": "string", "enum": ["debug", "starter", "professional"] },
          "status": { "type": "string", "enum": ["pending", "queued", "in_progress", "completed", "failed"] },
          "queuePosition": { "type": "integer", "description": "0 if this build is the in-progress one or in a terminal state" },
          "estimatedWaitSeconds": { "type": "integer", "description": "Live estimate for non-terminal builds. 0 for completed/failed." },
          "createdAt": { "type": "string", "format": "date-time", "nullable": true },
          "startedAt": { "type": "string", "format": "date-time", "nullable": true },
          "completedAt": { "type": "string", "format": "date-time", "nullable": true },
          "buildTime": { "type": "integer", "nullable": true, "description": "Actual build duration in seconds" },
          "logSummary": { "type": "array", "items": {
            "type": "object",
            "properties": {
              "time": { "type": "string", "format": "date-time" },
              "title": { "type": "string" },
              "message": { "type": "string" },
              "variant": { "type": "string", "enum": ["info", "success", "warning", "error"] }
            }
          }},
          "artifacts": {
            "type": "object",
            "properties": {
              "available": { "type": "boolean" },
              "expired": { "type": "boolean" },
              "expiresAt": { "type": "string", "format": "date-time", "nullable": true },
              "fileSize": { "type": "integer", "nullable": true }
            }
          }
        }
      },
      "CreateBuildRequest": {
        "type": "object",
        "required": ["siteId", "buildTier"],
        "properties": {
          "siteId": { "type": "string" },
          "buildTier": { "type": "string", "enum": ["debug", "starter", "professional"] }
        }
      },
      "CreateBuildResponse": {
        "type": "object",
        "properties": {
          "buildId": { "type": "string" },
          "status": { "type": "string", "enum": ["pending"] },
          "queuePosition": { "type": "integer" },
          "estimatedWaitSeconds": { "type": "integer" },
          "creditsUsed": { "type": "integer" },
          "creditsRemaining": { "type": "integer" }
        }
      },
      "BuildDownloadResponse": {
        "type": "object",
        "properties": {
          "buildId": { "type": "string" },
          "downloadUrl": { "type": "string", "format": "uri" },
          "expiresAt": { "type": "string", "format": "date-time" },
          "fileSize": { "type": "integer", "nullable": true },
          "buildTier": { "type": "string", "enum": ["debug", "starter", "professional"] }
        }
      },
      "SigningKey": {
        "type": "object",
        "properties": {
          "keyId": { "type": "string" },
          "name": { "type": "string" },
          "alias": { "type": "string" },
          "type": { "type": "string", "enum": ["uploaded", "generated"] },
          "status": { "type": "string", "enum": ["active", "inactive"] },
          "keystore": {
            "type": "object",
            "description": "Storage metadata only — passwords stripped",
            "properties": {
              "fileName": { "type": "string" },
              "fileSize": { "type": "integer" }
            }
          },
          "certificate": { "type": "object" },
          "usage": {
            "type": "object",
            "properties": {
              "appCount": { "type": "integer" },
              "buildCount": { "type": "integer" },
              "lastUsedAt": { "type": "string", "format": "date-time", "nullable": true }
            }
          },
          "createdAt": { "type": "string", "format": "date-time", "nullable": true },
          "updatedAt": { "type": "string", "format": "date-time", "nullable": true }
        }
      },
      "SigningKeysListResponse": {
        "type": "object",
        "properties": {
          "keys": { "type": "array", "items": { "$ref": "#/components/schemas/SigningKey" } }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid X-API-Key",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "InvalidRequest": {
        "description": "Bad or missing field",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "InsufficientCredits": {
        "description": "Not enough credits for this build",
        "headers": {},
        "content": { "application/json": { "schema": {
          "allOf": [
            { "$ref": "#/components/schemas/Error" },
            { "type": "object", "properties": { "error": { "type": "object", "properties": {
              "creditsAvailable": { "type": "integer" },
              "creditsRequired": { "type": "integer" },
              "purchaseUrl": { "type": "string", "format": "uri" }
            } } } }
          ]
        } } }
      },
      "TierLimitExceeded": {
        "description": "App count at tier cap",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "NotFound": {
        "description": "Resource doesn't exist or belongs to another user",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "ValidationFailed": {
        "description": "App not ready to build",
        "content": { "application/json": { "schema": {
          "allOf": [
            { "$ref": "#/components/schemas/Error" },
            { "type": "object", "properties": { "error": { "type": "object", "properties": {
              "errors": { "type": "array", "items": { "type": "string" } }
            } } } }
          ]
        } } }
      },
      "PayloadTooLarge": {
        "description": "Image file exceeds 5 MB",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "RateLimited": {
        "description": "Per-minute or per-day limit exceeded",
        "headers": {
          "Retry-After": {
            "schema": { "type": "integer" },
            "description": "Seconds until next eligible request"
          }
        },
        "content": { "application/json": { "schema": {
          "allOf": [
            { "$ref": "#/components/schemas/Error" },
            { "type": "object", "properties": { "error": { "type": "object", "properties": {
              "retryAfter": { "type": "integer" }
            } } } }
          ]
        } } }
      }
    }
  },
  "x-llm-context": {
    "summary": "REST API for triggering Android app builds. Authenticated by X-API-Key header. Paid users only.",
    "agentNotes": "Always GET /account first to verify credit balance. Prefer template-based workflow (POST /apps/:id/duplicate then PATCH then POST /builds) over create-from-scratch — avoids icon upload. Honour Retry-After exactly. Surface 402 insufficient_credits with purchaseUrl to the user; never auto-retry. Branch on error.code (stable), not error.message.",
    "documentation": "https://webtoappconvert.com/docs/developer-api-reference",
    "mcpServer": "https://webtoappconvert.com/docs/mcp-server (in development)"
  }
}
