Catalog spec v1
The JSON format for templates: items, groups, tiers, branding and credits.
A catalog (also called a template) is the list of items people rank on tiero. Data providers publish their own
catalogs and keep them in sync; every tier list made from a catalog points back at it. The machine-readable source
of truth is the @tiero/spec npm package: its zod schemas, the generated JSON Schema files and validateCatalogSpec. When this page and the package disagree, the package wins.
Two formats
| Format | spec | Used by | Images |
|---|---|---|---|
| Catalog spec | tiero-catalog/1 | the body of PUT /api/v1/catalogs/{slug} | uploaded media ids only (med_...) |
| Catalog source | tiero-catalog-source/1 | the tiero CLI and partner exporters | a media id, a local file or an HTTPS URL |
The source format lets you keep a catalog in a repository next to its images. The CLI uploads each {"file": ...} or {"url": ...} image, swaps in the returned media id and sends a
catalog spec. Apart from the spec value and the image references, the formats are identical.
Example
{
"spec": "tiero-catalog/1",
"slug": "nba-2k27-badges",
"title": "NBA 2K27 Badges",
"description": "Every badge in NBA 2K27.",
"game": "nba-2k27",
"tags": ["nba-2k27", "badges"],
"visibility": "public",
"showNames": true,
"license": { "note": "Game art (c) 2K", "url": "https://example.com/license" },
"branding": {
"name": "lockercodes.io",
"url": "https://lockercodes.io/nba-2k/tier-lists/badges",
"logo": "med_abcdefghijklmnop"
},
"defaultTiers": [
{ "id": "s", "label": "S", "color": "#e11d48" },
{ "id": "a", "label": "A", "color": "#f97316" }
],
"groups": [{ "id": "finishing", "label": "Finishing" }],
"items": [
{
"id": "posterizer",
"name": "Posterizer",
"image": "med_234567abcdefghij",
"group": "finishing",
"color": "#22c55e",
"sourceUrl": "https://example.com/badges/posterizer",
"credit": { "name": "Photo: Jane Doe", "url": "https://example.com/jane" },
"attributes": { "category": "Finishing", "tier": 3, "hallOfFame": true }
}
]
}The smallest valid catalog needs only spec, slug, title and one item with
an id and a name.
Catalog fields
Text lengths count characters after trimming, where the field is trimmed.
| Field | Required | Rule | Default |
|---|---|---|---|
spec | yes | tiero-catalog/1 (source: tiero-catalog-source/1) | |
slug | yes | 3 to 64 chars, lowercase letters, digits and hyphens, starting with a letter or digit; unique per owner | |
title | yes | 1 to 100 chars, trimmed, no URLs | |
description | no | 0 to 500 chars | "" |
game | no | 1 to 40 chars of a-z, 0-9 and hyphens | none |
tags | no | 0 to 8 tags, each 1 to 32 chars of a-z, 0-9 and hyphens, no repeats | [] |
visibility | no | public, unlisted or private (the visibility you request) | public |
showNames | no | boolean: show item names under images | true |
license | no | {note?, url?} with at least one key; note up to 200 chars, url HTTPS up to 300 chars | none |
branding | no | {name, url, logo?}: name 1 to 60 chars, url HTTPS up to 300 chars, logo an image reference | none |
defaultTiers | no | 2 to 12 tiers, unique ids | six tiers, below |
groups | no | 0 to 40 groups, unique ids | [] |
items | yes | 1 to 2,000 items, unique ids |
The whole request body may be at most 1 MB (1,048,576 bytes). The API checks the size before it parses the JSON.
Tiers and groups
| Field | Rule |
|---|---|
| Tier id | 1 to 64 chars of a-z, 0-9 and hyphens, starting with a letter or digit |
| Tier label | 1 to 40 chars, trimmed |
| Tier color | hex color, #rgb or #rrggbb |
| Group id | 1 to 64 chars of a-z, 0-9 and hyphens, starting with a letter or digit |
| Group label | 1 to 60 chars, trimmed |
When defaultTiers is left out, the catalog uses S-Tier #e11d48, A-Tier #f97316, B-Tier #eab308, C-Tier #22c55e, D-Tier #3b82f6 and
F-Tier #a855f7 (ids s, a, b, c, d, f).
Item fields
| Field | Required | Rule |
|---|---|---|
id | yes | 1 to 80 chars of a-z, 0-9, colon, period, underscore and hyphen, starting with a letter or digit; not starting with tier:; unique in the catalog |
name | yes | 1 to 80 chars, trimmed |
image | no | an image reference (see below) |
group | no | the id of a group defined in groups |
color | no | hex color, #rgb or #rrggbb |
sourceUrl | no | HTTPS URL up to 300 chars, where the item comes from |
credit | no | {name, url?}: name 1 to 80 chars, url HTTPS up to 300 chars; needs image |
attributes | no | up to 12 keys of 1 to 32 chars (a letter, then letters, digits or underscores); values are a string up to 80 chars, a finite number or a boolean |
Item ids are stable keys. Tier lists store item ids, so keep an id the same when you rename an item. Ids that
start with tier: are reserved for the builder. credit credits the item's image (for
example a photographer), so it is only accepted on an item that has an image.
Image references
| Where | Allowed |
|---|---|
| Catalog spec | a media id: med_ plus 16 characters from a-z and 2-7, for media
you uploaded |
| Catalog source | a media id, {"file": "<path>"} or {"url": "<https url>"} |
A source file path is relative to the source file: 1 to 300 characters, no leading /, no .. segment, no backslash and no :. A source url must be HTTPS and at most 2,048
characters. Each reference object has exactly one key.
Validation
Every object is strict: a field that is not listed here is an error, not ignored, so a typo such as showname cannot silently drop data.
import { validateCatalogSpec } from '@tiero/spec';
const result = validateCatalogSpec(JSON.parse(text));
if (!result.ok) for (const issue of result.issues) console.log(issue.pointer, issue.message);validateCatalogSpec and validateCatalogSource never throw. They return { ok: true, value } with every default filled in, or a list of issues:
{
"ok": false,
"issues": [
{ "pointer": "/items/1/id", "code": "duplicate_id", "message": "Duplicate id" },
{ "pointer": "/items/0/group", "code": "unknown_group", "message": "Unknown group" }
],
"truncated": true
}pointeris a JSON Pointer (RFC 6901) to the field;""is the whole document.- Issues are sorted by pointer and capped at 100.
truncated: truemeans there were more. codeis a zod issue code (such astoo_bigorunrecognized_keys) or a catalog rule:
| Code | Meaning |
|---|---|
duplicate_id | a tier, group or item id is used twice; the pointer is the second use |
unknown_group | an item names a group that groups does not define |
credit_requires_image | an item has credit but no image |
url_in_title | the title contains a URL |
reserved_item_id | an item id starts with tier: |
The cross-item rules (duplicate_id, unknown_group, credit_requires_image)
are skipped while the document has a structural error. Fix those first, then validate again. The API answers an
invalid catalog with 422 validation_failed and the same issues in error.details.issues.
JSON Schema
@tiero/spec ships generated JSON Schema files for editors and other languages:
@tiero/spec/schema/catalog-spec-v1.json@tiero/spec/schema/catalog-source-v1.json
They check shape, patterns and lengths only. The cross-item rules, the tier: reservation, the
12-attribute cap, unique tags, the license rule, the no-URL title rule, trimming and the HTTPS and file path
checks are not in them. validateCatalogSpec is the authoritative check.
Publishing and updates
PUT /api/v1/catalogs/{slug}creates the catalog or replaces it as a whole.- Add
If-None-Match: *to only create: a taken slug answers412 catalog_exists. - tiero hashes the canonical JSON. An unchanged catalog returns
changed: falseand keeps its version. - An item you leave out of a later PUT becomes a tombstone, and lists that ranked it show it as removed. Sending the same id again brings it back.
- Error codes are listed in
@tiero/spec(ERROR_CODES). They are only ever added.