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.
https://jinero.online/api/v1/colors/contrast
| Parameter | Type | Description |
|---|---|---|
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");
{
"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.
https://jinero.online/api/v1/colors/shades
| Parameter | Type | Description |
|---|---|---|
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");
{
"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).
https://jinero.online/api/v1/colors/extract
| Parameter | Type | Description |
|---|---|---|
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 }]
{
"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.