OKLCH Studio · Developers
Contrast checker API: WebAIM-compatible JSON with WCAG and APCA
Updated 2026-10-18
The contrast API is a single read-only endpoint, GET https://colorise.online/api/contrast, that takes two hex colours and returns the WCAG 2.x contrast ratio, the pass or fail verdicts for AA and AAA, and extra data the other checkers do not give you: the APCA Lc value, and a suggested replacement text colour that reaches AA while keeping the hue.
It uses the same parameter names and the same first five response fields as the WebAIM contrast checker's JSON mode, so code written for that API works here by changing the host. This page documents every parameter and field, shows real responses generated by the endpoint's own code at build time, and says plainly what the service does not promise.
Endpoint and quick start
Send a GET request with the text colour in fcolor and the background colour in bcolor. Colours are hex, three or six digits, with or without a leading #. A literal # must be percent-encoded as %23 in a URL, which is why most callers simply leave it out.
Smallest possible request
curl "https://colorise.online/api/contrast?fcolor=767676&bcolor=ffffff"The response is JSON with the content type application/json; charset=utf-8. The first five keys are WebAIM's, in WebAIM's order. After them comes one extra key, colorise, which carries everything WebAIM does not.
The endpoint answers browsers directly: it sends Access-Control-Allow-Origin: * and handles the OPTIONS preflight, so you can call it from client-side JavaScript on any origin. It is marked noindex in the X-Robots-Tag header, because this documentation page, not the raw JSON, is the thing meant to be found.
Parameters
| Parameter | Required | Meaning |
|---|---|---|
| fcolor | Yes | Foreground (text) colour, hex. WebAIM's name. |
| bcolor | Yes | Background colour, hex. WebAIM's name. |
| fg, bg | Alias | Same as fcolor and bcolor. They match the permalinks of the contrast checker page. If both spellings are sent, fcolor and bcolor win. |
| api | No | Ignored. WebAIM requires it to switch to JSON; here it is accepted so URLs can be copied unchanged. |
Hex is parsed case-insensitively. A three-digit colour such as fa0 is expanded to ffaa00. Anything else, including named colours, rgb() and oklch() strings, is rejected with a 400 response. If you need to convert those first, the MCP tool convert_color and the converter pages do it.
Missing or malformed colours return HTTP 400 with a JSON body that has an error code (missing_color or invalid_color), a human-readable message, the values you sent under received, and a usage line.
Response fields
| Field | Type | Meaning |
|---|---|---|
| ratio | string | WCAG 2.x contrast ratio floored to two decimals with trailing zeros removed: "21", "4.54", "4.5". |
| AA | "pass" | "fail" | Normal text, ratio of at least 4.5:1. |
| AALarge | "pass" | "fail" | Large text, ratio of at least 3:1. |
| AAA | "pass" | "fail" | Normal text, ratio of at least 7:1. |
| AAALarge | "pass" | "fail" | Large text, ratio of at least 4.5:1. |
| Field | Type | Meaning |
|---|---|---|
| fg, bg | string | The normalised colours as #RRGGBB. |
| ratio | number | The raw ratio rounded to four decimals, for arithmetic. |
| wcag.normal, wcag.large | "AAA" | "AA" | "Fail" | The WCAG level reached for normal and large text. |
| wcag.ui | "pass" | "fail" | Non-text contrast (success criterion 1.4.11), ratio of at least 3:1. |
| apca.lc | number | Signed APCA Lc to one decimal. Positive means dark text on a light background; negative means light text on dark. |
| apca.absLc | number | The magnitude of Lc. |
| apca.body, apca.large | "pass" | "fail" | Whether the magnitude reaches 75 (body text) and 45 (large text or UI). |
| fix.target | "AA" | Always AA for normal text. |
| fix.fg_after | string | The text colour after the lightness-only fix. Equal to the input when nothing needed changing. |
| fix.ratio_after, fix.oklch_after | number, string | Ratio of the fixed pair and the fixed colour as an oklch() string. |
| fix.changed, fix.achieved | boolean | Whether a different colour was suggested, and whether the result reaches AA. |
| studio_url | string | The pair opened in the interactive contrast checker. |
| fix_page_url | string | null | A static page for the pair, only when it is in the curated set of indexed pairs. |
| docs | string | A link to this page. |
Example responses
These responses were produced by calling the endpoint's own code while this page was built, so they cannot disagree with the live service. The first two pairs sit on either side of the 4.5:1 line; the third is a common button.
#767676 on #ffffff (the darkest grey that passes AA on white):
GET /api/contrast?fcolor=767676&bcolor=ffffff
{
"ratio": "4.54",
"AA": "pass",
"AALarge": "pass",
"AAA": "fail",
"AAALarge": "pass",
"colorise": {
"fg": "#767676",
"bg": "#FFFFFF",
"ratio": 4.5422,
"wcag": {
"normal": "AA",
"large": "AAA",
"ui": "pass"
},
"apca": {
"lc": 71.6,
"absLc": 71.6,
"body": "fail",
"large": "pass"
},
"fix": {
"target": "AA",
"fg_after": "#767676",
"ratio_after": 4.5422,
"oklch_after": "oklch(0.566 0.000 0.0)",
"changed": false,
"achieved": true
},
"studio_url": "https://colorise.online/en/contrast?fg=767676&bg=ffffff",
"fix_page_url": null,
"docs": "https://colorise.online/en/developers/contrast-api"
}
}#777777 on #ffffff (one step lighter, which fails):
GET /api/contrast?fcolor=777777&bcolor=ffffff
{
"ratio": "4.47",
"AA": "fail",
"AALarge": "pass",
"AAA": "fail",
"AAALarge": "fail",
"colorise": {
"fg": "#777777",
"bg": "#FFFFFF",
"ratio": 4.4781,
"wcag": {
"normal": "Fail",
"large": "AA",
"ui": "pass"
},
"apca": {
"lc": 71.1,
"absLc": 71.1,
"body": "fail",
"large": "pass"
},
"fix": {
"target": "AA",
"fg_after": "#6B6B6B",
"ratio_after": 5.3292,
"oklch_after": "oklch(0.529 0.000 0.0)",
"changed": true,
"achieved": true
},
"studio_url": "https://colorise.online/en/contrast?fg=777777&bg=ffffff",
"fix_page_url": null,
"docs": "https://colorise.online/en/developers/contrast-api"
}
}#ffffff on #2563eb (white text on a blue button):
GET /api/contrast?fcolor=ffffff&bcolor=2563eb
{
"ratio": "5.16",
"AA": "pass",
"AALarge": "pass",
"AAA": "fail",
"AAALarge": "pass",
"colorise": {
"fg": "#FFFFFF",
"bg": "#2563EB",
"ratio": 5.1688,
"wcag": {
"normal": "AA",
"large": "AAA",
"ui": "pass"
},
"apca": {
"lc": -80.2,
"absLc": 80.2,
"body": "pass",
"large": "pass"
},
"fix": {
"target": "AA",
"fg_after": "#FFFFFF",
"ratio_after": 5.1688,
"oklch_after": "oklch(1.000 0.000 0.0)",
"changed": false,
"achieved": true
},
"studio_url": "https://colorise.online/en/contrast?fg=ffffff&bg=2563eb",
"fix_page_url": null,
"docs": "https://colorise.online/en/developers/contrast-api"
}
}Look at the second response. The ratio is 4.47, so AA is fail, and colorise.fix.fg_after is a slightly darker grey of the same hue that reaches AA. That is the reason the extra block exists: a checker that only says fail leaves you to guess a replacement.
How the ratio and the verdicts are computed
The pass and fail verdicts are computed on the unrounded ratio, never on the string you see. The ratio string is floored to two decimals rather than rounded, which is what WebAIM does (it reports 4.47 for #777 on white, whose raw ratio is 4.4781) and what the rest of this site does, so a failing pair is never displayed as 4.50.
- A pair at 4.4999:1 returns
ratioof4.49andAAoffail. - A pair at exactly 4.5:1 returns
ratioof4.5andAAofpass. colorise.ratiokeeps four decimals if you want to do your own comparison.
The ratio itself follows the WCAG 2.x definition, the lighter relative luminance plus 0.05 over the darker one plus 0.05. The WCAG vs APCA guide shows the formula and the thresholds.
Code examples
curl
curl -s "https://colorise.online/api/contrast?fcolor=ffffff&bcolor=2563eb"JavaScript (fetch)
const params = new URLSearchParams({ fcolor: "767676", bcolor: "ffffff" });
const res = await fetch("https://colorise.online/api/contrast?" + params);
if (!res.ok) throw new Error((await res.json()).message);
const { ratio, AA, colorise } = await res.json();
console.log(ratio, AA); // "4.54" "pass"
console.log(colorise.apca.lc); // 71.6
console.log(colorise.fix.fg_after); // unchanged when AA already passesPython (requests)
import requests
r = requests.get(
"https://colorise.online/api/contrast",
params={"fcolor": "777777", "bcolor": "ffffff"},
timeout=10,
)
r.raise_for_status()
data = r.json()
print(data["ratio"], data["AA"]) # 4.47 fail
print(data["colorise"]["fix"]["fg_after"]) # a suggested replacementGitHub Actions: fail the build when a pair is below 4.5:1
name: contrast
on: [pull_request]
jobs:
contrast:
runs-on: ubuntu-latest
steps:
- name: Check brand colour pairs
run: |
fail=0
for pair in "1f2937:ffffff" "767676:ffffff" "ffffff:2563eb"; do
fg="${pair%%:*}"; bg="${pair##*:}"
if curl -fsS -G "https://colorise.online/api/contrast" \
--data-urlencode "fcolor=$fg" --data-urlencode "bcolor=$bg" \
| jq -e '.AA == "pass"' > /dev/null; then
echo "ok $fg on $bg"
else
echo "FAIL $fg on $bg is below 4.5:1"; fail=1
fi
done
exit $failjq -e exits non-zero when the expression is false, and curl -f makes HTTP errors fail the step, so a bad colour in your list also stops the build rather than passing silently. Because responses are cacheable, rerunning the job on the same pairs is cheap.
Migrating from WebAIM's API
WebAIM's contrast checker returns JSON when you add &api to a URL such as https://webaim.org/resources/contrastchecker/?fcolor=767676&bcolor=ffffff&api. To move over, change the host and path to /api/contrast and keep the rest.
- Same parameters:
fcolorandbcolor, hex without the#. A trailing&apiis accepted and ignored. - Same fields:
ratioas a string, andAA,AALarge,AAA,AAALargeaspassorfail. The first five keys match the three pairs we compared against WebAIM's live output: black on white, #767676 on white and #777777 on white. - Extra data: the
coloriseobject. Parsers that read only the WebAIM keys can ignore it. - CORS: every response allows any origin, so you can call it from a browser without a proxy.
- Caching: responses carry
Cache-Control: public, max-age=86400, s-maxage=86400, because the output depends only on the query string.
Two differences to keep in mind, both checked against WebAIM's live endpoint on 18 October 2026. Invalid input: here it returns HTTP 400 with a JSON error body, while WebAIM answered fcolor=zz with a normal 200 result (ratio 21, as if the colour were black), so a bad value can pass silently there. Short hex: this service expands three-digit hex the CSS way (fff is ffffff), while WebAIM did not (fcolor=FFF&bcolor=000 came back as 5.32 rather than 21), so send six digits if you need identical results from both.
What the APCA numbers mean
APCA scores contrast as a lightness contrast value, Lc, instead of a ratio. The sign carries information: a positive Lc means dark text on a light background, a negative Lc means light text on a dark one, and APCA gives the two directions different magnitudes for the same two colours. Black on white scores about +106 here and white on black about -108.
The body and large verdicts apply the thresholds this site uses everywhere: an absolute Lc of at least 75 for body text and at least 45 for large text and interface elements. They are informational. WCAG 2.2 AA, the ratio fields above, is still the level most laws and audits reference. The WCAG vs APCA guide explains where the two disagree and why.
Limits and fair use
There is no API key, signup or quota to manage, and the service is free. There is also no service-level agreement, no uptime guarantee and no promise that the endpoint will not change. The code does not enforce a rate limit today, but the host can throttle or block traffic that harms other users, and a limit may be added without notice.
- Cache results on your side. The same pair always returns the same answer, so there is rarely a reason to ask twice.
- For bulk work, such as scoring a whole palette, prefer computing locally; the WCAG formula is a few lines, and the guide lists it.
- Do not put the call in a hot loop in production code paths where a failure would break your product. Treat it as a convenience and fail soft.
- The results are informational and not a legal accessibility audit.
The terms of use for the site are on the terms page. If you need guarantees, run the maths yourself or put your own cache in front.
FAQ
- Is there a free contrast checker API?
- Yes.
GET https://colorise.online/api/contrast?fcolor=RRGGBB&bcolor=RRGGBBis free, needs no key, and returns JSON. It has no service-level agreement, so cache the results and do not build anything on it that cannot tolerate downtime. - Is it a drop-in replacement for the WebAIM contrast API?
- For the five documented fields, yes: the parameter names, field names, order and value format match WebAIM's JSON mode for the pairs we compared. The response also contains an extra
coloriseobject, so strict parsers that reject unknown keys should read only the first five. - Why is the ratio 4.47 and not 4.48?
- The ratio is floored to two decimals, not rounded. #777777 on white is 4.4781:1, which WebAIM and this API both show as 4.47. Flooring means a failing pair is never displayed as passing, and the pass or fail verdicts use the unrounded value.
- Can I call it from browser JavaScript?
- Yes. Every response, including errors, sends
Access-Control-Allow-Origin: *, and the endpoint answersOPTIONSpreflight requests. - Does it support APCA?
- Yes, in the
colorise.apcaobject: signed Lc, its magnitude, and pass or fail at 75 for body text and 45 for large text. Positive Lc is dark text on a light background. - Does it accept rgb(), hsl() or oklch() colours?
- No. Only three- and six-digit hex are accepted. Convert other formats first, for example with the converter pages or the MCP convert_color tool.
Try it
Prefer a visual tool? The same maths, with a live Fix preview, is in the interactive checker.