Codex

Last reviewed May 4, 2026

For the conceptual overview, see the codex docs. All endpoints are under /v1/codex and require no authentication — codex data is public. The HTTP API is read-only; imports happen via CLI inside the api container.

Quick start

Fetch one entity, then narrow a list with a filter:

shell
# Single entity by canonical id
curl https://api.katforge.com/v1/codex/dark-and-darker/id.item.longsword_5001

# Every epic-rarity item
curl 'https://api.katforge.com/v1/codex/dark-and-darker/item?filter[data.rarity_type]=Type.Item.Rarity.Epic&limit=20'

The full grammar lives in Entities → Filter grammar.

The legacy YAML-backed endpoints GET /v1/codex/{game}/manifest.json and the unprefixed GET /v1/codex/{game}/{id} (matching anything, not just id.<type>.<name>) are no longer served. Internal callers still resolve YAML data via KAT\Codex\Codex for EmberService and LevelService; consumer-facing access goes through the DB-backed endpoints below.

Data sources

Each endpoint reads from one of two backends. Endpoint subsections include a Source line so you can tell at a glance.

BackendWhere it livesUsed forEndpoints
Postgres codex schemaapi.katforge.com Postgres, see Schemabulk-extracted game data with full patch historymost of this page
Content-addressed assetsvar/codex-assets/<hash[0..2]>/<hash>.<ext> on disk, indexed by codex.assetsbinary blobs (icons, portraits, audio)/v1/codex/assets/{hash}
Games dictionaryapi.katforge.com/data/codex/games.yamldisplay names, abbreviations, adapter classGET /v1/codex/games (joined with patches)

The materialized views entity_current and locale_strings_current rebuild from their source-of-truth ledgers (entities, locale_strings) at every import, so endpoints reading the views always see the latest committed patch.

Games

GET /v1/codex/games

List of all games registered in the codex. One entry per game with slug, display name, available branches, and the current patch on the main branch.

  • Auth: public
  • Cache: Cache-Control: public, max-age=60
  • Source: data/codex/games.yaml (slug, display name, abbreviation) joined with codex.patches (current patch per branch)
GET/v1/codex/games

Manifest

GET /v1/codex/{game}/manifest

Bundled metadata for a game on a given branch. Type counts plus the current patch.

  • Auth: public
  • Cache: Cache-Control: public, max-age=60
  • Source: codex.entity_resolved_current (per-type counts, grouped by type) + codex.patches (current patch)

Path / Query

ParamTypeDefaultNotes
gamestring(required)DB-backed game slug
branchstringmainBranch slug

Patches

GET /v1/codex/{game}/patches

Patches for a game on a branch, newest first.

QueryTypeDefaultNotes
branchstringmainBranch slug
  • Source: codex.patches filtered by (game, branch), ordered by imported_at DESC

GET /v1/codex/{game}/patches/{version}

Single patch by version (canonical semver).

  • Source: codex.patches keyed by (game, branch, version)

GET /v1/codex/{game}/patches/{a}/diff/{b}

Field-level diff between two patches on the same branch. a and b accept canonical semver versions; the response always sorts from to be the earlier patch.

  • Source: codex.entity_diffs between the two patch_ids

Response200 OK

JSON
{
   "from": { "id": 41, "version": "0.16.134+build.8542", "version_raw": "0.16.134.8542" },
   "to":   { "id": 42, "version": "0.16.135+build.8645", "version_raw": "0.16.135.8645" },
   "entries": [
      {
         "id":     "id.item.iron_sword",
         "type":   "item",
         "op":     "added",
         "after":  { "...full new entity data..." }
      },
      {
         "id":     "id.item.oak_bow",
         "type":   "item",
         "op":     "removed",
         "before": { "...full prior entity data..." }
      },
      {
         "id":   "id.item.common_sword",
         "type": "item",
         "op":   "changed",
         "changes": [
            { "path": "data.gear_score",                  "before": 15, "after": 18 },
            { "path": "data.primary.physical_damage.max", "before": 22, "after": 25 }
         ]
      }
   ],
   "summary": { "added": 1, "removed": 1, "changed": 1 }
}

op is one of added, removed, changed. Path syntax is dotted and jq-compatible (data.primary.physical_damage.max); array indices use bracket notation (data.abilities[2]). The format is symmetric — swap beforeafter to invert.

Entities

GET /v1/codex/{game}/{type}

Paginated list of entities of one type. Filterable, searchable.

QueryTypeDefaultNotes
branchstringmainBranch slug
limitint501..200
cursorintnullResolved-row cursor returned as next from a prior page
qstringnullFull-text search across name + description
filter[<path>][:<op>]stringSee Filter grammar below
  • Source: codex.entity_resolved_current filtered by (game, branch, type) with optional q / filter[*] predicates

The response wraps entities (see single entity for shape) with count (filtered total, not table total) and next (cursor to pass on the next request, or null on the last page).

Filter grammar

The query string filter[<path>][:<op>]=<value> compiles to a SQL predicate against the resolved-current view.

OperatorSuffixExampleReads as
equalseq (default)filter[type]=itemtype is item
not equalsnefilter[type:ne]=questtype is not quest
less thanltfilter[data.gear_score:lt]=50gear_score < 50
less or equalltefilter[data.gear_score:lte]=50gear_score ≤ 50
greater thangtfilter[data.gear_score:gt]=100gear_score > 100
greater or equalgtefilter[data.gear_score:gte]=100gear_score ≥ 100

Tag membership uses the bare-key form: filter[tags]=weapon matches any entity whose tags[] contains weapon. Numeric comparisons coerce the JSON text to numeric; non-numeric values return 400.

The compiler refuses any path containing characters outside [a-zA-Z0-9._] and any path that doesn't start with data., name, type, or tags.

GET /v1/codex/{game}/{id}

Single entity at the current patch. {id} must start with id. or be the upstream key (Id_…); upstream keys resolve via codex.source_keys.

QueryTypeDefaultNotes
branchstringmainBranch slug
at_patchstringnullCanonical or raw version. When set, returns the entity as it was at that patch
hydratestringnullComma-separated rels to inline-resolve from links (e.g. primary_property,origin)
  • Source: codex.entity_resolved_current keyed by (game, branch, type, id), enriched with codex.assets for icon / assets[].hash. With ?at_patch=, falls back to the codex.entities ledger via EntityRepository::atPatch(). With ?hydrate=, follows codex.entity_links to resolve named rels.

Response200 OK

jsonc
{
   "id":     "id.item.longsword_5001",
   "type":   "item",
   "game":   "dark-and-darker",
   "branch": "main",
   "name":   "Longsword",
   "patch":  { "id": 4, "version": "0.16.135.8645-2663", "version_raw": "0.16.135.8645-2663" },
   "tags":   [ "primary", "two_handed", "epic", "sword", "weapon" ],
   "icon":   {
      "hash": "22cd8c50c8829958106150b86dc5a436567a01df7015404f213669c5675b5b39",
      "url":  "/v1/codex/assets/22cd8c50c8829958106150b86dc5a436567a01df7015404f213669c5675b5b39"
   },
   "data":   { "gear_score": 50, "rarity_type": "Type.Item.Rarity.Epic", ... },
   "links":  [ { "rel": "primary_property", "target": "id.item_property.primary_longsword_5001" }, ... ]
}

icon is an object (or null). The hash is content-addressable; the URL points at the asset endpoint below.

When ?hydrate=rel1,rel2 is set, the response carries an additional hydrated object keyed by rel — every requested rel appears, with null for misses (target entity not yet imported as a Type) and the full entity row for hits:

jsonc
{
   "id":   "id.item.longsword_5001",
   ...,
   "hydrated": {
      "primary_property": null,
      "origin":           { "id": "id.item.longsword_1001", "name": "Longsword", ... }
   }
}

404 when the entity does not exist at the queried patch (including tombstoned).

GET /v1/codex/{game}/{id}/history

Version history for an entity, oldest first. One row per change.

  • Source: codex.entities ledger filtered by (game, branch, type, id) joined with codex.patches, ordered by patch_id ASC

Each row carries row_id, patch_id, patch_version (canonical), patch_version_raw, released_at, content_hash, and is_tombstone.

Diffs

GET /v1/codex/{game}/diff

Whole-game diff between two patches. Returns added / removed / changed entities with field-level paths. Same shape as /patches/{a}/diff/{b} (which sorts by version) but takes from / to query params for clarity.

QueryTypeNotes
fromstring(required) version
tostring(required) version
branchstringdefault main
  • Source: codex.entity_diffs filtered to the (from_patch_id, to_patch_id) pair

GET /v1/codex/{game}/diff/{id}

Single-entity diff. Same query params. Returns op: unchanged with changes: [] when the entity didn't change in the (from, to] range — distinct from a 404 which means one of the patches doesn't exist.

  • Source: codex.entity_diffs filtered to (from_patch_id, to_patch_id, type, id); falls back to a live diff over codex.entities if the precomputed pair is missing

Assets

GET /v1/codex/assets/{hash}

Streams a content-addressed binary asset (icons, portraits, audio). The 64-char hex {hash} is the SHA-256 of the bytes — values come from each entity's icon object.

  • Auth: public
  • Cache: Cache-Control: public, max-age=31536000, immutable; ETag is the hash
  • Content-Type: declared at registration time (image/png, image/webp, audio/ogg, ...)
  • Content-Disposition: inline; filename=<hash>.<ext>
  • Source: codex.assets row keyed by hash (for media_type / bytes), backed by the file at var/codex-assets/<hash[0..2]>/<hash>.<ext>

?size=WxH and ?format=webp are CDN hints — the API itself always serves the original bytes. A fronting CDN can cache resized/reformatted variants by the query string; the canonical URL without params is the source of truth.

404 when the hash isn't registered.

Changelog

GET /v1/codex/{game}/changelog

Built changelog for a patch range. Returns added / changed / removed entities grouped by type, with field-level paths for changes. Either supply from + to explicitly, supply only to (changelog vs. the previous patch), or omit both (latest patch on branch vs. its parent).

  • Auth: public
  • Cache: public, max-age=300

Query params

FieldNotes
branchdefault main.
formatjson (default) or markdown / md — returns rendered markdown with Content-Type: text/markdown.
typefilter to a single entity type (e.g. item, monster).
fromearlier patch version. With both from and to, the changelog covers the half-open range (from, to].
tolater patch version. Supply alone to compare against the patch immediately before to.
limitdefault 100.
cursoroffset into the result list, default 0.

Response200 OK. JSON shape mirrors the /diff endpoints (added, removed, changed arrays grouped by type); the markdown variant is a human-readable patch-notes document.

Errors

CodeMeaning
400limit < 1, cursor < 0, or from supplied without to.
404Game not found, or one of the named patches missing on the branch.

Imports

There is no HTTP write endpoint for imports. The codex API is read-only. Imports happen as two CLI passes inside the api container:

shell
# 1. Per-game adapter — walks the dump tree, emits the envelope.
bin/console codex:adapt dark-and-darker \
   --dump-dir=/codex/dark-and-darker/patches/0.16.135.8645-2663 \
   --output=/codex/dark-and-darker/patches/0.16.135.8645-2663/import-envelope.json

# 2. Generic importer — lands the envelope.
bin/console codex:import dark-and-darker \
   --patch-version-raw=0.16.135.8645-2663 \
   --source=/codex/dark-and-darker/patches/0.16.135.8645-2663/import-envelope.json

The version-raw flag is the upstream string verbatim — pass exactly what the depot reports (e.g. 0.16.135.8645-2663). The canonical semver (0.16.135+build.8645.2663) is computed by KAT\Codex\Adapter\Version from that string.

The envelope is a JSON document with three top-level arrays — entities, assets, locales. See the extraction pipeline for the full shape. Both passes are idempotent; re-running with the same patch + branch yields added=0 changed=0 removed=0 unchanged=N.

Per-game adapters

Game-specific normalization lives in KAT\Codex\Adapter\<Game> inside the api repo (so types and the existing Importer can be shared). Each adapter is a tree of Type/<Name>.php classes — one per V2 entity type — extending the base Type class with per-type overrides for tags, links, and icon resolution.

The per-patch dump tree (paks + extracted JSON + icons + locales) lives outside the api in the codex monorepo:

text
~/.katforge/codex/                       own git repo
   compose.yaml                          poller + per-game extractor services
   dark-and-darker/                      Unreal Engine pipeline
      Dockerfile
      exporter/                          .NET exporter (CUE4Parse)
      scripts/                           fetch / usmap / decode bash
      patches/<version>/                 per-version output
         source/                         paks
         data/{json,icons,locales}/      extracted assets
         manifest/, logs/                pipeline diagnostics
   wc3/                                  deferred
   gear-goblins/                         in-house build output

The hearth api container bind-mounts ~/.katforge/codex at /codex so codex:adapt can read each game's patches. See the Dark and Darker runbook for operator-side workflow.