For developers

Trycolors API

The same mixing engine as trycolors.com, over HTTP. Send the paints you have and a target color, get back a recipe in parts. Mix paints in given proportions, or search the paint catalog.

Overview

The Trycolors API mixes paint colors, reverse-engineers a target color into a recipe from paints you name, and searches the Trycolors paint catalog.

  1. Sign up for an account at trycolors.com/signin.
  2. Generate your API key in your account settings.
  3. Send the key in the X-API-KEY header on every request.

Base URL

https://api.trycolors.com/v1

Endpoints

EndpointMethodWhat it does
/unmix-colorPOSTTurns a target color into a recipe from the paints you send
/mix-colorsPOSTMixes paints in given proportions and returns the result
/paints/searchPOSTSearches the paint catalog by keyword, color and filters
/similar-paintsPOSTFinds catalog paints close to a hex code

Any other path under /v1/ is not part of the public API.

Example: mixing colors

const response = await fetch('https://api.trycolors.com/v1/mix-colors', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': process.env.TRYCOLORS_API_KEY
  },
  body: JSON.stringify({
    colors: [
      { hex: '#FFAA16', count: 1 },
      { hex: '#BA12FF', count: 1 }
    ],
    mixerMode: 'basic'
  })
});

const data = await response.json();
console.log(data.mixedColor); // "#DD5E8B"

Responses carry no engine version and no request id. Store the settings you sent alongside any recipe you keep, so you can reproduce it later.

Authentication

Every request to the Trycolors API needs an API key.

Get an API key

  1. Sign in to your Trycolors account at trycolors.com/signin.
  2. Go to your account settings.
  3. Find the API Keys card.
  4. Click Generate API Key.
  5. Copy your new key and store it somewhere safe.

Use your API key

Send the key in the X-API-KEY header of each request:

X-API-KEY: your_api_key_here

A request with no key returns 401 with the plain-text body Authentication required: X-API-KEY or X-Session-ID header. A request with a key we do not recognize returns 401 with Invalid API Key. Both bodies are plain text, not JSON, so do not parse them as JSON.

Keep your key safe

  1. Never share your API key publicly or commit it to version control.
  2. Store it in environment variables or a secret manager.
  3. Rotate your API key from time to time.
  4. If you think your key leaked, generate a new one and delete the old one in your settings. Deleting a key takes effect within two minutes, because key lookups are cached that long.

Endpoints

Every endpoint below is a POST to https://api.trycolors.com/v1/... with a JSON body and these headers:

Content-Type: application/json
X-API-KEY: your_api_key_here

All examples on this page were run against the live API. Responses are real, trimmed where marked.

Common behavior

  • A missing key returns 401 with the plain-text body Authentication required: X-API-KEY or X-Session-ID header. An unknown key returns 401 with Invalid API Key. These two are plain text, not JSON.
  • A path that is not listed on this page returns 404 with {"error":"Endpoint not found","docs":"https://api.trycolors.com/docs"}.
  • Validation failures return 400 with {"error":"..."}.
  • Over the rate limit returns 429. See Rate limits.
  • Responses do not carry an engine version or a request id. If you store a recipe, store the engine, mixerMode and paint ids you sent with it.

Unmix color

Finds the mix of your paints that comes closest to a target color.

POST/v1/unmix-color

Request body

FieldTypeRequiredNotes
colorsarrayyesAt least 2 entries. Each entry is either a hex string or an object (see below).
targetHexstringyesThe color to reach, #RRGGBB.
maxColorsCountintegernoMost paints the recipe may use. Positive whole number. Defaults to the number of paints you sent.
maxDropsCountintegernoMost parts any single paint may take in the recipe. The whole recipe never exceeds 50 parts. Positive whole number, default 30.
mixerModestringno"basic" or "pro". Default "basic".
enginestringno"2023" or "2025". Default "2023". Only read when mixerMode is "pro".
tintingStrengthModestringno"uniform" or "adjusted". Default "uniform". Ignored when engine is "2025".

A colors entry is either a plain hex string:

["#00FFFF", "#FF00FF", "#FFFF00"]

or an object:

FieldTypeRequiredNotes
hexstringyes#RRGGBB. Case insensitive on input, echoed back as you sent it.
namestringnoYour label for the paint. Echoed back in structure, practicalSteps and omittedColors.
paint_idintegernoA Trycolors catalog paint id. When present, the engine uses that paint's fitted mixing curve instead of guessing from the hex.

paint_id is what makes a recipe reproducible with real tubes. Without it the engine only knows the color you typed; with it the engine knows the pigment behind that color.

Modes and engines

`mixerMode``engine`What runs
basicignoredSimple weighted average in RGB. Free tier behavior.
pro2023Legacy mixing algorithm. The default when you send mixerMode: "pro" and no engine.
pro2025Kubelka-Munk spectral mixing over fitted paint curves.

mixerMode: "pro" with engine: "2025" is the same calculation as the Pro mode of the mixer on trycolors.com. Send both, or you get the legacy 2023 engine.

Example request

{
  "colors": [
    { "hex": "#F7F5F1", "name": "Titanium White", "paint_id": 3554 },
    { "hex": "#19123F", "name": "Ultramarine Blue", "paint_id": 3656 },
    { "hex": "#FFC500", "name": "Cadmium Yellow Medium", "paint_id": 3573 },
    { "hex": "#673020", "name": "Burnt Sienna", "paint_id": 3610 },
    { "hex": "#5D162D", "name": "Quinacridone Magenta", "paint_id": 3650 }
  ],
  "targetHex": "#6E8B74",
  "maxColorsCount": 4,
  "maxDropsCount": 20,
  "mixerMode": "pro",
  "engine": "2025"
}

Those five paint ids are Golden Heavy Body tubes in the Trycolors catalog.

Example response

{
  "structure": [
    { "hex": "#F7F5F1", "name": "Titanium White", "count": 0.47368421052631576, "wholeParts": 9, "tintingStrength": 26, "paint_id": 3554 },
    { "hex": "#19123F", "name": "Ultramarine Blue", "count": 0.3157894736842105, "wholeParts": 6, "tintingStrength": 77, "paint_id": 3656 },
    { "hex": "#FFC500", "name": "Cadmium Yellow Medium", "count": 0.10526315789473684, "wholeParts": 2, "tintingStrength": 21, "paint_id": 3573 },
    { "hex": "#673020", "name": "Burnt Sienna", "count": 0.10526315789473684, "wholeParts": 2, "tintingStrength": 61, "paint_id": 3610 },
    { "hex": "#5D162D", "name": "Quinacridone Magenta", "count": 0, "wholeParts": 0, "tintingStrength": 67, "paint_id": 3650 }
  ],
  "mixedColor": "#718C76",
  "matchResult": 99.2,
  "ratioMixedColor": "#718C76",
  "ratioMatchResult": 99.2,
  "practicalSteps": [
    {
      "step": 1,
      "color": { "hex": "#FFC500", "name": "Cadmium Yellow Medium", "wholeParts": 2, "tintingStrength": 21 },
      "cumulative": { "#FFC500": 2 },
      "mixedColor": "#FFC500",
      "matchResult": 62.6
    },
    {
      "step": 2,
      "color": { "hex": "#F7F5F1", "name": "Titanium White", "wholeParts": 9, "tintingStrength": 26 },
      "cumulative": { "#FFC500": 2, "#F7F5F1": 9 },
      "mixedColor": "#FBEE76",
      "matchResult": 64.3
    },
    {
      "step": 3,
      "color": { "hex": "#673020", "name": "Burnt Sienna", "wholeParts": 2, "tintingStrength": 61 },
      "cumulative": { "#FFC500": 2, "#F7F5F1": 9, "#673020": 2 },
      "mixedColor": "#D7B06F",
      "matchResult": 71.8
    },
    {
      "step": 4,
      "color": { "hex": "#19123F", "name": "Ultramarine Blue", "wholeParts": 6, "tintingStrength": 77 },
      "cumulative": { "#FFC500": 2, "#F7F5F1": 9, "#673020": 2, "#19123F": 6 },
      "mixedColor": "#718C76",
      "matchResult": 99.2
    }
  ],
  "diagramSteps": [
    { "step": 1, "cumulative": { "#FFC500": 2 }, "mixedColor": "#FFC500" },
    { "step": 2, "cumulative": { "#FFC500": 2, "#F7F5F1": 1 }, "mixedColor": "#FFD800" },
    { "step": 18, "cumulative": { "#FFC500": 2, "#F7F5F1": 9, "#673020": 2, "#19123F": 6 }, "mixedColor": "#718C76" }
  ],
  "omittedColors": []
}

diagramSteps is trimmed above. The real call returned 18 steps, one per part added.

Response fields

FieldTypeNotes
structurearrayEvery paint you sent, in the order you sent it, including the ones the recipe did not use.
structure[].hexstringThe hex you sent.
structure[].namestring or nullThe name you sent, or null if you sent none.
structure[].paint_idintegerEchoed back only for entries where you sent one. Absent otherwise.
structure[].countnumberFractional share of the mix, 0 to 1. The used entries sum to 1.
structure[].wholePartsintegerWhole parts for mixing by hand, each at most maxDropsCount. 0 means the paint is not in the practical recipe.
structure[].tintingStrengthintegerBetween 10 and 90, derived from the hex brightness. Darker reads as stronger. Only used when tintingStrengthMode is "adjusted".
mixedColorstringThe color you get from the wholeParts recipe, which is what a painter actually mixes.
matchResultnumberHow close mixedColor is to targetHex, 0 to 100.
ratioMixedColorstringThe color you get from the exact count ratios. This is the best the palette can do.
ratioMatchResultnumberHow close ratioMixedColor is to targetHex, 0 to 100.
practicalStepsarrayOne step per paint, ordered so the mix stays workable: weak and light paints first, strong and dark last. Each step carries the paint added, the running cumulative map of hex to parts, and the color and match after that step.
diagramStepsarrayThe same order at finer grain: the first paint all at once, then one part at a time. Built for animation. Each step has step, cumulative and mixedColor, with no matchResult.
omittedColorsarrayPaints the exact ratios wanted but rounding dropped, that is count > 0 and wholeParts === 0. Each entry is { hex, name }, with name falling back to the hex. A paint the optimizer never picked at all has count: 0 and does not appear here.

Conditional fields

Three fields appear only in some responses. Treat all three as optional.

suggestedPigments and bestAchievableMatch appear when the match falls below 95. bestAchievableMatch is the match a full pigment set would reach, so you can tell "your palette cannot do this" from "this color is hard for anyone". suggestedPigments names up to a few paints that would close the gap.

{
  "matchResult": 90.9,
  "ratioMatchResult": 91,
  "bestAchievableMatch": 99.7,
  "suggestedPigments": [
    { "hex": "#002E2A", "name": "Phthalo Green (Yellow Shade)", "predictedMatch": 100, "gain": 9.01 },
    { "hex": "#00A1A4", "name": "Cobalt Teal", "predictedMatch": 99.8, "gain": 8.84 },
    { "hex": "#00656B", "name": "Cobalt Turquoise", "predictedMatch": 95.6, "gain": 4.59 }
  ]
}

That response came from the same five Golden tubes aimed at #00B894, a green they cannot reach.

dilution appears for trace recipes: pale targets that need a dose of strong pigment too small for any parts ratio, such as a cream that wants 0.3% cadmium red in white. When it is present the recipe splits in two. structure[].wholeParts becomes a small concentrate you mix first, structure[].count stays the final proportions, and mixedColor and matchResult describe the marked ladder stop, which is the color you end up with.

FieldNotes
dilution.concentrateHexColor of the concentrate, that is the wholeParts mix.
dilution.diluent{ hex, name } of the paint to dilute into, normally the white in your palette.
dilution.ladderStops from the concentrate toward the diluent. Each has hex, concentrateShare (the fraction of the final mix that is concentrate) and matchResult.
dilution.targetStopIndexIndex into ladder of the stop that matches the target.
dilution.approxRatio{ concentrateParts, whiteParts }, a readable "1 part concentrate to N parts white".
dilution.plainBest{ mixedColor, matchResult }, what the parts-limited recipe would have given without the ladder.

Errors

{ "error": "Invalid input colors. At least 2 valid colors are required." }
{ "error": "Invalid target hexadecimal color." }
{ "error": "maxColorsCount and maxDropsCount must be positive whole numbers." }
{ "error": "Invalid engine: 2024. Must be '2023' or '2025'." }

A 503 with {"error":"Unmixer service is temporarily unavailable..."} means the mixing service is down. Retry.

Mix colors

Mixes paints in the proportions you give and returns the result.

POST/v1/mix-colors

Request body

FieldTypeRequiredNotes
colorsarrayyesNon-empty array of objects.
colors[].hexstringyes#RRGGBB.
colors[].countintegeryesParts of this paint. Non-negative whole number. Not optional: a missing count is a 400.
colors[].paint_idintegernoCatalog paint id. Read only by mixerMode: "pro" with engine: "2025", where it picks that paint's fitted curve. Ignored by the other paths.
colors[].tintingStrengthintegernoBetween 1 and 100. Read only when tintingStrengthMode is "adjusted".
mixerModestringno"basic" or "pro". Default "basic".
enginestringno"2023" or "2025". Default "2023". Only read when mixerMode is "pro".
tintingStrengthModestringno"uniform" or "adjusted". Default "uniform". Ignored when engine is "2025".

Example request

{
  "colors": [
    { "hex": "#F7F5F1", "count": 3, "paint_id": 3554 },
    { "hex": "#19123F", "count": 1, "paint_id": 3656 }
  ],
  "mixerMode": "pro",
  "engine": "2025"
}

Example response

{ "mixedColor": "#7FAEEB" }

The response is only the mixed color. Nothing else is returned.

Notes

  • Paints with count: 0 are skipped by the 2025 path. If every count is 0, the response is {"mixedColor":"#000000"}.
  • The 2025 path returns 400 when it cannot resolve mixing data for a color: {"error":"Could not get K/S data for color #XXXXXX: ..."}.
  • Enamel paints only mix with enamels of the same brand. In a mixed request the enamel entries fall back to plain hex resolution, so the result is less accurate than a pure enamel mix.

Search paints

Searches the Trycolors paint catalog by keyword, by color similarity, or by filters, with pagination.

POST/v1/paints/search

Request body

Every field is optional. A body with no search term and no filter returns the whole catalog sorted by name, page by page, so send at least one of q, color or a filter.

FieldTypeNotes
qstringKeyword. Searches paint name, slug, product number, brand name and pigment name or code.
colorstring#RRGGBB for similarity search.
thresholdnumberMaximum LAB distance for color. Default 10. Ignored without color.
brandstring or arrayBrand paths, for example "daniel-smith". A string may be comma separated.
mediumstring or arrayMedium names, for example "Watercolor".
lineIdstring, number or arrayProduct line ids.
transparencystring or arrayFor example "Transparent", "Semi-Transparent", "Opaque".
stainingstring or arrayFor example "Low Staining".
granulationstring or array"Yes" or "No".
colorGroupNamesstring or arrayOne of Red, Orange, Yellow, Green, Blue, Purple, Brown, Black, White, Grey. Singular, exactly as returned in colorGroupName.
sortBystring"relevance", "name" or "distance". Defaults to "relevance" with q, "distance" with color, "name" otherwise. "distance" without color falls back.
pageintegerDefault 1.
limitintegerResults per page. Default 20.

Example request

{
  "q": "quinacridone gold",
  "brand": ["daniel-smith"],
  "limit": 2
}

Example response

{
  "paints": [
    {
      "hex": "#BF7105",
      "paintName": "Quinacridone Gold",
      "sku": "",
      "brandName": "Daniel Smith",
      "brandId": 91,
      "lineName": "Original Oils",
      "mediumName": "Oil",
      "colorGroupName": "Black",
      "distance": null,
      "relevance": 205.76,
      "pigments": [
        { "id": 235, "code": "PO48", "name": "Quinacridone Burnt Orange", "colorIndex": null },
        { "id": 783, "code": "PY150", "name": "Nickel Azo Yellow", "colorIndex": null }
      ],
      "slug": "daniel-smith-original-oils-quinacridone-gold",
      "brandPath": "daniel-smith",
      "properties": {
        "transparency": "Transparent",
        "staining": "",
        "granulation": null,
        "lightfastness": "I – Excellent"
      }
    }
  ],
  "totalCount": 74
}

One of the two returned paints is shown.

Response fields

FieldTypeNotes
paints[].hexstringCatalog display color.
paints[].paintNamestringPaint name without the product number.
paints[].skustring or nullManufacturer product number. Can be null or an empty string when the catalog has none.
paints[].brandNamestring
paints[].brandIdinteger
paints[].lineNamestring or nullProduct line.
paints[].mediumNamestringFor example Oil, Watercolor, Acrylic.
paints[].colorGroupNamestring or nullCatalog color group.
paints[].distancenumber or nullLAB distance to color, rounded to 2 decimals. null when you did not send color.
paints[].relevancenumber or nullKeyword relevance score, rounded to 2 decimals. null when you did not send q.
paints[].pigmentsarray{ id, code, name, colorIndex } per pigment. code is the color index code such as PO48, name is the pigment name, colorIndex is the CI number and is often null. Empty array when the paint has no pigment data.
paints[].slugstringPaint slug, used in the URL https://trycolors.com/paints/product/<slug>.
paints[].brandPathstringBrand slug. This is the value brand filters on.
paints[].propertiesobjecttransparency, staining, granulation ("Yes", "No" or null) and lightfastness. Any of them can be null or an empty string.
totalCountintegerMatches across all pages, not just this one.

The search response does not include the numeric paint id today. slug identifies a paint uniquely, so use it as your key. If you need paint_id values for /unmix-color or /mix-colors, contact support with the slugs.

Similar paints

Finds catalog paints close to a hex code. This endpoint reads an older catalog snapshot than /paints/search, so it returns fewer fields, and its brandId and seriesIds belong to that snapshot. They are not the ids /paints/search returns: Daniel Smith is brand 173 here and brand 91 there.

POST/v1/similar-paints

Request body

FieldTypeRequiredNotes
hexstringyes#RRGGBB.
countintegernoNumber of paints to return. Default 10, capped at 10.
brandIdsarray of integersnoLimit to these brands. All active brands when omitted.
seriesIdsarray of integersnoLimit to these series. All active series when omitted.

Example request

{ "hex": "#FF0C13", "count": 3 }

Example response

{
  "paints": [
    { "hex": "#FF0C13", "paintName": "Red Orange", "sku": "203-F", "brandName": "1 Shot", "brandId": 1, "seriesName": "Fluorescent", "distance": 0 },
    { "hex": "#F70714", "paintName": "Cadmium Red Hue", "sku": "1420", "brandName": "Lukas", "brandId": 414, "seriesName": "Aquarell Studio", "distance": 1.8248703279757885 },
    { "hex": "#FF0015", "paintName": "Vermilion", "sku": "436", "brandName": "Marabu", "brandId": 424, "seriesName": "DecorGlass", "distance": 0.6234378309325322 }
  ]
}

Response fields

hex, paintName, sku, brandName, brandId, seriesName, distance. There is no lineName, no pigments, no slug and no paint id.

Results are ranked in RGB while distance is reported in LAB, so the array is not sorted by distance. Sort client side if the order matters. For anything beyond a quick lookup, prefer /paints/search with color and sortBy: "distance", which searches the current catalog and returns pigments and slugs.

Other paths

A few other paths under /v1/ serve trycolors.com itself. They are not part of the public API, are not supported for third-party use, and can change without notice. Every path not listed on this page should be treated as unavailable, including any you may find by guessing.

Rate limits

Rate limits are per endpoint and per account, in two tiers. An API key you generate in your account settings gets the standard tier on a Hobby or Pro plan, and the Business tier on a Business plan.

Standard (Hobby and Pro individual plans)

EndpointRequests per secondRequests per day
/mix-colors550
/unmix-color13
/similar-paints110
/paints/search110

Business plan

EndpointRequests per secondRequests per day
/mix-colors5060,000
/unmix-color5300
/similar-paints51,000
/paints/search51,000

Business limits can be raised on request. Contact support if you need more.

How the limits work

  • The per-second counter resets one second after the first request in its window. The daily counter resets when the server's date rolls over.
  • Limits are counted per endpoint, not shared across endpoints.
  • Usage is counted per account, so several keys or servers on one account draw from the same allowance.

Handling 429

Over either limit the API returns 429 with a plain-text body: Rate limit exceeded: too many requests per second or Rate limit exceeded: daily limit reached. Back off and retry, with exponential backoff for the per-second case.

Good practice

  1. Cache responses. Mixing the same paints at the same ratios always gives the same result.
  2. Spread calls out rather than sending bursts.

Questions or a limit increase? Contact us.