Org API

Read your org's pooled resources, blueprints, Most Wanted and stats from a Discord bot, a spreadsheet or your own site. Read only, by design: nothing can be changed through it.

Getting a key

The org's owner makes keys on the website under Org settings → API access. Give each one a label for what will use it, and tick Member names only if that thing needs to know who holds what, and Activity ledger only if it reads the log. The key is shown once, right after it is made; Hangarworks keeps only a fingerprint of it, so treat it like a password. An org can hold up to five keys, and revoking one stops it on its next call.

A key belongs to the org, not to the person who made it. It keeps working if the org changes hands (the list says who made each one, so a new owner can revoke an old owner's), and it stops when the org is deleted.

Authentication

Send the key as a bearer token in the Authorization header. Never in the URL, which lands in logs and browser history.

curl -H "Authorization: Bearer hw_org_YOUR_KEY" \
  https://www.sc-hangarworks.org/api/v1/org/resources

Call it from a server, a script or a bot. There are no CORS headers on purpose: a key in browser code is a key anyone can read.

Endpoints

All under https://www.sc-hangarworks.org/api/v1, all GET. Every key reads the totals; the Member names scope opens the roster and the per-member query flags, and the Activity ledger scope opens the ledger. Add ?format=csv to any list for a spreadsheet-ready file with the same rows.

PathReturns
/meThe org the key opens, its scopes, and the rate limit with what is left of it. A bot's health check.
/orgProfile: id, name, logo, location, the requested resources with their quality floors, member count, created date, points mode.
/org/resources?members=1One row per resource anyone holds: name, unit (SCU for raw resources, EA for items), total, totals by quality, best quality held, how many hold it, whether it is on the requested list and at what floor. Sorted by total. With ?members=1 (Member names scope) each row also lists who holds it, how much at which quality, and their location notes.
/org/blueprints?holders=1One row per blueprint anyone holds: the game's id, tag, name, manufacturer, gear, type, subtype, grade, and how many members hold it. With ?holders=1 (Member names scope) their nicknames.
/org/blueprints/wanted?holders=1Most Wanted, as the app computes it: blueprints on members' hunt lists that nobody in the org holds yet, with how many want each. ?holders=1 names them.
/org/statsThe org page's headline numbers in one call: members, unique blueprints, total SCU, total EA, open requests, stock units, points issued (when an economy runs), the top ten resources and the top ten wanted blueprints.
/org/stats/history?days=30One point a day of the stats above, oldest first: members, unique blueprints, total SCU and EA, open requests, stock units, points issued and that day's top ten resources. Taken by the daily sweep at 07:00 UTC. days is 30 by default and at most 365, a window of calendar days, so a day the sweep missed is a gap rather than a stretch. The first point appears the morning after the org is made.
/org/membersThe roster (Member names scope): uid, nickname, title, role, rank with its order, division, joined date, patron tier, counts of blueprints, wishlist, SCU, EA and stock units, and their ships by id. In the app's own order: ranks first.
/org/stock?members=1Finished items on offer, pooled by blueprint: the catalog fields, units in total, reserved and open. With ?members=1 (Member names scope) each offer: who, how many, reserved for whom, free or for points, where it is stored.
/org/requests?status=open,claimed,ready,completedRequests in flight by default; name statuses to choose. Each: status, priority, kind (blueprint, kit or resources), what is wanted with quantities and units, bounty, created and last-changed dates, note count. Requester and claimer come along when the key has the Member names scope. Completed ones are the 200 most recent.
/org/ledger?since=&before=&limit=The activity log (Activity ledger scope), newest first: fulfilled requests, stock reservations and their fulfilment, sales. since and before take unix milliseconds or an ISO date, both exclusive; limit is 100 by default and at most 500. Poll with since set to the newest entry you have.

Blueprint and resource names come from the same published crafting catalog the apps read, so a row here matches a row in the app. Resource rows carry their unit because the app never adds SCU to EA, and neither should you.

Responses

Every answer is JSON in one envelope: ok, data, generatedAt (when the org's data was last read) and catalogVersion (the game data the names come from). Field names are a contract: new fields may appear, existing ones will not change or go away without a new version prefix.

{
  "ok": true,
  "data": [
    {
      "name": "Titanium",
      "unit": "SCU",
      "total": 142.6,
      "byQuality": { "3": 100.1, "5": 42.5 },
      "maxQuality": 5,
      "memberCount": 7,
      "requested": true,
      "minQuality": 700
    }
  ],
  "generatedAt": "2026-11-07T12:00:00.000Z",
  "catalogVersion": "4.10.2"
}

/org/stats, the one call for a status line:

{
  "ok": true,
  "data": {
    "members": 23,
    "uniqueBlueprints": 311,
    "totalSCU": 1890.4,
    "totalEA": 64,
    "openRequests": 5,
    "stockUnits": 18,
    "pointsIssued": null,
    "topResources": [{ "name": "Titanium", "unit": "SCU", "total": 142.6 }],
    "topWanted": [{ "id": "…", "name": "Arrowhead", "wantedBy": 4 }]
  },
  "generatedAt": "2026-11-07T12:00:00.000Z",
  "catalogVersion": "4.10.2"
}

/org/stats/history, the same numbers once a day for a trend (the org page draws a month of them under its stat cards):

{
  "ok": true,
  "data": [
    { "day": "2026-11-06", "at": "2026-11-06T07:00:04.000Z", "members": 22, "uniqueBlueprints": 309,
      "totalSCU": 1851.0, "totalEA": 61, "openRequests": 7, "stockUnits": 18, "pointsIssued": null,
      "topResources": [{ "name": "Titanium", "unit": "SCU", "total": 139.1 }] },
    { "day": "2026-11-07", "at": "2026-11-07T07:00:03.000Z", "members": 23, "uniqueBlueprints": 311,
      "totalSCU": 1890.4, "totalEA": 64, "openRequests": 5, "stockUnits": 18, "pointsIssued": null,
      "topResources": [{ "name": "Titanium", "unit": "SCU", "total": 142.6 }] }
  ],
  "generatedAt": "2026-11-07T12:00:00.000Z",
  "catalogVersion": "4.10.2"
}

Errors come with a proper status and the same envelope: 401 for a missing, malformed or revoked key, 403 for a query flag the key's scopes do not cover, 404 when the key's org no longer exists, 429 when the rate limit is hit, 503 while the catalog is unavailable.

{ "ok": false, "error": "unauthorized: send a live org API key as Authorization: Bearer hw_org_…" }

Freshness, caching and limits

An org's data is assembled once a minute at most and every endpoint reads that one snapshot, so polling six paths costs the same as polling one. generatedAt tells you how old it is. Answers carry an ETag; send it back as If-None-Match and an unchanged answer is a 304 with no body.

Each key may make 60 calls a minute. X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (unix seconds) come back on every answer; a 429 adds Retry-After. Nothing in the app changes faster than once a minute, so a bot that polls every minute is already reading as fast as there is anything to read.

The one thing that is not live is the stats history: a point a day, taken at 07:00 UTC by the same sweep that tidies every org. Poll it once a day, after that, and you have it all.

CSV

?format=csv on resources, blueprints, Most Wanted, members, stock, requests, the ledger and the stats history answers the same rows as comma-separated text with a header line, nested parts folded into one cell (3:15; 5:2.5 for totals by quality, names joined with semicolons). With ?members=1 on resources or stock the CSV is one row per member per entry instead, which is what a pivot table wants. Cells that would read as a formula are quoted, so a member named =SUM(A1) stays a name.

Google Sheets

Sheets' IMPORTDATA cannot send a header, and a key in a cell is shared with everyone who can see the sheet. Use Apps Script instead, with a timed trigger:

// Google Sheets: Extensions → Apps Script. Keep the key here, not in a cell.
const KEY = "hw_org_YOUR_KEY";

function pullResources() {
  const res = UrlFetchApp.fetch("https://www.sc-hangarworks.org/api/v1/org/resources", {
    headers: { Authorization: "Bearer " + KEY },
  });
  const rows = JSON.parse(res.getContentText()).data;
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Resources");
  sheet.clearContents();
  sheet.appendRow(["Resource", "Unit", "Total", "Best quality", "Members", "Requested"]);
  rows.forEach((r) => sheet.appendRow([r.name, r.unit, r.total, r.maxQuality, r.memberCount, r.requested]));
}
// Then Triggers → add a time-driven trigger for pullResources, hourly.

Python

import requests

KEY = "hw_org_YOUR_KEY"
r = requests.get("https://www.sc-hangarworks.org/api/v1/org/stats", headers={"Authorization": f"Bearer {KEY}"})
r.raise_for_status()
stats = r.json()["data"]
print(f'{stats["members"]} members, {stats["totalSCU"]:.0f} SCU pooled')

The rules

  • Read only. A key can never create, change or delete anything.
  • A key sees what every member of the org already sees in the app, and nothing from anyone's private profile. With Member names off it sees totals only; the ledger needs its own scope.
  • Keys are secrets. Keep them out of repositories, cells, URLs and browser code. Revoke one you have lost.
  • Stay under the limit. If your use needs more than 60 calls a minute, say so and we will talk.

Hangarworks is an unofficial fan tool and is not affiliated with or endorsed by Cloud Imperium Games. Star Citizen® and Squadron 42® are registered trademarks of Cloud Imperium Rights LLC.