The /analyze API
POST /api/v1/analyze is the embeddable engine's single entrypoint: give it an app URL and it autonomously generates a test suite, runs it in a real browser, and returns root-caused bugs — composing generate → run → root-cause so you don't orchestrate them yourself.
- Auth:
Authorization: Bearer tw_…(an org-scoped API token — see Authentication). Every call is isolated to that token's org. - Typed + documented: the request and response are Pydantic models, so the shape appears in the OpenAPI at
GET /api/v1/openapi.json(and/api/v1/docs).
Request
{
"app_url": "https://staging.app.com", // required (for kind=url)
"kind": "url", // url | docs | text | openapi
"base_url": null, // target app for non-url kinds
"text": null, // pasted prose / spec (kind=text/openapi)
"auth": { // optional: log in first (auth-gated apps)
"username": "test@app.com",
"password": "…",
"login_path": "/login"
},
"exhaustive": true, // deeper coverage
"file_issues": "gitlab", // github|gitlab|jira|native|off (default off)
"mode": "sync", // sync = wait for the result; async = run_id only
"timeout_s": 600, // sync wait budget (30–1800)
// Per-request overrides (else the org's saved Settings are used):
"anthropic_key": "sk-ant-…", // BYOK for this call
"log_backend": { // server-log source for root cause
"LOG_BACKEND": "loki",
"LOKI_URL": "https://loki.customer.internal:3100",
"LOKI_SELECTOR": "{app=\"checkout\"}"
}
}Only app_url is required. mode: "async" returns immediately with a run_id you can poll via GET /api/v1/runs/{run_id}; mode: "sync" blocks up to timeout_s and returns the finished result.
Response — AnalyzeResult
{
"run_id": "run_1720…",
"status": "done", // done | error | cancelled | queued | running
"suite": "Checkout",
"summary": { "passed": 5, "failed": 1, "broken": 0, "skipped": 0 },
"cases": [
{
"id": "pay",
"title": "Pay with card",
"status": "failed", // passed | failed | broken | skipped
"why": "500 on submit",
"root_cause": {
"category": "backend_exception",
"summary": "Checkout submit returned 500. Server: NPE computing tax for DE",
"suspected_cause": "NullPointerException computing tax for DE",
"suggested_fix": "guard the null tax tier in PricingService",
"confidence": 0.82,
"server_status": "diagnosed", // diagnosed | inferred_no_logs | no_trace_id | no_log_backend | no_logs_matched | diagnosis_failed
"trace_id": "4bf92f35…",
"evidence": ["[ERROR] NPE at PricingService.tax(...)"]
}
}
],
"issues": [ /* the filed issue dicts, when file_issues was set */ ]
}root_cause is null for passing cases. When there's no backend diagnosis, server_status says why (e.g. no_trace_id — the app didn't emit a trace header), so a gap is actionable rather than silent. See Connecting server logs.
Example
# synchronous — one call, wait for root-caused bugs
curl -X POST https://YOUR-UPLIFTR/api/v1/analyze \
-H "Authorization: Bearer tw_live_…" \
-H "Content-Type: application/json" \
-d '{
"app_url": "https://staging.app.com",
"file_issues": "gitlab",
"log_backend": { "LOG_BACKEND": "loki", "LOKI_URL": "https://loki:3100" }
}'Prefer a client? Use the Python SDK. Calling from an agent? Use the MCP tool. See also the full REST API reference.