← Expo Upgrade Desk / API
Tokens

Drive Expo Upgrade Desk from your own code

Everything the web page does is available over HTTP: paste a project in, get the same structured upgrade plan back. The natural use is a CI job that re-plans the upgrade whenever package.json changes, or a script that runs the same check across every app in a monorepo.

Base URL and the envelope

Every endpoint lives under https://api.skillsafe.ai/v1/app-api and every response uses the same envelope, so one helper covers the whole API:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }

Send your app slug as X-App-Slug: expo-upgrade-desk and your token as Authorization: Bearer … on every call.

Error codes

codestatuswhat to do
unauthorized401The token is missing, malformed or expired. Get a new one from the token page.
forbidden403The token is valid but not for this app. Check the X-App-Slug header.
payment_required402The balance is below min_credits. Call /estimate first and top up.
validation_error400The input object is missing a required field - `files` is the usual one.
rate_limited429Too many requests. Back off and retry; do not tight-loop.
not_found404Unknown job id, or the app slug does not exist.
internal500A server-side failure. Retry with the SAME Idempotency-Key so you are not billed twice.

1. A tiny client

One helper that adds the headers, unwraps data and raises on error.

2. Get a token

The easiest route is the token page: it shows the token this browser already holds, with Copy token and Copy shell export buttons, and a sign-in button for a personal token. You never need to open the developer console.

A guest token can call /me and /estimate. Running a plan is metered, so it needs a personal token from signing in.

3. Check the session and the balance

/me tells you whether the token is a guest or a person, and what the balance is. Compare it against min_credits from the next step before you run, so a shortfall surfaces as your own clear message rather than a 402.

4. Price the run — free

The input object is exactly what the app's own form submits:

fieldtypemeaning
filesstring, requiredThe pasted project. Put a # file: package.json marker line above each file so they can be told apart.
target_sdkstring"latest" or a bare major such as "56".
concernstringgeneral, dependencies, native, config or breaking-changes. Emphasis, not exclusivity.
contextstring, optionalFree-form notes: app size, stores, deadlines, whether anyone has tried this upgrade.
prescan_factsobject{resources: [{id,label}], flags: [{id,label}]} — what a deterministic scan established. Every flags id must come back in coverage_check, which is how you hold the model to the facts.

/estimate creates no job and charges nothing. It returns the model binding and the reservation: hold_credits is what gets held, and the actual charge is normally far lower because the hold prices the full output cap.

5. Run it, then poll

/run returns a job_id; poll jobs/{job_id} until status is succeeded or failed. The plan JSON is the string at data.output.output.

Always send an Idempotency-Key. Derive it from the input, as the web app does (expo-upgrade-desk:<hash>:a<attempt>). A retried request carrying the same key returns the same job instead of billing a second run — which is what makes a CI retry safe.

6. Or stream it

/run-stream is the same call over server-sent events. The web app uses it to advance a staged progress display as section headings arrive, and to keep whatever parsed if the stream dies mid-flight.

The output contract

data.output.output is a JSON string holding one object. This is exactly what the web app parses, so anything that renders here will render there:

{
  "plan_name":  "acme-mobile - SDK 53 to 56 upgrade plan",
  "readiness":  "safe-to-upgrade | staged-upgrade-needed | blocked-until-resolved",
  "verdict":    "one sentence naming the thing that decides the readiness",
  "current_sdk": "53",
  "target_sdk":  "56",
  "workflow":    "cng | bare | unknown",
  "exec_summary": "2-3 paragraphs separated by blank lines",
  "assumptions":    ["..."],
  "open_questions": ["..."],
  "inventory": [
    { "kind": "Dependency", "name": "expo-av", "version": "~15.0.2", "role": "..." }
  ],
  "hops": [
    { "from": "53", "to": "54", "theme": "...", "must_do": ["..."], "watch_for": ["..."] }
  ],
  "findings": [
    {
      "id": "EX-001",
      "category":   "dependencies | config | native | breaking-change | deprecation | hygiene",
      "severity":   "low | medium | high",
      "likelihood": "low | medium | high",
      "priority":   "critical | high | medium | low",
      "resource": "Dependency/expo-av",
      "problem":  "...",
      "impact":   "...",
      "fix":      "...",
      "snippet":  "corrected JSON / JS / shell fragment, or \"\""
    }
  ],
  "coverage_check": [
    { "id": "dep-deprecated:expo-av", "addressed": true, "note": "EX-001." }
  ],
  "corrected_manifest": "the corrected dependencies and devDependencies blocks, as a JSON string",
  "commands":    ["npx expo install expo@latest  # move to the target SDK"],
  "quick_wins":  ["..."],
  "focus_areas": [{ "area": "...", "why": "...", "finding_ids": ["EX-001"] }],
  "summary": "closing paragraph"
}

Two rules worth enforcing on your side, because the app enforces them too: every focus_areas[].finding_ids entry must name a real finding id, and every prescan_facts.flags id must appear exactly once in coverage_check. If a flag is missing from the reconciliation, the model quietly skipped a fact you established — treat that as a failed run, not a passing one.

A CI gate

The readiness value is the natural exit code. Fail the job when a project drifts into blocked-until-resolved, warn on staged-upgrade-needed, and pass on safe-to-upgrade — with the Idempotency-Key derived from the input so a re-run of the same commit replays instead of re-billing.

READINESS=$(printf '%s' "$PLAN" | python3 -c 'import sys,json;print(json.load(sys.stdin)["readiness"])')
case "$READINESS" in
  blocked-until-resolved) echo "::error::Expo upgrade is blocked"; exit 1 ;;
  staged-upgrade-needed)  echo "::warning::Expo upgrade needs staging"; exit 0 ;;
  safe-to-upgrade)        echo "Expo upgrade is clear"; exit 0 ;;
esac