---
name: api-endpoint
description: Add or change a GCC Player API endpoint — the Registry.cfc entry, the this.routable allowlist, access rule, caching, params and the response envelope. Use when writing or modifying anything in app/api/components/*.cfc.
---

# Adding a Player API endpoint

Endpoints live in `app/api/components/<Module>.cfc`, all extending `Base.cfc`.
Read [`../../docs/CONVENTIONS.md`](../../docs/CONVENTIONS.md) before writing
CFML here.

## Checklist

1. Add the entry to **`Registry.cfc`** — this is the declaration.
2. Add the method to the component named in `cfc`.
3. **Add the method name to that component's `this.routable`** — otherwise the
   router 500s with "registered but not exposed by its component".
4. Read every input through `p` / `pInt` / `pTrim` / `pRange`.
5. Resolve the server with `resolveServer()`, never a raw slot.
6. Bind every dynamic value in the query.
7. Wrap the body in `cached(...)` if the data tolerates it, and describe it with
   `cacheMeta(...)`.
8. Exit through `apiOut(...)` / `apiError(...)`.
9. Verify with the `api-verify` skill.

If the component itself is new, create it under `components/` extending `Base`,
with its own `this.routable`.

## The registry entry

```cfml
"ships.list": {
    "cfc":      "Ships",
    "method":   "list",
    "summary":  "Ship statistics.",
    "detail":   "Keyed by ship id, because names change. Cost and upkeep are "
              & "computed with the game's own formulas.",
    "requires": "",          // or "project:<n>"
    "cache":    3600,        // 0 = never cache
    "params": [
        { "name": "race", "type": "string", "required": false, "default": "all",
          "desc": "Neutral, Terran, AMiner, Marauder, Viral, Guardian or Collective." }
    ]
}
```

`summary`, `detail` and `params` are what players read — `meta.endpoints` and the
in-game docs page render straight from them. Write them for a player, not for a
maintainer.

## The method

```cfml
component extends="Base"
{
    this.routable = "list";

    variables.TTL = 3600;

    void function list()
    {
        var race = pTrim("race", "", 20);
        var slot = resolveServer();

        var payload = cached("ships_list_" & slot & "_" & lCase(race), variables.TTL, function()
        {
            var rows = qGame("
                SELECT id, name, race
                FROM ship_type
                WHERE (:race = '' OR race = :race)
                ORDER BY id
            ", { race: [ race, "varchar" ] });

            return qRows(rows);
        });

        apiOut(payload, cacheMeta(variables.TTL, arrayLen(payload)));
    }
}
```

## Traps

**`this.routable`, never `this.endpoints`.** A property on `this` shadows a
method of the same name. Naming the allowlist `endpoints` made `Meta.endpoints()`
unreachable — every call failed with *"Member [endpoints] ... is not a
function"*. Whatever the allowlist is called is barred from being a method name.

**Cache keys must include every input.** `cached("ranks_top50", ...)` would serve
one server's ranks for the other. Include the server, the sort, the filter —
anything that changes the result.

**Reserved scopes.** `var server`, `var local`, `var url` silently resolve to
CFML scopes. Use `slot`, `srv`, `rows`.

**No angle-bracketed tag names in CFC comments.** Lucee parses `<cfquery>` inside
`/** */` and fails the component at parse time. Write "the cfquery tag".

**Do not expose internals.** Hidden combat values, `dig_rate`, `special_flag`,
the race selectable flag — these were deliberately left out. If a column is not
visible in-game, it does not belong in the API.

## Changing an existing endpoint

This is a **public contract**. Adding a field is safe. Renaming or removing one
breaks callers you cannot see and cannot notify. If a field must change meaning,
add the new one and leave the old.
