The Gradient Library API
Overview
The gradient library holds 8,192 generated ramps, and every one of them carries a measured verdict on whether it bands. This guide is how to drive the collection from your own code — getting a key, the four endpoints, and three complete tasks worth doing with them.
One number is worth having before you start. 77.4% of the collection is smooth, 18.6% carries a visible step risk and 4.0% compresses somewhere. Roughly one gradient in five steps visibly at some point along its length, and that is the one thing a thumbnail cannot show you — which is why every response here carries the verdict rather than leaving you to squint at a swatch.
1. What this API is for
+
POST /v1/gradient/generate makes you a gradient. That is a different
job from the one these four endpoints do, which is to answer questions about
gradients that already exist — ours, or yours.
- Show me ramps that do not band —
/v1/gradient/library - Give me one of them whole, with its metrics —
/v1/gradient/library/{id} - What is in the collection, counted? —
/v1/gradient/library/stats - Will my gradient band, and where? —
/v1/gradient/analyse
The last one is the reason the other three are worth having. A collection of
8,192 measured ramps is not primarily a shop; it is the calibration set that
made the thresholds in /v1/gradient/analyse mean something. The
library page runs the identical measurement in
your browser, so the page and the API cannot disagree about a ramp.
2. Getting a key and making your first call
+
Create a key in your dashboard under API keys, then
send it as a bearer token. Every endpoint on this page is versioned under
/v1.
curl -H "Authorization: Bearer $AURIC_API_KEY" \
"https://api.auricartisan.com/v1/gradient/library?limit=3"
Three of the four are GETs with no body, so the quickest way to see
a real response is to paste one into a browser tab with your key on it. The
fourth, /v1/gradient/analyse, takes JSON.
3. What the collection is
+
8,192 gradients, generated rather than collected, across twelve schemes
(Aurora, Nebula, Spectral, Terrain, Thermal, Golden, Analogous, Complementary,
Duotone, Monochrome, Prismatic and Chaotic) at roughly 683 each. Each was built
in one of five interpolation spaces — oklch-short,
oklch-long, oklab, lch-short or
linear-rgb — at about 1,638 apiece, under one of five easing
curves.
Because it is finite and enumerated, matched in a browse response is
a real total, not a scan rate: filtering for the smooth ones tells you
there are exactly 6,342 of them, which is 77.42% of everything.
Ids are row numbers, 0 through 8191, and they are stable. The name is derived
from the row — Aurora Simple #00001 is id 0 — so a
name and an id are two spellings of the same reference.
4. Browsing and paging
+GET /v1/gradient/library?banding=smooth&sort=score&order=desc&limit=2
{
"collection": { "total": 8192 },
"filter": { "banding": "smooth", "scheme": null, "complexity": null, "type": null },
"sort": "score",
"order": "desc",
"matched": 6342,
"share_of_collection": 77.42,
"offset": 0,
"limit": 2,
"count": 2,
"next_offset": 2,
"gradients": [
{
"id": 2050,
"name": "Prismatic Detailed #02051",
"type": "linear",
"angle": 350,
"stops": [
{ "pos": 0, "hex": "#A3E817" },
{ "pos": 0.2035, "hex": "#1444C7" }
],
"css": "linear-gradient(350deg, #a3e817 0%, #0049c8 20%, ...)",
"scheme": "Prismatic",
"complexity": "detailed",
"banding": "smooth",
"score": 0.782
}
]
}
stops and css are not the same list
This trips people up, so it is worth being explicit. stops are the
control points, at their real positions, in the gradient’s own
interpolation space. css is a ready-to-paste
linear-gradient() that approximates that curve by resampling it at
even positions in sRGB, because a browser cannot interpolate in OKLCH from the
control points alone.
Paste css when you want the ramp. Read stops when you
want to know what it was built from. They describe the same gradient and they
will not match token for token.
Paging
Pass next_offset back as offset. It is null on the last
page. Nothing here is a cursor over a scan — the catalogue is finite, so an
offset is exactly what it looks like.
GET /v1/gradient/library?banding=smooth&sort=score&order=desc&limit=2&offset=2
Filters
banding—smooth,subtle-compressionorvisible-step-risk. The one to reach for.scheme— one of the twelve, e.g.Aurora.complexity—simple,detailedorextreme.type—linear,radialorconic.min_score— a quality floor, 0–1.min_stops,max_stops— how many control points.sort—id(default),scoreorstops;orderisascordesc.full=true— adds the interpolation space, easing, per-stop OKLCH and the generator metrics to every record in the page.
Leave full off for a grid. A tile needs the stops, the verdict and
the score; the interpolation space and the ΔE metrics are what you read
when you open one.
5. One gradient, whole
+GET /v1/gradient/library/0
{
"gradient": {
"id": 0,
"name": "Aurora Simple #00001",
"type": "linear",
"angle": 0,
"stops": [
{ "pos": 0, "hex": "#723C2D" },
{ "pos": 1, "hex": "#7B3600" }
],
"css": "linear-gradient(0deg, #723c2d 0%, #7b3600 100%)",
"scheme": "Aurora",
"complexity": "simple",
"banding": "smooth",
"score": 0.4045,
"stops_detail": [
{ "pos": 0, "hex": "#723C2D", "oklch": [0.4204, 0.08, 36.4] },
{ "pos": 1, "hex": "#7B3600", "oklch": [0.4194, 0.1115, 49.6] }
],
"method": "aurora",
"interpolation_space": "oklch-short",
"easing": "linear",
"sample_count": 96,
"complexity_score": 0.1005,
"delta_e_mean": 0.1732,
"uniformity": 0.0391,
"seed": 20260514
}
}
seed is the useful one for reproducibility: it pins the gradient to
the generator run that produced it, so a record can be cited and regenerated
rather than merely copied.
6. The collection’s own numbers
+GET /v1/gradient/library/stats
{
"collection": { "total": 8192 },
"mean_score": 0.5109,
"mean_stops": 9.99,
"banding": [
{ "name": "smooth", "count": 6342, "share": 77.42 },
{ "name": "visible-step-risk", "count": 1520, "share": 18.55 },
{ "name": "subtle-compression", "count": 330, "share": 4.03 }
],
"complexity": [ ... ],
"scheme": [ ... ],
"interpolation_space": [ ... ],
"easing": [ ... ]
}
The banding split is the headline, and it is the fact that makes the collection worth having. A generated set is mostly smooth — but nothing like entirely so, and a set that came back 99% smooth would be evidence that the measurement was not measuring anything.
7. Measuring a gradient you already have
+POST /v1/gradient/analyse
Content-Type: application/json
{ "stops": ["#000000", "#ffffff"] }
{
"stops": [ { "pos": 0, "hex": "#000000" }, { "pos": 1, "hex": "#ffffff" } ],
"steps": 96,
"metric": "Euclidean distance in OKLab between consecutive samples",
"sampled_as": "linear between stops in sRGB \u2014 the ramp a browser paints",
"delta_e_mean": 0.010526,
"delta_e_max": 0.093398,
"step_ratio": 8.873,
"uniformity": 0.008802,
"banding": "visible-step-risk",
"worst_transition_at": 0,
"note": "The largest step is at the dark end. sRGB compresses its darkest values, so even a plain black-to-white ramp steps there \u2014 interpolate in OKLCH, or start from a lifted black rather than #000."
}
How the verdict is reached
The ramp is sampled at 96 points (steps, 8–1024), each sample
is converted to OKLab, and the distance between consecutive samples is measured.
step_ratio is the largest of those steps over the
average one. A ratio rather than an absolute is deliberate: it does not
move when you ask for more samples, so a verdict at 32 steps and a verdict at 512
agree.
The thresholds are not invented. Measured across 3,000 gradients this collection had already labelled, that ratio runs a median of 1.55 for the ones the generator calls smooth and 5.09 for the ones it calls a step risk. 3.2 and 5.5 are where those two populations part.
The part you can act on
worst_transition_at is a fraction along the ramp, and it matters more
than the verdict. A gradient can be smooth on average and lurch once; knowing it
lurches at 95% tells you to put a stop near there. The note says the
same thing in a sentence, and it distinguishes the two cases — a step near
zero is sRGB’s dark compression, not a missing stop, and no amount of extra
stops will fix it.
Both stop formats
Pass bare colours and they are spaced evenly; pass {pos, hex} objects
to place them. Two to 64 stops.
{ "stops": [
{ "pos": 0, "hex": "#001a33" },
{ "pos": 0.92, "hex": "#003d66" },
{ "pos": 1, "hex": "#ffcc00" }
] }
→ step_ratio 10.807, banding "visible-step-risk", worst_transition_at 0.947
8. Two verdicts, two curves
+
Feed a stored record’s stops into
/v1/gradient/analyse and you will sometimes get a different verdict
from the one the record carries. That is not a bug in either, and it is worth
understanding before you build anything on top of it.
-
A record’s
bandingdescribes the ramp the generator built, along a curve in its own interpolation space — often OKLCH or LCH. -
/v1/gradient/analysedescribes the ramp a browser paints: linear, in sRGB, between the stops it was given.
How far apart those are depends entirely on how much the stop list constrains the curve. Sampling the collection by stop count, and asking only whether each ramp is smooth:
- 2 stops (1,258 records) — the two agree 100% of the time. There is no room to disagree.
- 3 to 9 stops (5,569 records) — 77.5%. The interpolation space is doing most of the shaping, and the two are describing different curves.
- 10 or more stops (1,365 records) — 94.1%. The stop list pins the curve down, so both measurements see nearly the same ramp.
Those are counted over every record in each group, not a sample — 83.7% across the whole collection. It matters here: 12 generator methods cycle through the records in order, so a fixed stride lands on a particular mix of them and answers differently depending on the stride you picked.
The practical rule: to know how a stored gradient will look on your page,
analyse its css stops, because that is what the browser will actually
paint. The record’s own banding is the generator’s verdict
on the ideal ramp it was aiming at.
9. Three complete tasks
+Check the gradients already in your stylesheet
The most useful thing here is not picking a new gradient; it is finding out which of the ones you already ship will band on a large hero.
const ramps = {
hero: ["#0f2027", "#203a43", "#2c5364"],
cta: ["#000000", "#434343"],
sidebar: ["#001a33", "#003d66", "#ffcc00"],
};
for (const [name, stops] of Object.entries(ramps)) {
const res = await fetch("https://api.auricartisan.com/v1/gradient/analyse", {
method: "POST",
headers: { "content-type": "application/json", authorization: `Bearer ${KEY}` },
body: JSON.stringify({ stops }),
});
const m = await res.json();
if (m.banding !== "smooth") {
console.warn(`${name}: ${m.banding} \u2014 worst at ${Math.round(m.worst_transition_at * 100)}%`);
console.warn(` ${m.note}`);
}
}
Pick a background that will survive being 1200px wide
GET /v1/gradient/library?banding=smooth&min_stops=10&sort=score&order=desc&limit=12
Smooth and many-stopped is the combination that holds up at size, and it is the population where the stored verdict and the painted ramp agree 94% of the time — so the label you filtered on is the label you will get.
Find where to add a stop
const m = await analyse(stops);
if (m.banding !== "smooth" && m.worst_transition_at > 0.06) {
// Not the sRGB dark end \u2014 a genuine gap. Interpolate a stop at the
// midpoint of the worst transition and measure again.
const at = m.worst_transition_at;
const fixed = insertStopAt(stops, at);
console.log(await analyse(fixed)); // step_ratio should fall
}
The > 0.06 guard matters. A worst transition at the very start is
sRGB’s dark compression, and adding a stop there does nothing; the fix is to
lift the black or interpolate in OKLCH.
10. What each call costs
+GET /v1/gradient/library/{id}— 1 credit. A lookup.GET /v1/gradient/library— 3 credits. Filters and sorts the whole catalogue.POST /v1/gradient/analyse— 3 credits. Samples a ramp and converts every sample to OKLab.GET /v1/gradient/library/stats— 10 credits. Walks all 8,192 records and counts five dimensions.
API credits are a separate balance from tool tokens; neither pays for the other. Statistics do not change between calls — cache the stats response rather than fetching it per page render.
11. Errors, limits and pitfalls
+A rejected filter names itself and lists what it would have accepted.
GET /v1/gradient/library?banding=lovely
{
"error": {
"code": "INVALID_INPUT",
"message": "banding: must be one of smooth, subtle-compression, visible-step-risk"
},
"request_id": "6f4204af-38ba-4114-8ba3-ec2aa47858e1"
}
- Ids are 0-based, names are 1-based.
Aurora Simple #00001is id0. An id outside 0–8191 is a validation error, not a 404. limitis 1–100, default 24.stopsis 2–64 on analyse, andstepsis 8–1024.- Do not compare
step_ratioacross different step counts as if it were a ΔE. It is a shape measure;delta_e_maxis the magnitude one. - Do not read
cssandstopsas the same list. See section 4.
12. Endpoint reference
+| Endpoint | Method | Credits | Answers |
|---|---|---|---|
/v1/gradient/library | GET | 3 | Which ramps match this filter, and how much of the collection is that? |
/v1/gradient/library/{id} | GET | 1 | One gradient, with its per-stop OKLCH and generator metrics. |
/v1/gradient/library/stats | GET | 10 | What is in the collection, counted rather than asserted. |
/v1/gradient/analyse | POST | 3 | Will this ramp band, how badly, and where? |
The machine-readable form of all four is in the API reference, which is generated from the same catalogue the server routes from, so it cannot drift from the implementation.
To see all of this working without writing any code, open the gradient library — the browse grid, the verdict on every card and the “will your gradient band?” panel are the same measurement this API exposes.