# Endpoints

The live list is always `action=meta.endpoints` — it renders straight out of
`Registry.cfc`, so it cannot drift. This page is the orientation copy: what
exists, and what each one is for.

```bash
curl -H "X-API-Key: $KEY" "https://gcc.wrindustries.com/api/?action=meta.endpoints"
```

---

## meta

| Action | Requires | Cache | What |
|---|---|---|---|
| `meta.endpoints` | any key | 300s | Every endpoint, its params and its access rule |
| `meta.whoami` | any key | none | Confirms a key works; lists what *this* caller can reach |

`meta.whoami` returns `available` — only the actions this caller passes the
access rules for. A client should use it to hide what it cannot call rather than
discovering 403s at runtime.

## ranks

| Action | Requires | Cache |
|---|---|---|
| `ranks.top50` | any key | 60s |

The top 50 empires on a server, mirroring the in-game Rankings page. Rank comes
from the server's live ranking view, so it matches what players see. **NPC and
vacation-mode empires are excluded.**

Params: `server` (`TB`/`RT`, defaults to the caller's own), `by` (`power` or
`planets`), `limit` (1–500).

Every row carries **`protected`** — `true` when the empire is under damage
protection. That is `planetlost >= 3`, the same test both DP branches of
`s_com_attack_req.cfm` refuse an attack on and the in-game Intel page prints
"Damage Protection" for. Whether protection is up is published; **when it lifts
is not**, because a countdown is targeting information. The response is cached
for a minute, so treat the flag as up to a minute stale rather than as
attack-time truth. `empires.info` reports the same flag on the same test.

## chat

| Action | Requires | Cache |
|---|---|---|
| `chat.recent` | any key | none |

Recent lines from the in-game chat lobby, newest first. **Read only — it returns
chat, it does not accept it.** Posting stays in the game, where the flood
control and the session that proves who is speaking already live.

Chat is one **global** lobby, not a per-server one, so there is no `server`
parameter and rows do not say where they came from.

Paging is by id: keep the highest `id` you have seen and pass it back as
`after`, or pass `before` to walk backwards into history. `meta.next_after` is
the cursor for the next call. There is no `since` — see below.

Each line carries `posted`, `posted_utc` and `posted_epoch`, **or `null` for
all three**. The `chat` table had no timestamp at all until 30 Aug 2026; adding
the column stamped every pre-existing row with the instant of the `ALTER`, which
is not when any of them was posted. Those rows report `null` rather than a time
that would claim twenty years of chat happened in one second. `meta.undated`
counts how many of the returned lines are in that state — which is why paging
stays on `id` and there is no time filter.

The timestamp is written by the **database** clock, not the game's, so it is the
one datetime in the app that is not America/New_York on disk. The API takes the
instant from `UNIX_TIMESTAMP()` and derives all three fields from it, so
`posted` is game-local, `posted_utc` is UTC and `posted_epoch` is the epoch —
consistent with every other timestamp the API publishes.

Staff lines carry `badge` (`admin` or `guide`), split out of the name the game
stores it glued onto; `badge` is `null` for ordinary players.

`post` is **raw player text** — the game writes it unescaped and renders it
back the same way, so bodies genuinely contain markup. **Encode it before
putting it in a page.** Posts an admin has scrubbed arrive already scrubbed; the
original text lives only in the admin audit log, which this application cannot
reach.

Params: `limit` (1–200, default 50), `after`, `before`.

## dsr

| Action | Requires | Cache |
|---|---|---|
| `dsr.battles` | `project:5` | short |

Recent battles picked up by Deep Space Radar, newest first — the same feed the
project's in-game page shows. **Requires an active DSR project**; without it the
call is a 403, exactly as the in-game page would be unavailable.

Capped at 5000 entries.

## ships

| Action | Requires | Cache |
|---|---|---|
| `ships.list` | any key | long |

Ship statistics from `ship_type`. Keyed by **ship id**, not name, because names
change. Optional `race` filter (`Neutral`, `Terran`, `AMiner`, `Marauder`,
`Viral`, `Guardian`, `Collective`); omitted means all.

Cost and upkeep are computed with the **game's own formulas**, not read from
stored columns — upkeep comes back as `{ base, viral, collective }`. Hidden
combat internals (`attack_fire`, `defence_fire`, capturability) are deliberately
not exposed.

## planets

| Action | Requires | Cache |
|---|---|---|
| `planets.types` | any key | long |

Planet types as the in-game manual presents them
(`p_manual.cfm?file=planettype`). Percentage modifiers are returned as
percentages. `dig_rate` and `special_flag` are internal and not exposed.

## races

| Action | Requires | Cache |
|---|---|---|
| `races.list` | any key | long |

Playable races only — the ones flagged selectable, i.e. Terran through A.Miners.
The selectable flag itself is not exposed. Modifiers match
`p_manual.cfm?file=race2`, including `demand_for_goods` (which is *not* the same
as industry — an easy thing to mislabel).

---

## Adding one

Two steps, in `Skills/api-endpoint/SKILL.md`. Both docs pages and `meta.endpoints`
update themselves from `Registry.cfc`; the only thing this file needs is a row in
the right table above, for orientation.
