Colors API

Colors API

WCAG contrast ratios, design-token shades & tints, harmonic palettes (complementary, analogous, triadic), and dominant-color extraction — over one clean REST contract.

Three endpoints for color work: contrast for accessibility audits, shades for design tokens and harmonic palettes, and extract for pulling dominant colors out of an image. Math endpoints run pure-PHP and are byte-identical with our web tools; extraction uses a Python pipeline so it shares the heavy throttle of the Images API.

# Check black-on-white contrast
curl "https://jinero.online/api/v1/colors/contrast?fg=%23000&bg=%23fff"

# Generate shades for a brand color
curl "https://jinero.online/api/v1/colors/shades?hex=%233b82f6&step=10"
const r = await fetch("https://jinero.online/api/v1/colors/contrast?fg=%23000&bg=%23fff");
const { ratio, aa, aaa } = await r.json();

All endpoints are public — no key required. Limits per IP:

# Anonymous limits (per IP):
#   GET  /colors/contrast    60 req/min
#   GET  /colors/shades      60 req/min
#   POST /colors/extract      5 req/min  (Python pipeline)

# Max image upload: 10 MB
# Max image_url fetch: 10 MB (server-side, image/* only)

Compute the WCAG 2.1 relative-luminance contrast ratio between two colors and return AA/AAA pass flags for both normal and large text. Accepts #rgb, #rrggbb, rgb(...) or hsl(...) input.

GET https://jinero.online/api/v1/colors/contrast
ParameterTypeDescription
fg string Foreground color. Hex, rgb(), or hsl().
bg string Background color. Same formats as fg.
curl "https://jinero.online/api/v1/colors/contrast?fg=%23000000&bg=%23ffffff"

# Equivalent with rgb()
curl "https://jinero.online/api/v1/colors/contrast?fg=rgb(0,0,0)&bg=rgb(255,255,255)"
const url = new URL("https://jinero.online/api/v1/colors/contrast");
url.searchParams.set("fg", "#1d4ed8");
url.searchParams.set("bg", "#ffffff");

const r = await fetch(url);
const data = await r.json();
if (!data.aa.normal) console.warn(`Ratio ${data.ratio_string} fails WCAG AA`);
$r = Http::get("https://jinero.online/api/v1/colors/contrast", [
    "fg" => "#1d4ed8",
    "bg" => "#ffffff",
]);
$ratio = $r->json("ratio");
Example response
{
  "success": true,
  "foreground": "#000000",
  "background": "#ffffff",
  "ratio": 21,
  "ratio_string": "21.00:1",
  "aa":  { "normal": true, "large": true },
  "aaa": { "normal": true, "large": true },
  "thresholds": {
    "aa_normal": 4.5,
    "aa_large":  3,
    "aaa_normal": 7,
    "aaa_large": 4.5
  }
}

Generate a complete design token set from one base color: tints (lighter), shades (darker), and five harmonic relationships (complementary, analogous ±30°, triadic ±120°, split-complementary, tetradic). Each color comes with hex/rgb/hsl strings, relative luminance, and recommended text color.

GET https://jinero.online/api/v1/colors/shades
ParameterTypeDescription
hex string Base color. Hex/rgb()/hsl().
step integer Step size in percent: 5, 10 (default), 20, or 25.
limit integer Cap on how many tints/shades to emit. Default fills up to 100% in step increments (typically 9 for step=10).
curl "https://jinero.online/api/v1/colors/shades?hex=%233b82f6&step=20"
const r = await fetch("https://jinero.online/api/v1/colors/shades?hex=%231d4ed8&step=10");
const { base, tints, shades, harmonies } = await r.json();

// Drop into a design token map
const tokens = Object.fromEntries(
  tints.map(t => [`brand-${100 - t.percent}`, t.hex])
);
$res = Http::get("https://jinero.online/api/v1/colors/shades", ["hex" => "#3b82f6", "step" => 20]);
$harmony = $res->json("harmonies.complementary.0.hex");
Example response
{
  "success": true,
  "step": 20,
  "base": {
    "hex": "#3b82f6",
    "rgb": [59, 130, 246],
    "rgb_string": "rgb(59, 130, 246)",
    "hsl": [217, 91, 60],
    "hsl_string": "hsl(217, 91%, 60%)",
    "luminance": 0.2355,
    "is_light": true,
    "text_color": "#000000",
    "contrast_on_white": 3.68,
    "contrast_on_black": 5.71
  },
  "tints":  [ { "percent": 20, "hex": "#629bf8", ... } ],
  "shades": [ { "percent": 20, "hex": "#2f68c5", ... } ],
  "harmonies": {
    "complementary":      [ { "rotation": 180, "hex": "#f6af3b", ... } ],
    "analogous":          [ { "rotation": -30, ... }, { "rotation": 30, ... } ],
    "triadic":            [ { "rotation": -120, ... }, { "rotation": 120, ... } ],
    "split_complementary":[ { "rotation": 150, ... }, { "rotation": 210, ... } ],
    "tetradic":           [ { "rotation": 90, ... }, { "rotation": 180, ... }, { "rotation": 270, ... } ]
  }
}

Pull the top N colors out of any image — either an uploaded file or a public URL. Powered by k-means clustering with three flavour modes: balanced, vibrant (favors saturated hues), and muted (favors low-chroma neutrals).

POST https://jinero.online/api/v1/colors/extract
ParameterTypeDescription
image file (multipart) Image upload — jpg, jpeg, png, webp, gif, bmp. Max 10 MB. Mutually exclusive with image_url.
image_url string Public image URL the server will fetch (capped at 10 MB, must respond image/*).
count integer Number of dominant colors to return. 2–16, default 8.
mode string balanced (default) · vibrant · muted.
# Option 1: file upload
curl -X POST https://jinero.online/api/v1/colors/extract \
  -F "[email protected]" \
  -F "count=6" \
  -F "mode=vibrant"

# Option 2: URL fetch
curl -X POST https://jinero.online/api/v1/colors/extract \
  -H "Content-Type: application/json" \
  -d '{"image_url": "https://example.com/cover.jpg", "count": 6}'
// File upload
const form = new FormData();
form.append("image", file);
form.append("count", "6");
form.append("mode", "vibrant");

const r = await fetch("https://jinero.online/api/v1/colors/extract", { method: "POST", body: form });
const { colors } = await r.json();
// colors: [{ hex, rgb, hsl, percent }]
Example response
{
  "success": true,
  "mode": "vibrant",
  "count": 6,
  "colors": [
    { "hex": "#f9f9f9", "rgb": {"r":249,"g":249,"b":249}, "hsl": {"h":0,"s":0,"l":97.6}, "percent": 90.8 },
    { "hex": "#1d1c1b", "rgb": {"r":29,"g":28,"b":27},    "hsl": {"h":30,"s":3.6,"l":11}, "percent": 5.8 },
    { "hex": "#a1989b", "rgb": {"r":161,"g":152,"b":155}, "hsl": {"h":340,"s":4.6,"l":61.4}, "percent": 1.9 }
  ]
}

Standard HTTP semantics. Errors return JSON with a "message" field.

// 422 — invalid color format
{ "message": "Invalid color format. Accepts #rgb, #rrggbb, rgb(r,g,b), or hsl(h,s%,l%)." }

// 422 — neither image nor image_url provided
{ "message": "Provide one of: image (file) or image_url." }

// 422 — URL fetch failed (404 from remote, non-image content, > 10 MB)
{ "message": "Could not fetch image from URL." }

// 429 — rate limit exceeded (5 req/min on extract)
{ "message": "Too Many Attempts." }

About the jinero.online Colors API

Free REST API for everything color-math: WCAG contrast, shades and tints generation, five harmonic palettes, and k-means dominant-color extraction from any image.

WCAG 2.1 ready

Contrast ratios pass through the same sRGB linearisation Google Lighthouse uses — your accessibility CI will agree with our numbers.

Five harmonies built in

Complementary, analogous, triadic, split-complementary, tetradic — all computed as HSL hue rotations from your base color.

Image → palette

k-means dominant-color extraction with vibrant / muted / balanced flavours. Accepts uploads or public URLs.

Design-token friendly

Each color comes back with hex, rgb, hsl, luminance and recommended text color — wire it straight into a token map.

Frequently Asked Questions

No. Contrast and shades endpoints allow 60 req/min per IP. Color extraction is heavier (Python k-means) so it is capped at 5 req/min. Sign up for an API token for higher limits.

Hex with or without # (#rgb / #rrggbb), rgb(r,g,b), and hsl(h,s%,l%). Mixed-case is fine. Internally we normalize to lowercase 6-char hex before the math runs.

It is the same engine — the web /colors/extract page calls the same Python script (resources/python/color_extract.py). The API just adds a JSON wrapper and lets you pass image_url for server-side fetching.

WCAG 2.1 levels: AA normal text 4.5:1, AA large text 3:1, AAA normal 7:1, AAA large 4.5:1. We return the raw ratio plus boolean flags for all four checks so you can run your own policy.

We rotate hue on the HSL wheel while preserving saturation and lightness. Complementary = +180°, analogous = ±30°, triadic = ±120°, split-complementary = ±150°/210°, tetradic = 90/180/270°. Other tools (LCH-based, OKLCh-based) may produce different colors with the same names.