DarkerDB
Read-only, public, unauthenticated. Two equivalent base URLs:
https://api.katforge.com/v1/realms/darkerdb (canonical, KATforge-versioned)
https://api.darkerdb.com/v2 (bare-host shorthand, identical bytes)
All paths below are written under /v1/realms/darkerdb. The /v2 shadow accepts the same routes with the prefix stripped.
The DarkerDB API is the projection layer over the codex: per-game typed tables that get refreshed after every codex import. Most endpoints follow a single uniform shape; a handful add per-type filters or attribute hydration.
Conventions
- IDs are
id.<type>.<slug>everywhere (e.g.id.item.jester_outfit_4001,id.monster.abomination_common). Slugged from raw upstream ids at projection time. - Enum values in responses are slugs (
"rare","cloth","chest"). Their localized display labels are served via/facets. - Per-document text (name, description, flavor) is resolved inline against
codex.localesat response time. Override locale with?locale=fr|de|ja|…(defaulten). - Icons are resolved against the asset index at response time and emitted as
{ hash, url, media_type, bytes }objects. - Pagination is cursor-based on
id— pass back thenextvalue via?cursor=…. Defaultlimit=50, max200. - Sort is
asc(default) ordescvia?sort=…. - Historical reads are single-id only via
?at_patch=<version>. LIST endpoints rejectat_patchwith400.
Quick start
# Browse rare cloth chest armor at gear score >= 20
curl 'https://api.katforge.com/v1/realms/darkerdb/items?rarity=rare&armor_type=cloth&slot_type=chest&gear_score_min=20&limit=10'
# Get one item with attributes hydrated
curl https://api.katforge.com/v1/realms/darkerdb/items/id.item.jester_outfit_4001
# Fetch the slug→label dictionary for all enum columns
curl https://api.katforge.com/v1/realms/darkerdb/facets
# Time-travel a single item to an older patch
curl 'https://api.katforge.com/v1/realms/darkerdb/item/id.item.jester_outfit_4001?at_patch=0.16.135.8645-2663'
Response envelope
Every LIST endpoint returns the same envelope:
{
"count": 481,
"limit": 50,
"sort": "asc",
"next": "id.item.adventurer_cloak_4001",
"rows": [ { /* row */ }, ... ]
}
Single-row GET returns the row directly at body.
Facets
GET /facets[?locale=en] returns a slug → label map for every enum-typed column across all projection tables, in one payload. Cache it client-side for the session; refetch on locale change.
{
"rarity": { "common": "Common", "rare": "Rare", "epic": "Epic", "legend": "Legendary" },
"armor_type": { "cloth": "Cloth", "leather": "Leather", "plate": "Plate" },
"slot_type": { "chest": "Chest", "head": "Head", ... },
"class_type": { "common": "Common", "sub_boss": "Sub-Boss", "boss": "Boss" },
"grade_type": { ... }
}
Single-row reads (generic)
GET /{type}/{id} current state
GET /{type}/{id}?at_patch=<version> historical (on-demand reprojection)
Works for every projected type. The type segment of the URL must match the type segment of the id — e.g. /item/id.item.foo, not /item/id.monster.foo.
Resource tables
| Endpoint | Rows (current patch) | Notes |
|---|---|---|
/items | ~2,400 | Plus item attribute pivot |
/monsters | ~400 | Plus monster_abilities junction |
/classes | 20 | 10 playable + 10 NPC arena variants |
/skills | ~70 | Active class abilities |
/spells | ~80 | Magic |
/perks | ~130 | Passive class traits |
/emotes | ~70 | Cosmetic emotes |
/religions | ~40 | Deity definitions |
/quests | ~640 | Everything about a quest — both list and single GET inline objectives + rewards |
/merchants | 26 | NPC vendors |
/shops | ~300 | Merchant inventories |
/dungeons | ~80 | Game-mode definitions |
/dungeon_grades | ~60 | Difficulty tiers |
/dungeon_cards | ~10 | Game-mode variants |
/dungeon_layouts | ~270 | Physical map layouts |
/spawners | ~520 | Monster/loot/props placement |
/achievements | ~240 | Wiki-surfaced achievements |
/triumphs | ~10 | Adventure rank progression |
/faustian_bargains | ~150 | Faustian system |
/workshops | 2 | Crafting buildings |
/workshop_levels | ~6 | Upgrade tiers |
/workshop_upgrades | ~50 | Upgrade tracks |
/workshop_services | ~30 | Craft / rebuild / enhance / scrap |
/loot_drop_groups | ~390 | Loot tier 1 (monster → drop set) |
/loot_drops | ~390 | Loot tier 2 (drop list) |
/loot_drop_rates | ~2,300 | Loot tier 3 (rate weights) |
Items
GET /items list with typed filters
GET /items/{id} single with full attribute hydration
Filters
| Name | Op | Description |
|---|---|---|
rarity | eq | poor / common / uncommon / rare / epic / legend / unique / artifact |
item_type | eq | armor / weapon / consume / utility / misc |
slot_type | eq | chest / head / hands / feet / legs / ring / … |
armor_type | eq | cloth / leather / plate (null for non-armor) |
hand_type | eq | one_handed / two_handed / off_hand |
gear_score_min, gear_score_max | range | inclusive |
is_droppable | eq | true / false |
is_tradable | eq | true / false |
archetype | eq | family slug (e.g. id.item.jester_outfit) |
id_like | substring | wildcards optional; underscores are literal |
Single-item response
GET /items/{id} includes primary_attributes and secondary_attributes arrays. Each attribute entry:
{ "attribute": "effect_armor_rating", "min": 58, "max": 58, "enchanted_min": 0, "enchanted_max": 0 }
The attribute field is the snake-cased game property type (mechanically derived from Id_ItemPropertyType_*; new types in future patches arrive without code changes).
Monsters
Filters
| Name | Op | Description |
|---|---|---|
class_type | eq | normal_mobs / sub_boss / boss / … |
grade_type | eq | common / elite / nightmare / … |
hp_min, hp_max | range | filter on max_health |
archetype | eq | family slug |
id_like | substring |
Single-monster response
Includes attributes (str/dex/agi/will/knw/rsc/vig base values), damage_reductions per element, and an abilities array of id.monster_ability.* refs.
Classes
| Name | Op | Description |
|---|---|---|
is_playable | eq | true for the 10 player-selectable classes; false for the 10 Master Arena NPC variants (grand_master_*). |
Each class lists perk_ids and emote_ids (text arrays).
Skills · Spells · Perks · Emotes
Same shape per type. Each row carries name, icon, refs to abilities/effects (text[]), and (where applicable) allowed_classes. Filterable via id_like and archetype only.
Religions
Includes description, subtitle, display_order, offering_cost, offering_lv_xp (the per-level XP requirement array).
Quests
The quest endpoint is consolidated: both list and single-GET inline the resolved objectives and rewards. Quest contents, rewards, and chapters are no longer separate public endpoints — everything you need to render a quest is on the quest response.
| Name | Op | Description |
|---|---|---|
name | substring | matches the localized title (case-insensitive) |
is_repeatable | eq | |
is_daily | eq | |
chapter_id | eq | id.quest.* (prerequisite quest) |
rank_min, rank_max | range | adventure rank gate |
id_like | substring | matches the canonical id |
Response shape (list + single GET are identical per row)
{
"id": "id.quest.alchemist_05",
"title": "Beasts of the Dark Forest",
"description": "...",
"chapter_id": "id.quest.alchemist_04",
"order": 109,
"objectives": [
{
"id": "id.quest_content.fetch_blue_eyeball_03",
"content_type": "fetch",
"content_count": 1,
"target_kind": "item",
"target_archetype": "id.item.blue_eyeballs",
"target_tag": "Id.Item.BlueEyeballs",
"dungeon_tags": []
}
],
"rewards": [
{
"id": "id.reward.quest_alchemist_05",
"entries": [
{ "type": "item", "count": 30, "item_id": "id.item.gold_coins" },
{ "type": "random", "count": 1, "random_reward_id": "id.random_reward.quest_armor_uncommon_01" },
{ "type": "experience", "count": 25 }
]
}
],
"is_repeatable": false,
"is_daily": false
}
Reward entry types
The rewards[*].entries[] array contains one entry per grant. Each entry has type + count, plus a type-specific reference field where applicable:
| Type | Reference field | Example |
|---|---|---|
item | item_id | grants N copies of a specific item |
experience | (none) | XP grant |
adventure_points | (none) | AP grant |
random | random_reward_id | rolls from a random pool |
affinity | merchant_id (optional) | merchant affinity |
item_skin | item_skin_id | cosmetic |
character_skin | character_skin_id | cosmetic |
armor_skin | armor_skin_id | cosmetic |
nameplate_skin | nameplate_skin_id | cosmetic |
action | action_skin_id | cosmetic action |
emote | emote_id | unlocks emote |
stash / none | (none) | placeholder rows |
Type slugs are mechanically derived from EDCRewardType::* (with Exp → experience and Adv → adventure_points for readability). New types in future patches appear automatically.
Objective shape
The objectives[] array contains one entry per Id_QuestContent_* ref the quest carries. Each entry:
{
"id": "id.quest_content.fetch_blue_eyeball_03",
"content_type": "fetch",
"content_count": 1,
"target_kind": "item",
"target_archetype": "id.item.blue_eyeballs",
"target_tag": "Id.Item.BlueEyeballs",
"dungeon_tags": []
}
8 content_type values: fetch, kill, explore, props, escape, hold, use_item, damage. The target_archetype field uses the same shape as the archetype column on /items, /monsters, etc., so you can pivot directly:
# What items satisfy a "fetch Bandage" objective?
curl 'https://api.darkerdb.com/v2/items?archetype=id.item.bandage'
Merchants
| Name | Op | Description |
|---|---|---|
religion_id | eq | filter by deity affinity |
Carries shop_id, religion_id, quest_chapter_ids[].
Shops
Carries stock (jsonb array). Shop documents have many sub-shapes upstream (ItemSkinShop, EmoteShop, etc.); all are unified into this table.
Dungeons
Carries grade_ids[], layout_ids[], card_ids[], adventure_rank_min.
Dungeon grades
| Name | Op | Description |
|---|---|---|
tier | eq | difficulty tier integer |
Dungeon cards
| Name | Op | Description |
|---|---|---|
mode | eq | game mode slug |
party_size_min, party_size_max | range |
Dungeon layouts
Physical map layouts (what used to be called "maps"). Carries floor (1-based for multi-floor dungeons).
Spawners
| Name | Op | Description |
|---|---|---|
spawner_type | eq | monster / treasure / props / empty |
monster_id | eq | filter to spawners of a specific monster |
loot_drop_group_id | eq | filter to spawners pointing at a loot group |
The 516 spawner rows are evenly split between monster (~155), treasure (~225), and props (~135). Each row references at most one of monster_id, loot_drop_group_id, or implicit props via the full payload.
Achievements
| Name | Op | Description |
|---|---|---|
category | eq | derived from id prefix — arena, class, community, coreplay, event, feat, highroller, item, … |
Carries the full game props at payload.
Triumphs
Adventure rank progression. tier + xp_requirement per row.
| Name | Op | Description |
|---|---|---|
tier_min, tier_max | range |
Faustian bargains
| Name | Op | Description |
|---|---|---|
tier | eq |
Each row carries the full bargain definition at payload. Companion docs (FaustianBargainAbility, Effect, Skill, Override) are resolved into the payload at projection time.
Workshops
The buildings themselves. 2 rows.
Workshop levels
Per-workshop tier definitions with tier and xp_requirement.
Workshop upgrades
Upgrade requirements. 54 rows.
Workshop services
The crafting recipes — four sub-types unified.
| Name | Op | Description |
|---|---|---|
service_type | eq | craft / rebuild / enhance / scrap |
Loot drop groups
Header table that ties monsters/treasures to drop sets. entries[] is a jsonb array of (loot_drop_id, loot_drop_rate_id, count, dungeon_grade) tuples.
Loot drops
The actual drop list. items[] is a jsonb array of {item_id, count, luck_grade} entries.
Loot drop rates
The weight tables. params[] is the (drop_rate, luck_grade) array used when drawing from a loot drop.
Refresh model
After a codex:import run (which hearth codex update dark-and-darker invokes), the projection auto-refreshes:
codex.assetswalked →codex.asset_indexrebuilt- All registered projectors run in dependency order
- Tables become consistent before the import command returns
The pipeline is idempotent — re-running the same patch is a no-op. Re-projection alone (without re-importing codex) is bin/console darkerdb:project inside the api container.
Historical reads
GET /{type}/{id}?at_patch=<version>
Triggers an on-demand reprojection of the single document at the given patch — reads from codex.documents_at(<version>) and runs the projector kernel against that raw payload without touching the live projection tables.
Aggregate historical (?at_patch=<X> on LIST endpoints) is not supported by design — returns 400. The codex retains the raw history for ad-hoc queries via /v1/codex/{game}/documents/{id}?at_patch=….
Conventions on booleans, nulls, and missing data
- All boolean columns use the
is_*prefix (is_playable,is_droppable,is_tradable,is_repeatable,is_daily). - Defaults match the common case (
truefor the permissive flags; explicitfalseonly for the exception rows). - Missing/unknown enum values come back as
null. The wire never carries empty strings for enum-typed columns. - Asset references resolve to
nullwhen no entry exists incodex.asset_indexfor that family. - Locale keys resolve to
nullwhen the key isn't in the locales table for the requested locale and English fallback also misses.