---
name: unofficial-star-citizen-api-v2-migration
description: Migrate code that calls the v1 Unofficial Star Citizen API (URLs like /{apikey}/v1/live/user/..., /{apikey}/cache/ships, success === 1 checks) to the v2 API (x-api-key header, /v2/... paths, ?refresh=true). Use when a codebase calls starcitizen-api.com with the key in the URL, when asked to upgrade, port or migrate to v2, or when v1 calls start failing or returning deprecation warnings.
---

# Migrate from v1 to v2 of the Unofficial Star Citizen API

v1 is deprecated and no longer maintained: no new features, no bug fixes, no support. It currently runs as a compatibility layer on top of v2 and will be removed in a future release. Every v1 call in the codebase has a v2 replacement.

- API host: `https://api.starcitizen-api.com`
- v1 OpenAPI spec (the API being migrated from): `https://api.starcitizen-api.com/v1/swagger.json`
- v2 OpenAPI spec (the target, source of truth for parameters and schemas): `https://api.starcitizen-api.com/swagger.json`
- Full mapping reference: `https://starcitizen-api.com/docs/reference/v1-to-v2-mapping`

Fetch both specs at the start of the migration (no API key needed). Use the v1 spec to identify each existing call and the fields it returned, and the v2 spec to find its replacement's exact path, parameters and response schema.

## Workflow

1. **Inventory.** Search the codebase for every v1 call: `starcitizen-api.com`, `/v1/`, `/live/`, `/cache/`, `/auto/`, `/eager/`, `success === 1`, `success == 0`, `json_path`. List each call site with its v1 path and mode, and match it to its operation in the v1 spec.
2. **Centralize.** If calls are scattered, introduce one HTTP client (base URL + `x-api-key` header + error handling) before changing paths. Migrate the call sites one at a time onto it.
3. **Translate each call** with the rules and tables below. Check every v2 path and parameter against the v2 spec; when a mapping is unclear, compare the two specs rather than guessing.
4. **Re-check the fields read from `data`.** v1 re-shaped many payloads; v2 returns the raw models (see "Payload changes"). Compare the v1 response schema with the v2 one for each endpoint and update types from the v2 schema.
5. **Verify.** Run the existing tests. Make one real cached request per migrated endpoint (no `?refresh=true`, so it costs nothing) and compare the fields the code reads.

## Core changes (apply to every call)

| Topic | v1 | v2 |
|-------|----|----|
| Auth | Key in the URL: `/{apikey}/v1/...` | Header `x-api-key: <key>` on every request |
| Path | `/{apikey}/v1/{mode}/{endpoint}` (`/v1` optional) | `/v2/{endpoint}` — paths are renamed per endpoint, see the table below |
| Mode `live` / `eager` | Path segment | `?refresh=true` — costs daily quota |
| Mode `cache` / `auto` | Path segment | No parameter — cached data, always **free** |
| `success` | `1` / `0` | `true` / `false` |
| Freshness | `source: "live" \| "cache"` | `updatedAt`, `expiresAt` (ISO 8601) |
| Errors | HTTP 200 + `success: 0` (old behavior) | Real HTTP status + `{ "success": false, "message", "code" }` |
| `json_path` | Supported by the old API | Not supported — filter client-side |
| `/gamedata/...` | — | `410 Gone`, no replacement — remove the calls |

Rules:

- Keep the key out of URLs, logs and client-side code. Read it from configuration (environment variable or secret store) and send it only in the `x-api-key` header from a server.
- Branch on the HTTP status (`res.ok`), never on `message`. A `success === 0` check no longer catches errors: v1 paths already return real status codes today.
- Translate `live`/`eager` to `?refresh=true` **only where real-time data is truly required.** Cached reads are free and the cache re-scrapes on its own when it expires; most `live` calls should become plain cached calls. `?refresh=true` costs 1 daily request, 50 on ship catalog routes.
- Handle `401` (missing/invalid key), `403` (account blocked), `404` (not found on the RSI website), `429` (daily quota or per-minute rate limit — wait for `Retry-After` seconds) and `5xx`.

## Endpoint mapping

v1 paths are shown without the `/{apikey}/v1/{mode}` prefix.

| v1 | v2 |
|----|----|
| `/me` (no mode segment) | `GET /v2/me` |
| `/user/{handle}` | `GET /v2/users/{handle}` (or `/profile`, `/organization` sub-routes) |
| `/organization/{sid}` | `GET /v2/organizations/{sid}` |
| `/organization_members/{sid}` | `GET /v2/organizations/{sid}/members` |
| `/stats` | `GET /v2/funding-goals` (`?chart=hour\|day\|week\|month`) |
| `/ships` (no filter) | `GET /v2/ships` |
| `/ships?name=&classification=...` | `GET /v2/ships/search?name=&type=...` |
| `/ships?id={id}` | `GET /v2/ships/{id}` |
| `/versions` | `GET /v2/versions` |
| `/versions?filter=latest` | `GET /v2/versions` → read `data.latest` |
| `/roadmap/{board}` | `GET /v2/roadmap/{board}` (`starcitizen` or `squadron42`) |
| `/progress-tracker` | `GET /v2/progress-tracker/teams` |
| `/progress-tracker/{team_slug}` | `GET /v2/progress-tracker/deliverables/by-team-slug/{slug}` |
| `/telemetry/{version}` | `GET /v2/telemetry/{version}?timetable=MONTH` (`timetable` is **required**: `DAY`, `WEEK`, `MONTH`) |
| `/starmap/search?name=X` | `GET /v2/starmap/search?query=X` |
| `/starmap/systems` | `GET /v2/starmap/systems` |
| `/starmap/systems?name=X` | `GET /v2/starmap/systems/by-name/{name}` |
| `/starmap/star-system?code=X` | `GET /v2/starmap/star-systems/{code}` |
| `/starmap/object?code=SYS.A.B` | `GET /v2/starmap/objects/{SYS}/by-code/{SYS.A.B}` (system code = first segment) |
| `/starmap/affiliations` | `GET /v2/starmap/affiliations` |
| `/starmap/affiliations?name=X` | `GET /v2/starmap/affiliations/by-name/{name}` |
| `/starmap/species` | `GET /v2/starmap/species` |
| `/starmap/species?name=X` | `GET /v2/starmap/species/by-name/{name}` |
| `/starmap/tunnels` | `GET /v2/starmap/tunnels` |
| `/starmap/tunnels?id=X` | `GET /v2/starmap/tunnels/{id}` |
| `/gamedata/...` | Removed (`410 Gone`) |

## Parameter changes

- **Organization members:** `rank` → `rankIndex`, `role` → `roleIndex`, `main_org` → `isMainOrg`. `page` is unchanged (32 members per page). New: `search`.
- **Ships search:** `classification` → `type` (comma-separated or repeated). `id` → use `/v2/ships/{id}`. `page` and `page_max` are removed: search returns every match. Range filters (`length_*`, `crew_*`, `price_*`, `mass_*`) keep their names.
- **Ships by name:** `/v2/ships/by-name/{name}` is an exact, case-insensitive match returning one ship or `404`. Partial names go to `/v2/ships/search?name=`.
- **Roadmap:** `date_min` / `date_max` are not supported in v2 — filter the board client-side.
- **Versions:** the `filter` parameter does not exist in v2.
- Unknown query parameters are ignored, not rejected. Remove v1-only parameters anyway: they silently do nothing.

## Payload changes

v2 returns raw models. Re-check every field the code reads:

- **Users:** v1 merged profile, main organization and affiliations into one object; v2 `/v2/users/{handle}` has its own shape, with `/profile` and `/organization` sub-routes.
- **Funding goals:** `funds` is in **cents** (v1 divided by 100). `current_live` / `current_ptu` / `current_etf` are gone — use `/v2/versions`.
- **Versions:** v1 returned an array of strings; v2 returns `{ latest: {...}, versions: [...] }` with full objects.
- **Ships:** v1 converted numeric strings to numbers and rewrote `url` as a relative path; v2 returns the raw ship model.
- **Roadmap:** v1 normalized `released` to `0`/`1` and added `category_name`; v2 returns the raw model.
- **Progress tracker:** dates (`startDate`, `endDate`, `updateDate`) are ISO strings; v1 used RFC 1123.
- **Starmap:** v1 converted numeric strings (positions, sizes, ids) to numbers; v2 returns raw values — parse explicitly where the code does math.

## Example

```javascript
// v1
const res = await fetch(`https://api.starcitizen-api.com/${API_KEY}/v1/live/user/dymerz`);
const body = await res.json();
if (body.success === 0) throw new Error(body.message);

// v2 — cached read (free); add ?refresh=true only if live data is required
const res = await fetch('https://api.starcitizen-api.com/v2/users/dymerz', {
  headers: { 'x-api-key': process.env.USC_API_KEY },
});
if (!res.ok)
{
  const err = await res.json(); // { success: false, message, code }
  throw new Error(`${err.code}: ${err.message}`);
}
const { data, updatedAt, expiresAt } = await res.json();
```

## Optional improvements once migrated

- Send `If-None-Match` with the previous `ETag` to get `304 Not Modified` instead of the full body.
- Cache client-side until `expiresAt` instead of polling.
- Check `GET /v2/me` (`dailyRequestsRemaining`) before scheduled `?refresh=true` jobs.
