The Palette Library API
Overview
The palette library holds five million palettes of five colours each, and ships as a 660-byte manifest. Nothing is stored: every record is computed from a seed. The expanded form was 3.9 GB.
That size changes what an API can honestly promise. The colour, gradient, harmony and shade collections here hold 8,192 records each, so a filter walks all of them and every total is exact. Nothing walks five million inside a request. So a filtered browse SCANS, and tells you how far it got — which is the single most important thing to understand before you build on it.
1. What this API is for
+
POST /v1/palette/generate makes you a palette. These three let you
search five million that already exist.
- Find palettes matching a description —
/v1/palette/library - Pull one whole, with the seed that reproduces it —
/v1/palette/library/{id} - What does the collection look like? —
/v1/palette/library/stats
The library page runs the same derivation in your browser, so a palette fetched here and one shown there cannot disagree.
2. Getting a key and making your first call
+curl -H "Authorization: Bearer $AURIC_API_KEY" \
"https://api.auricartisan.com/v1/palette/library?limit=3"
An unfiltered browse is cheap and exact: palette n is directly addressable, so asking for three derives three.
3. Five million from 660 bytes
+
palettes.json holds a seed (42), a batch size (50,000), a batch
stride, five colours per palette and seven method names. Palette n
comes from an xorshift generator seeded for its batch, stepped once per
channel, five colours per record.
One property of that is worth knowing because it shapes every endpoint here: deriving palette n means replaying the generator from the start of its batch. Reading a contiguous run is therefore vastly cheaper than reading the same records one at a time — the difference between about a second and about thirty. Ask for ranges.
The derivation block on a single record carries the seed and batch
so anybody can recompute it. A palette id is a claim you can check, not a row
somebody could edit.
4. A filter is a scan, not a query
+There is no index over five million derived records, and building one would mean materialising the 3.9 GB this design exists to avoid. So a filter derives records in order and keeps what passes, up to a budget.
GET /v1/palette/library?carries_body_text=true&limit=5
{
"collection": { "total": 5000000, "derived": true },
"scanned": 7,
"hit_rate": 0.714286,
"cursor": 0,
"next_cursor": 7,
"exhausted": false,
"truncated": false,
"count": 5,
"sorted_within_scan": false,
"palettes": [ ... ]
}
scanned is the honest part. Five results cost seven palettes
derived, and the response says so rather than presenting five as if they were
the first five of five million.
Paging
Pass next_cursor back as cursor. It is a collection
index, so it is stable and shareable — the same cursor resumes at the same
place tomorrow.
5. Truncated is not exhausted
+This is the one distinction that will bite you if you skip it.
exhausted— the end of the collection was genuinely reached. Only then isnext_cursornull.truncated— the scan budget ran out first. This is not “no more results”.
GET /v1/palette/library?min_contrast=1.9&limit=3&max_scan=5000
{
"scanned": 5000,
"hit_rate": 0,
"count": 0,
"exhausted": false,
"truncated": true,
"next_cursor": 5000
}
An empty page with a cursor. A client that treats empty as done stops here and
reports that no palette in five million has a worst pair above 1.9:1, which is
false — it simply has not looked far enough yet. Keep going from
next_cursor, or raise max_scan (default 20,000,
maximum 500,000).
The loop that is correct: keep requesting while next_cursor is not
null and you still want more, and treat truncated as “ask
again”, never as “finished”.
6. Sorting orders the scan, not the collection
+GET /v1/palette/library?sort=contrast&order=desc&limit=5&max_scan=50000
"sorted_within_scan": true
“The five highest-contrast palettes” would be a claim about five
million records, and no request makes that claim honestly. What you get is the
five highest-contrast palettes among the ones the scan derived, and
the flag says so. Raise max_scan to widen the field it ranked.
sort=id is the exception and the default: it needs no ranking, so
it streams.
7. One palette, whole
+GET /v1/palette/library/pal_0
{
"palette": {
"id": 0,
"palette_id": "pal_0",
"method": "complementary",
"colors": [
{ "hex": "#28ACE4", "rgb": [40, 172, 228], "oklch": [0.7016, 0.1343, 232.59] },
...
],
"mean_lightness": 0.6499,
"mean_chroma": 0.1588,
"hue_spread": 233.3,
"character": "diverse",
"min_pair_contrast": 1.3,
"max_pair_contrast": 3.72,
"carries_body_text": false,
"derivation": { "seed": 42, "batch": 0, "batch_size": 50000, ... }
}
}
Ids come in two spellings: pal_zz is base-36 for 1,295, and
/v1/palette/library/1295 returns the same record.
character is measured, not generated: it comes from the hue spread
and the mean chroma, so it describes what a palette turned out to be rather
than which method produced it.
8. Statistics, and the sample behind them
+GET /v1/palette/library/stats
{
"measured_on": {
"sample": 42000,
"runs": 70,
"run_length": 600,
"note": "Every figure below is from 70 runs of 600 consecutive palettes,
spread evenly across the collection — 42,000 sampled out of
5,000,000. Shares are of the sample, not of the collection."
},
"mean_min_contrast": 1.111,
"with_an_aa_pair": { "count": 22944, "share": 54.63 },
"character": [
{ "name": "diverse", "count": 28489, "share": 67.83 },
{ "name": "triadic", "count": 12484, "share": 29.72 },
{ "name": "analogous", "count": 982, "share": 2.34 },
{ "name": "monochrome", "count": 44, "share": 0.10 },
{ "name": "neutral", "count": 1, "share": 0.00 }
]
}
Five million palettes cannot be counted inside a request, so this is a sample and it says so with every figure. The sampling scheme matters as much as the size: a stride over the whole collection pays the batch replay on every single sample and takes 28 seconds, where contiguous runs pay it once per run and take under a second. Each generator method comes out at 14.28% or 14.29% of the sample — one seventh — which is the check that runs are not biasing it.
Two numbers worth comparing
54.6% of these palettes contain a pair that carries body text. The harmony collection manages 25.4%. The difference is structural: a harmony is hue rotations around one base at a fixed lightness, so its members are nearly equiluminant by construction. These are five independent colours, so they differ in lightness by accident — and accident, here, is more useful than theory.
And only 1 in 42,000 is neutral. Five random colours are
almost never all low-chroma. If you want a muted palette, this collection is
the wrong instrument — use /v1/palette/generate with a
method.
9. Three complete tasks
+Page a rare filter correctly
let cursor = 0;
const found = [];
while (found.length < 20) {
const url = `https://api.auricartisan.com/v1/palette/library`
+ `?min_contrast=1.8&limit=20&max_scan=100000&cursor=${cursor}`;
const r = await (await fetch(url, { headers: { authorization: `Bearer ${KEY}` } })).json();
found.push(...r.palettes);
if (r.exhausted) break; // the collection really ended
cursor = r.next_cursor; // truncated or not, carry on from here
}
Note what the loop does not do: break on an empty page. That is the mistake this endpoint's response shape exists to prevent.
Find a palette you can build an interface from
GET /v1/palette/library?carries_body_text=true&min_chroma=0.08&limit=12
Some pair clears AA, and the set is not washed out. Roughly one palette in two passes the first test, so this scan stays cheap.
Cite one
GET /v1/palette/library/pal_2s9y
The response carries seed 42 and the batch, so the citation is checkable: anybody can recompute palette 129,958 and get the same five colours.
10. What each call costs
+GET /v1/palette/library/{id}— 1 credit. One derivation.GET /v1/palette/library— 3 credits, whatevermax_scanyou ask for.GET /v1/palette/library/stats— 10 credits. Samples 42,000 palettes.
A wide scan costs the same three credits as a narrow one, so there is no reason to under-ask on a rare filter. It costs latency, not money.
11. Errors, limits and pitfalls
+- Never treat an empty page as the end. Check
exhausted. See section 5. - Never read a sorted page as a ranking of the collection. Check
sorted_within_scan. - Ids run 0–4,999,999, in either spelling. Anything else is a validation error.
limitis 1–100;max_scanis 1,000–500,000.- Ask for ranges, not scattered ids. Deriving a record replays its batch from the start, so a hundred sequential reads cost far less than a hundred random ones.
- The library page cannot scroll to all five million. Browsers cap an element's height near 33.5 million pixels, which is about 550,000 palettes at four to a row. The page says so, and its search box takes any id. This API has no such limit.
12. Endpoint reference
+| Endpoint | Method | Credits | Answers |
|---|---|---|---|
/v1/palette/library | GET | 3 | Which palettes match, and how far did it have to look? |
/v1/palette/library/{id} | GET | 1 | One palette, with the derivation that reproduces it. |
/v1/palette/library/stats | GET | 10 | What the collection looks like, on a stated sample. |
The machine-readable form is in the API reference, generated from the same catalogue the server routes from.
To see the scan behaviour without writing any code, open the
palette library and pick a filter — the
counter under the grid is the same scanned this API reports.