API 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_hereAll examples on this page were run against the live API. Responses are real, trimmed where marked.
Common behaviour
- A missing key returns
401with the plain-text bodyAuthentication required: X-API-KEY or X-Session-ID header. An unknown key returns401withInvalid API Key. These two are plain text, not JSON. - A path that is not listed on this page returns
404with{"error":"Endpoint not found","docs":"https://api.trycolors.com/docs"}. - Validation failures return
400with{"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,mixerModeand paint ids you sent with it.
Unmix Color
Finds the mix of your paints that comes closest to a target color.
- URL:
/v1/unmix-color - Method: POST
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
colors | array | yes | At least 2 entries. Each entry is either a hex string or an object (see below). |
targetHex | string | yes | The color to reach, #RRGGBB. |
maxColorsCount | integer | no | Most paints the recipe may use. Positive whole number. Defaults to the number of paints you sent. |
maxDropsCount | integer | no | Most parts any single paint may take in the recipe. The whole recipe never exceeds 50 parts. Positive whole number, default 30. |
mixerMode | string | no | "basic" or "pro". Default "basic". |
engine | string | no | "2023" or "2025". Default "2023". Only read when mixerMode is "pro". |
tintingStrengthMode | string | no | "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:
| Field | Type | Required | Notes |
|---|---|---|---|
hex | string | yes | #RRGGBB. Case insensitive on input, echoed back as you sent it. |
name | string | no | Your label for the paint. Echoed back in structure, practicalSteps and omittedColors. |
paint_id | integer | no | A 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 |
|---|---|---|
basic | ignored | Simple weighted average in RGB. Free tier behaviour. |
pro | 2023 | Legacy mixing algorithm. The default when you send mixerMode: "pro" and no engine. |
pro | 2025 | Kubelka-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
| Field | Type | Notes |
|---|---|---|
structure | array | Every paint you sent, in the order you sent it, including the ones the recipe did not use. |
structure[].hex | string | The hex you sent. |
structure[].name | string or null | The name you sent, or null if you sent none. |
structure[].paint_id | integer | Echoed back only for entries where you sent one. Absent otherwise. |
structure[].count | number | Fractional share of the mix, 0 to 1. The used entries sum to 1. |
structure[].wholeParts | integer | Whole parts for mixing by hand, each at most maxDropsCount. 0 means the paint is not in the practical recipe. |
structure[].tintingStrength | integer | 10 to 90, derived from the hex brightness. Darker reads as stronger. Only used when tintingStrengthMode is "adjusted". |
mixedColor | string | The color you get from the wholeParts recipe, which is what a painter actually mixes. |
matchResult | number | How close mixedColor is to targetHex, 0 to 100. |
ratioMixedColor | string | The color you get from the exact count ratios. This is the best the palette can do. |
ratioMatchResult | number | How close ratioMixedColor is to targetHex, 0 to 100. |
practicalSteps | array | One 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. |
diagramSteps | array | The 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. |
omittedColors | array | Paints 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.
| Field | Notes |
|---|---|
dilution.concentrateHex | Color 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.ladder | Stops from the concentrate toward the diluent. Each has hex, concentrateShare (the fraction of the final mix that is concentrate) and matchResult. |
dilution.targetStopIndex | Index 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.
- URL:
/v1/mix-colors - Method: POST
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
colors | array | yes | Non-empty array of objects. |
colors[].hex | string | yes | #RRGGBB. |
colors[].count | integer | yes | Parts of this paint. Non-negative whole number. Not optional: a missing count is a 400. |
colors[].paint_id | integer | no | Catalog paint id. Read only by mixerMode: "pro" with engine: "2025", where it picks that paint's fitted curve. Ignored by the other paths. |
colors[].tintingStrength | integer | no | 1 to 100. Read only when tintingStrengthMode is "adjusted". |
mixerMode | string | no | "basic" or "pro". Default "basic". |
engine | string | no | "2023" or "2025". Default "2023". Only read when mixerMode is "pro". |
tintingStrengthMode | string | no | "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: 0are skipped by the 2025 path. If every count is 0, the response is{"mixedColor":"#000000"}. - The 2025 path returns
400when 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.
- URL:
/v1/paints/search - Method: POST
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.
| Field | Type | Notes |
|---|---|---|
q | string | Keyword. Searches paint name, slug, product number, brand name and pigment name or code. |
color | string | #RRGGBB for similarity search. |
threshold | number | Maximum LAB distance for color. Default 10. Ignored without color. |
brand | string or array | Brand paths, for example "daniel-smith". A string may be comma separated. |
medium | string or array | Medium names, for example "Watercolor". |
lineId | string, number or array | Product line ids. |
transparency | string or array | For example "Transparent", "Semi-Transparent", "Opaque". |
staining | string or array | For example "Low Staining". |
granulation | string or array | "Yes" or "No". |
colorGroupNames | string or array | One of Red, Orange, Yellow, Green, Blue, Purple, Brown, Black, White, Grey. Singular, exactly as returned in colorGroupName. |
sortBy | string | "relevance", "name" or "distance". Defaults to "relevance" with q, "distance" with color, "name" otherwise. "distance" without color falls back. |
page | integer | Default 1. |
limit | integer | Results 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
| Field | Type | Notes |
|---|---|---|
paints[].hex | string | Catalog display color. |
paints[].paintName | string | Paint name without the product number. |
paints[].sku | string or null | Manufacturer product number. Can be null or an empty string when the catalog has none. |
paints[].brandName | string | |
paints[].brandId | integer | |
paints[].lineName | string or null | Product line. |
paints[].mediumName | string | For example Oil, Watercolor, Acrylic. |
paints[].colorGroupName | string or null | Catalog color group. |
paints[].distance | number or null | LAB distance to color, rounded to 2 decimals. null when you did not send color. |
paints[].relevance | number or null | Keyword relevance score, rounded to 2 decimals. null when you did not send q. |
paints[].pigments | array | { id, code, name, colorIndex } per pigment. code is the colour 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[].slug | string | Paint slug, used in the URL https://trycolors.com/paints/product/<slug>. |
paints[].brandPath | string | Brand slug. This is the value brand filters on. |
paints[].properties | object | transparency, staining, granulation ("Yes", "No" or null) and lightfastness. Any of them can be null or an empty string. |
totalCount | integer | Matches 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 (opens in a new tab) 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.
- URL:
/v1/similar-paints - Method: POST
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
hex | string | yes | #RRGGBB. |
count | integer | no | Number of paints to return. Default 10, capped at 10. |
brandIds | array of integers | no | Limit to these brands. All active brands when omitted. |
seriesIds | array of integers | no | Limit 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.