Skip to main content
Auric Artisan · Documentation

The Palette Library API

Date: August 29, 2026 For: Developers Category: Guide Author: Chirag Bansal
Back to Documentation API reference Open the library

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.

Table of contents

  1. 1. What this API is for
  2. 2. Getting a key and making your first call
  3. 3. Five million from 660 bytes
  4. 4. A filter is a scan, not a query
  5. 5. Truncated is not exhausted
  6. 6. Sorting orders the scan, not the collection
  7. 7. One palette, whole
  8. 8. Statistics, and the sample behind them
  9. 9. Three complete tasks
  10. 10. What each call costs
  11. 11. Errors, limits and pitfalls
  12. 12. Endpoint reference

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 is next_cursor null.
  • 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, whatever max_scan you 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.
  • limit is 1–100; max_scan is 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

+
EndpointMethodCreditsAnswers
/v1/palette/libraryGET3Which palettes match, and how far did it have to look?
/v1/palette/library/{id}GET1One palette, with the derivation that reproduces it.
/v1/palette/library/statsGET10What 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.