The WebToAppConvert REST API is the same engine that powers the dashboard, exposed for scripts, CI workflows, and AI agents. With it you can list and manage apps, trigger builds, poll status, and download the resulting APK or AAB — all from anything that speaks HTTP.
This page is the canonical reference: it covers authentication, every endpoint, error codes, and patterns AI agents should follow. The full machine-readable contract is also published at /openapi.json (OpenAPI 3.1).
Overview
The API is available to paid and subscribed accounts. Free-tier accounts can't generate API keys — make any credit purchase or start a subscription to unlock it. Once you have credits, generate your key from the My Profile page → API Access.
Base URL for every request:
https://api.webtoappconvert.com/api/v1All endpoints below are relative to this URL.
Design at a glance
- JSON in, JSON out. Multipart only for file uploads (icon, splash, round icon).
- One key per user. To rotate, delete and regenerate.
- Single rate limit: 20 requests/minute, 20 builds/UTC-day per key.
- Standard error envelope (
{ error: { code, message, ...extras } }) so clients can branch on a stable code rather than parsing prose. - Credit-system safety. Builds always burn credits — the API never bypasses the credit ledger that powers the dashboard.
Quickstart
The fastest path from zero to a downloaded APK using an existing app you've already configured in the dashboard.
1. Get your API key
Sign in, open My Profile, scroll to API Access, click Generate API Key. The raw key is shown only once — copy it immediately. It looks like:
wac_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxStore it the way you'd store a password — environment variable, secrets manager, never source-controlled.
2. Make your first request
export WAC_KEY="wac_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export WAC_BASE="https://api.webtoappconvert.com/api/v1"
curl -H "X-API-Key: $WAC_KEY" "$WAC_BASE/account"import os, requests
WAC_KEY = os.environ["WAC_KEY"]
WAC_BASE = "https://api.webtoappconvert.com/api/v1"
resp = requests.get(f"{WAC_BASE}/account", headers={"X-API-Key": WAC_KEY})
resp.raise_for_status()
print(resp.json())const WAC_KEY = process.env.WAC_KEY;
const WAC_BASE = "https://api.webtoappconvert.com/api/v1";
const resp = await fetch(`${WAC_BASE}/account`, {
headers: { "X-API-Key": WAC_KEY },
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
console.log(await resp.json());If the key is valid you'll see your tier, credit balance, and limits. If it's missing or invalid you'll get an HTTP 401 with {"error":{"code":"unauthorized", ...}}.
3. Trigger a build and download it
List your apps, pick one, trigger a debug build (10 credits), wait for the server's own time estimate before polling, then download:
Don't poll blindly. The build trigger response includes estimatedWaitSeconds — sleep that long before your first status check, then use the fresh estimate from each status response to set your next sleep interval. Polling faster than the estimate wastes your rate-limit budget and doesn't speed up the build.# 1. List apps
SITE_ID=$(curl -s -H "X-API-Key: $WAC_KEY" "$WAC_BASE/apps" | jq -r '.apps[0].siteId')
# 2. Trigger debug build — capture the estimate from the response
BUILD=$(curl -s -X POST "$WAC_BASE/builds" \
-H "X-API-Key: $WAC_KEY" \
-H "Content-Type: application/json" \
-d "{\"siteId\":\"$SITE_ID\",\"buildTier\":\"debug\"}")
BUILD_ID=$(echo "$BUILD" | jq -r .buildId)
WAIT=$(echo "$BUILD" | jq -r .estimatedWaitSeconds)
echo "Build $BUILD_ID queued — est. ${WAIT}s. Waiting…"
# 3. Wait the server's estimate, then poll using each response's fresh estimate
sleep "$WAIT"
while true; do
RESP=$(curl -s -H "X-API-Key: $WAC_KEY" "$WAC_BASE/builds/$BUILD_ID")
STATUS=$(echo "$RESP" | jq -r .status)
WAIT=$(echo "$RESP" | jq -r '.estimatedWaitSeconds // 30')
echo "$BUILD_ID → $STATUS (next check in ${WAIT}s)"
[[ "$STATUS" == "completed" ]] && break
[[ "$STATUS" == "failed" ]] && exit 1
sleep "$WAIT"
done
# 4. Get a fresh download URL
curl -s -H "X-API-Key: $WAC_KEY" "$WAC_BASE/builds/$BUILD_ID/download" | jqAuthentication
API key header
Every request must include your key in the X-API-Key header:
curl -H "X-API-Key: wac_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
https://api.webtoappconvert.com/api/v1/accountresp = requests.get(
f"{WAC_BASE}/account",
headers={"X-API-Key": WAC_KEY},
timeout=30,
)const resp = await fetch(`${WAC_BASE}/account`, {
method: "GET",
headers: { "X-API-Key": WAC_KEY },
});Each user holds at most one active key at a time. To rotate, delete the existing key from the dashboard and generate a new one.
How keys are stored
The plaintext of your key is shown to you exactly once — at generation. We persist only a one-way hash on our side, so even our own engineers can't recover the plaintext. If you lose the key, the only path forward is to delete it and generate a new one.
Security best practices
- Never commit keys to source control. Use environment variables (
WAC_KEY) or your platform's secrets manager. - Don't share keys across environments. One key per user is the contract today, but you can rotate per environment by deleting and regenerating between deploys.
- Rotate immediately if you suspect leakage — log files, screenshots, support tickets, anywhere it shouldn't be. Deletion is instant; integrations using the old key stop working immediately.
- Don't include the full key in support requests. Share the prefix (
wac_a1b2c3d4) — that's enough for us to identify the key in our logs without you exposing the secret.
Rate Limits
Limits are enforced per API key, atomically, in a single Firestore transaction.
Per-minute limit
- 20 requests per UTC-minute window, all endpoints combined.
- The window resets exactly at the next minute boundary (e.g.,
05:01:00,05:02:00).
Per-day build limit
- 20 builds per UTC-day, applied only to
POST /builds. - Resets at
00:00:00 UTC. - The cap exists as a safety net against runaway scripts; the credit system is the primary throttle.
Handling 429 responses
When a limit is exceeded, the response is HTTP 429 with both a header and a body field telling you exactly how long to wait:
HTTP/2 429
Retry-After: 45
Content-Type: application/json
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded (20/min).",
"retryAfter": 45
}
}Honour Retry-After exactly. Don't add jitter, retries inside the window will all fail. The seconds value is the time until the next minute (per-minute limit) or next UTC midnight (per-day limit).
import time, requests
def call_with_retry(method, url, **kwargs):
while True:
resp = requests.request(method, url, **kwargs)
if resp.status_code != 429:
return resp
wait = int(resp.headers.get("Retry-After", "30"))
print(f"Rate limited, sleeping {wait}s")
time.sleep(wait)async function callWithRetry(method, url, init = {}) {
while (true) {
const resp = await fetch(url, { ...init, method });
if (resp.status !== 429) return resp;
const wait = Number(resp.headers.get("Retry-After") ?? 30);
console.log(`Rate limited, sleeping ${wait}s`);
await new Promise(r => setTimeout(r, wait * 1000));
}
}Errors
Error envelope
Every non-2xx response shares this shape:
{ "error": { "code": "snake_case_code", "message": "Human-readable message", "...extras": "..." } }Always branch on error.code — it's the contract. The message field is for humans and may change between releases. Extras (e.g. retryAfter, creditsAvailable, purchaseUrl) are documented per code below.
Status code reference
| HTTP | code | When it happens | Extra fields |
|---|---|---|---|
| 400 | invalid_request | Missing or malformed field | — |
| 401 | unauthorized | Missing, invalid, or revoked API key | — |
| 402 | insufficient_credits | Build trigger but credit balance is too low | creditsAvailable, creditsRequired, purchaseUrl |
| 403 | tier_limit_exceeded | App count is at your tier's maxApps | — |
| 404 | not_found | Resource doesn't exist (or belongs to another user — we mask) | — |
| 409 | build_not_complete | Download requested before the build reached completed | currentStatus |
| 410 | artifact_expired | Build artifact removed by retention policy | expiredAt |
| 413 | payload_too_large | Image file exceeds 5 MB | — |
| 422 | validation_failed | Build readiness check failed (missing icon/key/version) | errors[] |
| 429 | rate_limited | Per-minute or per-day cap reached | retryAfter |
| 500 | internal_error | Unexpected server error — safe to retry once with backoff | — |
Recovery patterns
- 402 insufficient credits — never auto-retry. Surface
purchaseUrlto the human user; credits don't appear by themselves. - 422 validation failed — fix what's listed in
errors[]and retry. Don't loop without a fix; you'll keep getting the same response. - 429 rate limited — sleep
Retry-Afterseconds, then retry once. Stop if the second attempt also returns 429. - 500 internal error — exponential backoff, max 3 attempts. If still failing, escalate to support with the
buildIdor request URL.
Endpoints
GET /account
Returns the authenticated user's tier, credit balance, and tier limits. Always call this first — agents should verify the user has enough credits before triggering a build.
curl -H "X-API-Key: $WAC_KEY" $WAC_BASE/accountResponse (200):
{
"userId": "abc123",
"email": "user@example.com",
"tier": "paid",
"credits": { "rollover": 690, "monthly": 0, "total": 690 },
"limits": { "maxApps": 30, "maxSigningKeys": 5, "artifactRetentionDays": 30 }
}GET /apps
Paginated list of your apps. Query params: limit (1–100, default 20) and cursor (siteId from the previous response's nextCursor).
curl -H "X-API-Key: $WAC_KEY" "$WAC_BASE/apps?limit=20"Response (200):
{
"apps": [
{
"siteId": "abc123",
"name": "My App",
"url": "https://example.com",
"packageName": "com.example.app",
"status": "ready",
"buildStatus": "completed",
"iconImage": "users/.../sites/abc123/icon.png",
"updatedAt": "2026-05-09T03:24:14.420Z"
}
],
"nextCursor": null
}POST /apps
Creates a new app. Requires multipart/form-data with a JSON metadata part and an icon file part. Icon is mandatory — every build (including debug) needs one.
metadata fields (all required):
| Field | Type | Constraints |
|---|---|---|
name | string | 2–50 chars, no control chars |
packageName | string | regex ^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$ — lowercase, dot-separated, ≥2 segments |
url | string | http/https, has TLD, not localhost, ≤2048 chars |
primaryColor | string | hex (#RRGGBB or #RRGGBBAA) |
primaryDarkColor | string | hex |
accentColor | string | hex |
backgroundColor | string | hex |
version | string | semver X.Y.Z |
versionCode | string | integer ≥ 1, ≤ 2,100,000,000 |
buildType | string | debug, starter, professional, or customize |
icon: PNG/JPEG/WEBP, 100 bytes – 5 MB.
curl -X POST "$WAC_BASE/apps" \
-H "X-API-Key: $WAC_KEY" \
-F 'metadata={"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"}' \
-F 'icon=@./logo.png'import json, requests
metadata = {
"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",
}
with open("logo.png", "rb") as fp:
resp = requests.post(
f"{WAC_BASE}/apps",
headers={"X-API-Key": WAC_KEY},
data={"metadata": json.dumps(metadata)},
files={"icon": ("logo.png", fp, "image/png")},
)
resp.raise_for_status()
print(resp.json())import fs from "node:fs";
const metadata = {
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",
};
const form = new FormData();
form.set("metadata", JSON.stringify(metadata));
form.set("icon", new Blob([fs.readFileSync("./logo.png")], { type: "image/png" }), "logo.png");
const resp = await fetch(`${WAC_BASE}/apps`, {
method: "POST",
headers: { "X-API-Key": WAC_KEY },
body: form,
});
console.log(await resp.json());Response (201): { "siteId": "...", "message": "App created successfully." }
GET /apps/:siteId
Full app config (server-internal fields stripped). 404 if the app doesn't exist or isn't yours — we don't leak existence to non-owners.
curl -H "X-API-Key: $WAC_KEY" "$WAC_BASE/apps/SITE_ID"PATCH /apps/:siteId
Update metadata, replace assets, or both. Two content types accepted:
JSON body — metadata-only update:
curl -X PATCH "$WAC_BASE/apps/SITE_ID" \
-H "X-API-Key: $WAC_KEY" \
-H "Content-Type: application/json" \
-d '{"basic":{"name":"New Name","url":"https://newurl.com"}}'multipart/form-data — metadata plus asset replacement:
curl -X PATCH "$WAC_BASE/apps/SITE_ID" \
-H "X-API-Key: $WAC_KEY" \
-F 'metadata={"basic":{"name":"New Name"}}' \
-F 'icon=@./new-logo.png' \
-F 'roundIcon=@./round-logo.png' \
-F 'splash=@./splash.png'Updatable top-level fields: basic, strings, colors, webview, settings, monetization, introScreens, plus selective build fields.
Forbidden fields (silently dropped): userId, id, createdAt, resources.* (use file parts instead), build.signing (managed in dashboard), build.status (server-managed).
POST /apps/:siteId/duplicate
Duplicate an existing app — copies all configuration, icon, splash, intro screens, signing references to a new app. Build state resets. The new app's name gets a "(Copy)" suffix; PATCH to rename.
This is the cleanest path for AI agents: configure a "template" app once in the dashboard, then duplicate + customise + build per request without re-uploading assets.
curl -X POST "$WAC_BASE/apps/TEMPLATE_ID/duplicate" \
-H "X-API-Key: $WAC_KEY"Response (201): { "siteId": "newSiteId", "name": "My App (Copy)", "message": "App duplicated successfully." }
POST /builds
Trigger a build. Body fields:
| Field | Type | Required | Notes |
|---|---|---|---|
siteId | string | yes | Must be an app you own and that's "ready" |
buildTier | string | yes | debug (10 credits) | starter (50) | professional (100) |
curl -X POST "$WAC_BASE/builds" \
-H "X-API-Key: $WAC_KEY" \
-H "Content-Type: application/json" \
-d '{"siteId":"SITE_ID","buildTier":"debug"}'resp = requests.post(
f"{WAC_BASE}/builds",
headers={"X-API-Key": WAC_KEY, "Content-Type": "application/json"},
json={"siteId": SITE_ID, "buildTier": "debug"},
)
resp.raise_for_status()
print(resp.json())const resp = await fetch(`${WAC_BASE}/builds`, {
method: "POST",
headers: { "X-API-Key": WAC_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ siteId: SITE_ID, buildTier: "debug" }),
});
console.log(await resp.json());Response (201):
{
"buildId": "xyz789",
"status": "pending",
"queuePosition": 0,
"estimatedWaitSeconds": 180,
"creditsUsed": 10,
"creditsRemaining": 680
}Insufficient credits (402):
{
"error": {
"code": "insufficient_credits",
"message": "You have 5 credits but this debug build needs 10.",
"creditsAvailable": 5,
"creditsRequired": 10,
"purchaseUrl": "https://webtoappconvert.com/billing"
}
}App not ready (422):
{
"error": {
"code": "validation_failed",
"message": "App is not ready to build: ...",
"errors": ["App icon is required", "Signing key configuration is required for non-debug builds"]
}
}GET /builds
List your builds. Optional query params: siteId (filter to one app), limit, cursor.
curl -H "X-API-Key: $WAC_KEY" "$WAC_BASE/builds?siteId=SITE_ID&limit=20"GET /builds/:buildId
Full build details with a live wait-time estimate while the build is pending or in progress. Status transitions: pending → queued → in_progress → completed | failed.
curl -H "X-API-Key: $WAC_KEY" "$WAC_BASE/builds/BUILD_ID"resp = requests.get(f"{WAC_BASE}/builds/{build_id}", headers={"X-API-Key": WAC_KEY})
resp.raise_for_status()
build = resp.json()
print(f"{build['status']}: queue={build['queuePosition']}, eta={build['estimatedWaitSeconds']}s")const resp = await fetch(`${WAC_BASE}/builds/${buildId}`, {
headers: { "X-API-Key": WAC_KEY },
});
const build = await resp.json();
console.log(`${build.status}: queue=${build.queuePosition}, eta=${build.estimatedWaitSeconds}s`);Response (200):
{
"buildId": "xyz789",
"siteId": "abc123",
"appName": "My App",
"buildTier": "debug",
"status": "in_progress",
"queuePosition": 0,
"estimatedWaitSeconds": 90,
"createdAt": "2026-05-09T...",
"startedAt": "2026-05-09T...",
"completedAt": null,
"buildTime": null,
"logSummary": [
{ "time": "...", "title": "Build Queued", "message": "Waiting in build queue", "variant": "info" },
{ "time": "...", "title": "Build Started", "message": "Builder picked up the build", "variant": "info" }
],
"artifacts": { "available": false, "expired": false, "expiresAt": null, "fileSize": null }
}GET /builds/:buildId/download
Returns a fresh download URL for the artifact ZIP — debug APK or release AAB depending on tier. Only works when status === "completed" and the artifact hasn't expired (retention: 7d free, 30d paid, unlimited subscribed).
curl -H "X-API-Key: $WAC_KEY" "$WAC_BASE/builds/BUILD_ID/download"Response (200):
{
"buildId": "xyz789",
"downloadUrl": "https://storage.googleapis.com/...",
"expiresAt": "2026-05-09T03:57:55.106Z",
"fileSize": 12345678,
"buildTier": "debug"
}Then download the file directly:
curl -o app-build.zip "https://storage.googleapis.com/..."409 if not complete: { "error": { "code": "build_not_complete", "currentStatus": "in_progress" } }
410 if expired: { "error": { "code": "artifact_expired", "expiredAt": "..." } }
GET /signing-keys
Read-only list of your signing keys (passwords stripped server-side). Useful when you want to reference an existing key from a PATCH /apps/:id call. Signing key creation and deletion are dashboard-only in this version.
curl -H "X-API-Key: $WAC_KEY" "$WAC_BASE/signing-keys"Workflows
Template-based (recommended for agents)
Create one well-configured "template" app in the dashboard with logo, colors, signing settings, and a generic name like "Customer Template". Then have your agent or script duplicate it for each customer:
# 1. Duplicate the template
NEW=$(curl -s -X POST "$WAC_BASE/apps/TEMPLATE_ID/duplicate" \
-H "X-API-Key: $WAC_KEY" | jq -r .siteId)
# 2. Customise (URL, name, package — keep colors/icon/signing from template)
curl -X PATCH "$WAC_BASE/apps/$NEW" \
-H "X-API-Key: $WAC_KEY" \
-H "Content-Type: application/json" \
-d '{"basic":{"name":"Customer XYZ","url":"https://customerxyz.com","packageName":"com.customer.xyz"}}'
# 3. Build
BUILD=$(curl -s -X POST "$WAC_BASE/builds" \
-H "X-API-Key: $WAC_KEY" \
-H "Content-Type: application/json" \
-d "{\"siteId\":\"$NEW\",\"buildTier\":\"starter\"}" | jq -r .buildId)
# 4. Wait + download (loop omitted for brevity)This pattern is faster, avoids re-uploading icons, and keeps the agent's tool calls JSON-only — no binary plumbing.
Create from scratch
Use this when you don't have a template — typically a one-off integration or when the agent is generating a unique brand from scratch.
curl -X POST "$WAC_BASE/apps" \
-H "X-API-Key: $WAC_KEY" \
-F 'metadata={"name":"Fresh App","packageName":"com.fresh.app","url":"https://fresh.com","primaryColor":"#000000","primaryDarkColor":"#000000","accentColor":"#FFFFFF","backgroundColor":"#FFFFFF","version":"1.0.0","versionCode":"1","buildType":"starter"}' \
-F 'icon=@./logo.png'Polling pattern
Builds run on a single-machine builder serially. Always read estimatedWaitSeconds from the build trigger response and sleep that long before your first poll. After each poll, use the fresh estimatedWaitSeconds from the status response as the next sleep interval. Polling faster than the estimate doesn't speed up the build and burns your rate-limit budget.
If you're building a UI that shows live status, present the estimate to the user ("your build should be ready in ~3 minutes") rather than polling aggressively on their behalf.
import time, requests
def wait_for_build(build_id, *, key, base, timeout_min=20):
deadline = time.time() + timeout_min * 60
while time.time() < deadline:
resp = requests.get(f"{base}/builds/{build_id}", headers={"X-API-Key": key})
b = resp.json()
if b["status"] in ("completed", "failed"):
return b
# Poll cadence based on the API's own estimate, capped at 60s
sleep_s = min(60, max(15, b.get("estimatedWaitSeconds", 30) // 4))
print(f"{b['status']}: eta {b['estimatedWaitSeconds']}s — sleep {sleep_s}s")
time.sleep(sleep_s)
raise TimeoutError(f"Build {build_id} did not finish within {timeout_min} min")async function waitForBuild(buildId, { key, base, timeoutMin = 20 } = {}) {
const deadline = Date.now() + timeoutMin * 60_000;
while (Date.now() < deadline) {
const resp = await fetch(`${base}/builds/${buildId}`, { headers: { "X-API-Key": key } });
const b = await resp.json();
if (b.status === "completed" || b.status === "failed") return b;
const eta = b.estimatedWaitSeconds ?? 30;
const sleep = Math.min(60_000, Math.max(15_000, (eta * 1000) / 4));
console.log(`${b.status}: eta ${eta}s — sleep ${Math.round(sleep / 1000)}s`);
await new Promise(r => setTimeout(r, sleep));
}
throw new Error(`Build ${buildId} did not finish within ${timeoutMin} min`);
}For AI Agents
The API was designed with autonomous agents in mind. Here's how an agent should use it.
Discovery
- OpenAPI spec:
https://webtoappconvert.com/openapi.json— full machine-readable contract (paths, schemas, error codes). - llms-full.txt:
/llms-full.txt— product context for language models, includes API summary. - JSON-LD
APIReferenceon this page — schema.org structured data so search engines and AI surfaces can identify it as a programmable API. - MCP server (in development) — Model Context Protocol stdio server that wraps these endpoints as native agent tools. Once published, it'll be installable via
npx @webtoapp/mcpand configurable in Claude Desktop, Claude Code, and any MCP-compatible client.
Recommended agent loop
- Always call
GET /accountfirst. Confirm the user has enough credits for the requested tier (debug: 10, starter: 50, professional: 100). If they don't, surfacepurchaseUrland stop. - Prefer the template-based workflow. Ask the user once to set up a template app in the dashboard. Then duplicate-and-customise per request. Avoids the icon-upload friction and keeps tool args small.
- Honour
retryAfter. The API tells you exactly how long to wait. Sleep that long, no jitter. The 20-req/min limit is generous for legitimate use; hitting it usually means a bug. - Branch on
error.code, not onmessage. Codes are stable; messages can change. - Surface 402 to the user; don't auto-retry. Credits don't appear by themselves. The error includes
purchaseUrl— pass it through. - Never poll before the estimate expires. Read
estimatedWaitSecondsfrom the POST /builds response and sleep that long before your first status check. After each poll, use the freshestimatedWaitSecondsfrom the status response as the next sleep. The estimate is updated as the build progresses — follow it, not a fixed interval.
Failure handling cheatsheet
| Error code | Agent action |
|---|---|
unauthorized | Ask user to verify their API key in Account → API Access. Don't auto-retry. |
insufficient_credits | Tell user the gap and surface purchaseUrl. Stop. |
tier_limit_exceeded | Suggest deleting unused apps in dashboard, or upgrading to a higher tier. Stop. |
validation_failed | List the errors. If one is "icon required", ask user to upload via dashboard or provide a URL/file. Don't loop. |
not_found | Tell user the resource doesn't exist or isn't theirs. Don't probe. |
build_not_complete | Wait — keep polling. Use the wait estimate. |
artifact_expired | Inform user; offer to trigger a fresh build (will cost credits). |
rate_limited | Sleep retryAfter seconds, retry once. If second attempt also 429, escalate. |
internal_error | Exponential backoff (15s → 60s → 240s), max 3 attempts. Then escalate. |
Reference
OpenAPI specification
The full machine-readable contract is published at /openapi.json (OpenAPI 3.1). Tools that consume OpenAPI — Postman, Insomnia, openapi-generator, MCP clients — can ingest it directly.
Limits summary
| Limit | Value |
|---|---|
| API requests | 20 per UTC-minute per key |
| Build triggers | 20 per UTC-day per key |
| Icon / asset file size | 5 MB per file |
| Image formats | PNG, JPEG, WEBP |
| App name | 2–50 characters |
| Package name | Lowercase, ≥2 dot-separated segments, no underscores |
| Apps per account | Free: 3 · Paid: 30 · Subscribed: unlimited |
| Signing keys per account | Free: 1 · Paid: 5 · Subscribed: unlimited |
| Artifact retention | Free: 7 days · Paid: 30 days · Subscribed: unlimited |
Versioning policy
The API path is versioned (/api/v1/...). We commit to the v1 contract going forward. Additive changes — new fields in responses, new endpoints, new error codes — can land without bumping the version. Breaking changes — removed fields, renamed paths, semantic shifts — will land on /api/v2/ with a deprecation notice on v1.
What's stable:
- HTTP status codes per endpoint
- Error
codevalues (snake_case, never renamed) - Path structure (
/api/v1/<resource>/<id>/<action>) - Authentication header (
X-API-Key)
What's likely to evolve:
- Error
messagetext (always human-readable, never machine-parsed) - Additional fields on response bodies (clients should ignore unknown fields)
- Rate-limit numbers (we'll loosen them as the platform scales; never tighten without notice)
Frequently Asked Questions
How do I get an API key?
Make any purchase (credits or subscription) to lift your tier above free. Then go to My Profile → API Access → Generate API Key. Copy the raw value immediately; we don't show it again.
Can I have multiple keys?
Not in this version — each user has exactly one key at a time. To rotate, delete the existing key and generate a new one. If you have multiple environments (staging vs. production), generate separately and update each environment as you go.
What happens if my key is leaked?
Delete it from Account → API Access immediately. Any integration using it will stop working. Generate a new key and update your scripts. There's no rate limiting on revocation — do it as fast as you can.
Why is the icon required at creation?
Every Android build, including debug, needs an icon — the app installer can't run without one. Required at creation so you can immediately trigger a build. The template-based workflow avoids re-uploading icons.
Why doesn't the API support custom signing-keystore uploads?
JKS uploads are out of scope for V1 to keep the JSON API contract clean. Create signing keys from the dashboard; reference them in apps via build.signing.keyId. We're tracking demand — if you need this, email support.
Are public download URLs sufficient?
Build artifacts are served from public Storage URLs. The /download endpoint returns a fresh URL each call, with an explicit expiresAt hint. Treat the URL as one-shot — fetch the artifact promptly rather than caching the URL.
How do agents handle insufficient credits?
The 402 response includes purchaseUrl. A well-behaved agent surfaces this to the end user and stops — credits don't appear by themselves. Don't auto-retry, the user has to take action.
How long do builds typically take?
Debug: ~2 minutes. Starter: ~3–5 minutes. Professional: ~5–8 minutes. The estimatedWaitSeconds field is updated based on rolling per-tier averages from your account's actual builds — it's a moving estimate, not a hard guarantee.
Support
API issues, feature requests, or strange responses? Email support@webtoappconvert.com with:
- The HTTP method and path you called (e.g.,
POST /api/v1/builds) - Your key prefix only —
wac_a1b2c3d4is enough for us to find it in our logs - The error response body
- The approximate UTC timestamp
Never share the full key in support tickets, screenshots, or chat — we can identify it from the prefix alone, and exposing the secret only puts your account at risk.