Codex
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:
# 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.
| Backend | Where it lives | Used for | Endpoints |
|---|---|---|---|
Postgres codex schema | api.katforge.com Postgres, see Schema | bulk-extracted game data with full patch history | most of this page |
| Content-addressed assets | var/codex-assets/<hash[0..2]>/<hash>.<ext> on disk, indexed by codex.assets | binary blobs (icons, portraits, audio) | /v1/codex/assets/{hash} |
| Games dictionary | api.katforge.com/data/codex/games.yaml | display names, abbreviations, adapter class | GET /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 withcodex.patches(current patch per branch)
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 bytype) +codex.patches(current patch)
Path / Query
| Param | Type | Default | Notes |
|---|---|---|---|
game | string | (required) | DB-backed game slug |
branch | string | main | Branch slug |
Patches
GET /v1/codex/{game}/patches
Patches for a game on a branch, newest first.
| Query | Type | Default | Notes |
|---|---|---|---|
branch | string | main | Branch slug |
- Source:
codex.patchesfiltered by(game, branch), ordered byimported_at DESC
GET /v1/codex/{game}/patches/{version}
Single patch by version (canonical semver).
- Source:
codex.patcheskeyed 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_diffsbetween the twopatch_ids
Response — 200 OK
{
"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 before ↔ after to invert.
Entities
GET /v1/codex/{game}/{type}
Paginated list of entities of one type. Filterable, searchable.
| Query | Type | Default | Notes |
|---|---|---|---|
branch | string | main | Branch slug |
limit | int | 50 | 1..200 |
cursor | int | null | Resolved-row cursor returned as next from a prior page |
q | string | null | Full-text search across name + description |
filter[<path>][:<op>] | string | — | See Filter grammar below |
- Source:
codex.entity_resolved_currentfiltered by(game, branch, type)with optionalq/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.
| Operator | Suffix | Example | Reads as |
|---|---|---|---|
| equals | eq (default) | filter[type]=item | type is item |
| not equals | ne | filter[type:ne]=quest | type is not quest |
| less than | lt | filter[data.gear_score:lt]=50 | gear_score < 50 |
| less or equal | lte | filter[data.gear_score:lte]=50 | gear_score ≤ 50 |
| greater than | gt | filter[data.gear_score:gt]=100 | gear_score > 100 |
| greater or equal | gte | filter[data.gear_score:gte]=100 | gear_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.
| Query | Type | Default | Notes |
|---|---|---|---|
branch | string | main | Branch slug |
at_patch | string | null | Canonical or raw version. When set, returns the entity as it was at that patch |
hydrate | string | null | Comma-separated rels to inline-resolve from links (e.g. primary_property,origin) |
- Source:
codex.entity_resolved_currentkeyed by(game, branch, type, id), enriched withcodex.assetsforicon/assets[].hash. With?at_patch=, falls back to thecodex.entitiesledger viaEntityRepository::atPatch(). With?hydrate=, followscodex.entity_linksto resolve named rels.
Response — 200 OK
{
"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:
{
"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.entitiesledger filtered by(game, branch, type, id)joined withcodex.patches, ordered bypatch_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.
| Query | Type | Notes |
|---|---|---|
from | string | (required) version |
to | string | (required) version |
branch | string | default main |
- Source:
codex.entity_diffsfiltered 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_diffsfiltered to(from_patch_id, to_patch_id, type, id); falls back to a live diff overcodex.entitiesif 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.assetsrow keyed byhash(for media_type / bytes), backed by the file atvar/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
| Field | Notes |
|---|---|
branch | default main. |
format | json (default) or markdown / md — returns rendered markdown with Content-Type: text/markdown. |
type | filter to a single entity type (e.g. item, monster). |
from | earlier patch version. With both from and to, the changelog covers the half-open range (from, to]. |
to | later patch version. Supply alone to compare against the patch immediately before to. |
limit | default 100. |
cursor | offset into the result list, default 0. |
Response — 200 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
| Code | Meaning |
|---|---|
400 | limit < 1, cursor < 0, or from supplied without to. |
404 | Game 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:
# 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:
~/.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.