Skip to content
xivdyetools.app ↗

Harmony ​

4 endpoints · 5 colour wheels · 10 harmony types

The Harmony Explorer, the Discord bot's /harmony and the share-link cards all pick their dyes with one selector, and since core 5.2.0 that selector measures its angles on a colour wheel you choose. These endpoints expose the wheels themselves and run the same selector for any base colour.

The five wheels ​

idTagWhat it is
rgbRGBThe screen wheel — and also the CMY print wheel, same circle, different names. The default, and what an absent wheel means everywhere in the suite.
rybRYBThe painter's wheel the harmony rules were written for. Red's complement is green.
munsellMUNSELLThe evenly spaced perceptual hue circle behind Japan's JIS colour standard. Red's complement is blue-green.
oklch-hueOKLCH·HThe screen wheel re-spaced so equal angles are equal perceived hue steps. Keeps the base's vividness and brightness.
oklch-lightnessOKLCH·LRotates hue at constant perceived lightness and colourfulness. Partners match the base's brightness; palettes lean toward mid-tones.

The wheel is material, not cosmetic: across every dye × harmony slot the suite measured, choosing the RYB wheel changes the chosen partner dye 45% of the time (mean ΔE2000 of 15 between the two picks), and the constant-lightness OKLCH wheel changes it 62% of the time. Ids are the wire format everywhere — share URLs, the bot option, this API — and never change; the localized name does.

Munsell

MUNSELL is a registered trademark of Amazys Holding GmbH; this wheel is computed from published renotation data and is not affiliated with or endorsed by X-Rite or Pantone.

GET /v1/wheels ​

Every wheel, in the suite's display order (rgb first), with its short tag and localized name.

GET/v1/wheelsThe five colour wheels harmony angles can be measured on.…

The five colour wheels harmony angles can be measured on.

Parameters · query
en · ja · de · fr · ko · zh — localizes name
GET /v1/wheels?locale=en
STATUS · TIME · X-REQUEST-ID — after Send
// tap Send — nothing is fetched until you ask

GET /v1/wheels/:id ​

One wheel: its ring paint (ringStops, evenly spaced angles around the wheel as in-gamut hex colours — draw them as a conic gradient) and where every dye sits on it (dyes[].wheelHue, 0–360). Two dyes 180° apart on this list are complements on this wheel; the same pair on another wheel usually is not.

GET/v1/wheels/:idOne wheel: its ring paint and where every dye sits on it.…

One wheel: its ring paint and where every dye sits on it.

Parameters · path
Wheel id
Parameters · query
How many ring colours to return, evenly spaced (3–360)
en · ja · de · fr · ko · zh — adds localizedName to each dye
GET /v1/wheels/ryb?stops=72&locale=en
STATUS · TIME · X-REQUEST-ID — after Send
// tap Send — nothing is fetched until you ask

The response carries the wheel's own fields (id, tag, name, isDefault) alongside ringStops and dyes.

GET /v1/harmony/types ​

The ten harmony types and the hue offsets each one measures from the base, in degrees. A type is a row in this table, nothing more — the same offsets apply on every wheel; the wheel decides what an angle means.

GET/v1/harmony/typesThe ten harmony types and their hue offsets.…

The ten harmony types and their hue offsets.

Parameters · query
en · ja · de · fr · ko · zh — localizes name
GET /v1/harmony/types?locale=en
STATUS · TIME · X-REQUEST-ID — after Send
// tap Send — nothing is fetched until you ask
json
{
  "success": true,
  "data": [
    { "id": "complementary", "offsets": [180], "name": "Complementary" },
    { "id": "analogous", "offsets": [30, 330], "name": "Analogous" },
    { "id": "triadic", "offsets": [120, 240], "name": "Triadic" },
    { "id": "split-complementary", "offsets": [150, 210], "name": "Split-Complementary" },
    ...
  ],
  "meta": { ... }
}

GET /v1/harmony ​

Choose a dye for every slot of a harmony. The base is a dye (dye= — itemID or stainID, auto-detected like /v1/dyes/:id) or any colour (hex=). For each offset of the harmony type the selector finds the ideal colour on the chosen wheel, then ranks every candidate dye against it: by the chosen ΔE method when strict (the default), or by plain hue distance in degrees when strict=false. The base dye is never chosen for a slot; the dye filters and excludeIds narrow the pool exactly as on /v1/dyes.

GET/v1/harmonyA dye for every slot of a harmony, on the wheel you choose.…

A dye for every slot of a harmony, on the wheel you choose.

Parameters · query
Base dye — itemID or stainID, auto-detected. Give dye or hex, not both
Base colour — #RRGGBB or RRGGBB — when the base is not a dye
Harmony type
Colour wheel the offsets are measured on
ΔE method the strict ranking uses
Rank by ΔE against the ideal colour (true) or by hue angle alone (false)
Runner-up dyes to return per slot (0–5)
Never choose one dye for two slots
Comma-separated IDs to keep out of every slot (max 50; itemID or stainID)
en · ja · de · fr · ko · zh
Only / never metallic dyes
Only / never pastel dyes
Only / never dark dyes
Only / never Cosmic Exploration dyes
Only / never Ishgardian dyes
Only / never vendor-acquired dyes
Only / never crafted dyes
Only / never premium-cost dyes
GET /v1/harmony?dye=13&type=complementary&wheel=rgb&method=ciede2000&strict=true&companions=0&preventDuplicates=false&locale=en
STATUS · TIME · X-REQUEST-ID — after Send
// tap Send — nothing is fetched until you ask
json
{
  "success": true,
  "data": {
    "base": { "hex": "#cc6c5e", "dye": { "stainID": 13, "name": "Coral Pink", ... } },
    "harmonyType": "triadic",
    "harmonyTypeName": "Triadic",
    "wheel": { "id": "ryb", "tag": "RYB", "name": "RYB (artist's)", "isDefault": false },
    "method": "ciede2000",
    "strict": true,
    "distanceUnit": "ciede2000",
    "baseWheelHue": 14.271,
    "slots": [
      { "index": 0, "offset": 120, "wheelHue": 134.271, "targetHue": 97.55, "targetHex": "#8ECC5E", "dye": { "name": "Moss Green", ... }, "distance": 6.1204, "companions": [] },
      { "index": 1, "offset": 240, "wheelHue": 254.271, "targetHue": 236.8, "targetHex": "#5E6ACC", "dye": { "name": "Lavender Purple", ... }, "distance": 8.4415, "companions": [] }
    ]
  },
  "meta": { ... }
}

Read a slot as: the ideal is targetHex, sitting at wheelHue on the wheel; the nearest allowed dye is dye, distance away. distanceUnit names the unit — the method when strict, degrees otherwise — so a client never compares a ΔE to an angle. A slot with no surviving candidate (filters that exclude every dye) comes back with dye: null and distance: null rather than an error.

Everything here is deterministic and edge-cached like the dye routes: one wheel × harmony × base is one cache entry for a day.

FINAL FANTASY XIV © 2010-2026 SQUARE ENIX CO., LTD. All Rights Reserved.
XIV Dye Tools is a fan-made application and is not affiliated with or endorsed by Square Enix. · Discord ↗