Start
The API always describes the build players are on. When the game ships, the API updates itself. Every response carries X-Citadel-Build, the commit of that build, and /v1/status reports it.
| Route | Returns |
|---|---|
GET /v1/status | The build, when it went live, and counts. |
GET /v1/items, /v1/items/{id} | Modules and conduits players can obtain. |
GET /v1/hulls, /v1/hulls/{id} | Hulls with their fitting grids. |
GET /v1/ciphers, /v1/artifacts, /v1/regalia, /v1/regalia-sets, /v1/heretical-specials, /v1/harmonic-sets, /v1/affinity | What an item roll can reference. |
GET /v1/modifier-pools, /v1/qualities, /v1/placement-rules | The roll and placement rules. |
GET /v1/npcs | NPC ships, bosses and their weapons. |
GET /v1/stats | Every stat type, worded as the game words it. |
GET /v1/art/{file} | WebP art. File names come from the art objects. |
POST /v1/fits/evaluate, /v1/fits/import, /v1/fits/export, /v1/items/facts | Tools that run the game's simulation on a fit. |
Ids are the game's asset GUIDs as decimal strings and stay the same across builds. Fixed-point values are raw decimal strings: divide by 65536. Errors are { "error": { "code", "message" } } with status 400, 404, 405, 413, 415, 429, 500 or 503.
Rules
User-Agent
Send a User-Agent that names your app and a contact, for example MyFitTool/1.0 (me@example.com). A missing or generic one, such as curl/8.4.0 or python-requests/2.31, is still served, with a Warning header telling you to fix it. Blocking such callers stays a later decision.
Rate limits
Each client has a bucket of 60 requests that refills at 10 a second. Art has a bucket of its own (600, refilling at 100 a second), and sde.zip has one too (5, refilling one every 6 seconds). Requests whose body fails validation still count. A 429 carries Retry-After, in seconds. Request bodies are capped at 256 KB.
Caching
Every response has an ETag and a Cache-Control. Send the tag back in If-None-Match and expect a 304 until the data changes. Art files are named by a hash of their bytes and are served immutable for a year. CORS is open to every origin, so browsers can call the API directly.
Examples
curl
curl -H 'User-Agent: MyFitTool/1.0 (me@example.com)' https://api.thecitadel.com/v1/status
# Conditional request: 304 with no body while the data is unchanged
curl -i -H 'User-Agent: MyFitTool/1.0 (me@example.com)' \
-H 'If-None-Match: "<etag from the last response>"' \
https://api.thecitadel.com/v1/hulls
curl -H 'User-Agent: MyFitTool/1.0 (me@example.com)' \
-H 'Content-Type: application/json' \
-d '{ "fitString": "<a fit string from the game>" }' \
https://api.thecitadel.com/v1/fits/import
JavaScript
Browsers set their own User-Agent and cannot change it; the API serves them without a warning. Server-side code should set one.
const api = "https://api.thecitadel.com";
const { build, hulls } = await fetch(`${api}/v1/hulls`).then((r) => r.json());
console.log(build, hulls.length);
// Art: every item and hull has an `art` object of API-relative URLs.
const icon = `${api}${hulls[0].art.base}`;
// Evaluate it with the game's own simulation.
const imported = await fetch(`${api}/v1/fits/import`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ fitString }),
}).then((r) => r.json());
const result = await fetch(`${api}/v1/fits/evaluate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ fit: imported.fit }),
}).then((r) => r.json());
console.log(result.valid, result.rating);
Bulk download
If you need everything, download one file instead of crawling.
It holds one JSON file per static route, the art under art/, and a manifest.json with the build and a SHA-256 for every other file. The JSON files are byte for byte what the routes serve. It changes only when the data does, so use If-None-Match here too.
Stability policy
- Adding a route or a field is not a breaking change and happens without notice. Ignore fields you do not know.
- Removing or renaming a field, or changing its meaning or type, is a breaking change. Breaking changes go to
/v2./v1keeps running for at least 90 days after/v2ships, with aDeprecationheader. - Game data changes are not contract changes: items come and go with builds.
- The OpenAPI document is checked against a committed snapshot on every change, so a contract change is always visible in review.
Terms Draft
Short version: community and non-commercial use of the data and art, credit "Data from the Citadel game data API", do not imply we endorse your tool, and the service may change or stop. The full text is a draft that the owner has not yet approved: read the draft terms.
Reference
Generated from /v1/openapi.json, the OpenAPI 3.1 document.