---
name: unofficial-star-citizen-api-integration
description: Build features on the Unofficial Star Citizen API v2 (api.starcitizen-api.com) — citizens, organizations and members, ships, roadmap, progress tracker, starmap, funding goals, telemetry and game versions scraped from the RSI website. Use when writing code, a bot, a website or a script that reads Star Citizen / RSI data, when choosing an endpoint, handling quota and caching, or when deriving types and clients from the OpenAPI spec.
---

# Integrate the Unofficial Star Citizen API (v2)

A read-only REST API over data scraped from the RSI website, with a server-side smart cache. Cached reads are free; only forced live scrapes cost daily quota.

- API host: `https://api.starcitizen-api.com` — every route is under `/v2`
- OpenAPI spec (source of truth for parameters and schemas): `https://api.starcitizen-api.com/swagger.json`
- Docs: `https://starcitizen-api.com/docs/introduction` · page index for agents: `https://starcitizen-api.com/llms.txt`
- API key: created by the user at `https://starcitizen-api.com/auth/profile` (Discord or Google sign-in)

## Using the OpenAPI spec while building

Fetch the OpenAPI document at `https://api.starcitizen-api.com/swagger.json` (no API key needed) **before writing types, parsers or UI**:

- Look up each endpoint the feature uses under `paths` and read its exact parameter names, allowed values and response schema; follow the `$ref`s into `components.schemas`.
- Derive types from the spec, not from guesses or from the endpoint table below. Generators such as `openapi-typescript` can produce them directly from the spec URL.
- When a schema is ambiguous, confirm it with one cached request (no `?refresh`, so it costs nothing) on a canonical test value.
- `GET /v2/me` shows the daily limit and remaining requests.

## Request rules

- Send the key in the `x-api-key` header on every request. Never in the URL.
- **Keep the key server-side.** Read it from an environment variable or secret store; never ship it in browser or mobile code, never commit it, never log it. A frontend calls the application's own backend, which calls the API.
- Default to **cached reads (no `?refresh`)** — free, and the API re-scrapes automatically once the cache expires.
- Use `?refresh=true` only for an explicit user action that needs real-time data. It costs **1 daily request**, **50** on ship catalog routes (`/v2/ships`, `/search`, `/by-type`, `/by-manufacturer`), and only 1 on `/v2/ships/{id}`, `/by-name/{name}`, `/by-chassis/{chassisId}`. Never put it in loops, polling or page loads.
- Unknown query parameters are ignored silently: check parameter names against the spec.

## Response envelope

```json
{
  "success": true,
  "message": "ok",
  "data": { },
  "updatedAt": "2026-11-06T15:30:02.373Z",
  "expiresAt": "2026-11-09T15:30:02.373Z"
}
```

Errors use real HTTP status codes and `{ "success": false, "message": "...", "code": 404 }`. Branch on the status, never on `message`.

| Status | Meaning | Handling |
|--------|---------|----------|
| `304` | `If-None-Match` matched the `ETag` | Reuse the cached copy |
| `400` | Invalid parameter value | Fix the request, do not retry |
| `401` | Missing, malformed or unknown key | Check configuration, do not retry |
| `403` | Account blocked | Do not retry |
| `404` | Not found on the RSI website | Show "not found", cache the miss briefly |
| `410` | Removed route (`/gamedata/...`) | Remove the call |
| `429` | Daily quota or per-minute rate limit reached | Wait `Retry-After` seconds; never retry immediately |
| `5xx` / `503` | API or RSI website unavailable | Retry with exponential backoff, serve stale data meanwhile |

Useful response headers: `ETag`, `Last-Modified`, `Expires`, `X-Quota-Consumed` (daily requests used by the call), `X-RateLimit-Limit` / `-Remaining` / `-Reset` (per-minute window, applies to every request, cached or not).

## Endpoints

All `GET`, all accept `?refresh=true`. Paths relative to `https://api.starcitizen-api.com`.

| Area | Endpoints |
|------|-----------|
| Account | `/v2/me` — daily limit, `dailyRequestsRemaining`, keys |
| Citizens | `/v2/users/{handle}`, `/v2/users/{handle}/profile`, `/v2/users/{handle}/organization` |
| Organizations | `/v2/organizations/{sid}`, `/v2/organizations/{sid}/members` (`page`, `search`, `rankIndex`, `roleIndex`, `isMainOrg`; 32 per page), `/v2/organizations/search` (`search`, `page`, `sort`, `size`, `activity`, `commitment`, `languages`, `model`, `recruiting`, `roleplay`) |
| Ships | `/v2/ships` (whole catalog), `/v2/ships/search` (`name`, `type`, `focus`, `manufacturer`, `size`, `production_status`, `*_min`/`*_max` ranges), `/v2/ships/{id}`, `/v2/ships/by-name/{name}` (exact match), `/by-type/{type}`, `/by-manufacturer/{code}`, `/by-chassis/{chassisId}`, `/v2/ships/manufacturers` |
| Roadmap | `/v2/roadmap/{board}` — `starcitizen` or `squadron42`, optional `version` |
| Progress tracker | `/v2/progress-tracker/teams`, `/teams/by-slug/{slug}`, `/deliverables`, `/deliverables/by-slug/{slug}`, `/deliverables/by-team-slug/{slug}` |
| Starmap | `/v2/starmap/search?query=`, `/systems` (+ `/{id}`, `/by-name/{name}`, `/by-code/{code}`), `/star-systems/{code}`, `/objects/{code}` (+ `/{id}`, `/by-name/{name}`, `/by-code/{objectCode}`), `/affiliations`, `/species`, `/tunnels` (each + `/{id}`, `/by-name/{name}`, `/by-code/{code}`), `/routes?from=&to=&ship_size=` |
| Funding | `/v2/funding-goals?chart=hour\|day\|week\|month` — `funds` is in **cents** |
| Telemetry | `/v2/telemetry/{version}?timetable=DAY\|WEEK\|MONTH` (`timetable` required) |
| Versions | `/v2/versions` — `{ latest, versions }` |

Canonical test values: citizen handle `dymerz`, organization SID `PROTECTORA`, starmap system `STANTON`.

## Recommended client shape

Build one small client module and route every call through it:

```typescript
const BASE_URL = 'https://api.starcitizen-api.com';

export class ScApiError extends Error
{
  constructor(public status: number, message: string, public retryAfter?: number)
  {
    super(message);
  }
}

export async function scApi<T>(path: string, params: Record<string, string> = {}): Promise<T>
{
  const url = new URL(path, BASE_URL);
  for (const [key, value] of Object.entries(params))
    url.searchParams.set(key, value);

  const res = await fetch(url, { headers: { 'x-api-key': process.env.USC_API_KEY! } });
  if (!res.ok)
  {
    const body = await res.json().catch(() => ({ message: res.statusText }));
    const retryAfter = Number(res.headers.get('Retry-After')) || undefined;
    throw new ScApiError(res.status, body.message, retryAfter);
  }

  const body = await res.json();
  return body.data as T;
}

// const org = await scApi<Organization>('/v2/organizations/PROTECTORA');
```

Add as the feature needs:

- **Local cache** keyed by URL, valid until the response's `expiresAt`; serve it instead of calling again.
- **Conditional requests:** store the `ETag` and send `If-None-Match`; a `304` has no body.
- **Quota guard:** before scheduled `?refresh=true` jobs, read `dailyRequestsRemaining` from `/v2/me`.
- **Pagination:** organization members and organization search use `page`; ship search returns all matches.

## Checklist before finishing

- [ ] Key read from configuration, sent only as `x-api-key`, never reaches a browser or logs.
- [ ] No `?refresh=true` in loops, polling, startup or page loads.
- [ ] Every call branches on HTTP status; `429` honors `Retry-After`; `5xx` retries with backoff.
- [ ] Types match real responses (checked against the OpenAPI spec, confirmed with a cached request where unclear).
- [ ] Unit conversions handled (`funds` in cents, ISO date strings).
