Skip to main content

Colors

Endpoints for the tenant Color palette, a small per-tenant key/value store of named colors that visuals and category styling draw from. Each Color is a tiny row keyed by a free-form string slug (a category name or data value), with a stored color value and an optional source tag.

A Color has no UUID id, no createdAt/updatedAt, and no owner column. Rows are tenant-scoped through row-level security on the color table. Reads are open within the tenant (any authenticated session). Writes require the Designer role.

Color value vs. derived hex

The write path (PUT /api/colors/\{key\}) accepts only a hex color such as #fff or #F57C00. The flat list (GET /api/colors-list) additionally resolves stored values to a concrete hex and a family:shade named label, so a stored material name like blue reads back with a resolved hex even though the API write path would reject that same name.

GET /api/colors

List the tenant's color palette as a HAL collection.

Authentication: Required (read is open within the tenant)

Response:

A HAL collection (rel inf:colors) with items embedded under _embedded["inf:color"], each linked by its key (href is ./\{key\}). This is not a {total, limit, offset, results} envelope.

List colorsGET /api/colors
GET /api/colors is a HAL collection (rel inf:colors) of the tenant’s color palette, items embedded under inf:color (each linked by its key). Read is open within the tenant. It is NOT a {total,limit,offset,results} envelope.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/colors"
}
},
"start": 0,
"count": 3,
"total": 3,
"_embedded": {
"inf:color": [
{
"_links": {
"self": {
"href": "https://informer.example.com/api/colors/Order%20Desk"
}
},
"key": "Order Desk",
"color": "blue",
"source": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/colors/Shipping"
}
},
"key": "Shipping",
"color": "lightGreen",
"source": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/colors/Returns"
}
},
"key": "Returns",
"color": "#F57C00",
"source": "order-desk",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
}
]
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

Color properties:

FieldTypeDescription
keystringThe color's key (a free-form string slug; primary key)
colorstringThe stored color value
sourcestring | nullOptional source tag

GET /api/colors-list

Get a flat array of every color, each enriched with derived fields.

Authentication: Required (read is open within the tenant)

Response:

A flat JSON array (rel inf:colors-list), not a HAL collection envelope. It runs a tenant-scoped select * from color and enriches each row with two derived fields the CRUD routes do not return:

FieldTypeDescription
keystringThe color's key
colorstringThe stored color value
sourcestring | nullOptional source tag
hexstringConcrete hex resolved from color (a material name like blue or blue:300, or a hex)
namedstringThe family:shade label for a known material hex, otherwise ""

Rows come back in raw DB order. On any error the route answers 422.

List colors (flat, with derived hex)GET /api/colors-list
GET /api/colors-list (rel inf:colors-list) is the flatter sibling of GET /api/colors. It runs a tenant-scoped `select * from color` and enriches each row with two DERIVED fields the CRUD routes do not return: `hex` (highchart-colors.getHex(color) — resolves a material name like "blue" or "blue:300", or a hex, to a concrete hex) and `named` (the "family:shade" label for a known material hex, else ""). Rows come back in DB order. Read is open within the tenant.
Response · 200
[
{
"key": "Order Desk",
"color": "blue",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"source": null,
"tenant": "acme",
"hex": "#64b5f6",
"named": "blue:300"
},
{
"key": "Returns",
"color": "#F57C00",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"source": "order-desk",
"tenant": "acme",
"hex": "#F57C00",
"named": "orange:700"
},
{
"key": "Shipping",
"color": "lightGreen",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"source": null,
"tenant": "acme",
"hex": "#9ccc65",
"named": "lightGreen:400"
}
]
Captured from the API examples test suite; ids and timestamps are normalized.

GET /api/colors/{key}

Retrieve a single color resolved by its string key.

Authentication: Required (read is open within the tenant)

Path Parameters:

ParameterTypeDescription
keystringThe color's key. A free-form slug, so spaces are valid (URL-encode them)

Response:

The single color (rel inf:color), resolved by Color.findById(key) with a 404 when the key does not exist. It carries one link, inf:permissions, at ./permissions.

Get a colorGET /api/colors/Order%20Desk
GET /api/colors/{key} returns the single color (HAL rel inf:color) resolved by its string key (db.lookup -> Color.findById; 404 if the key does not exist). It carries one link, inf:permissions -> ./permissions. Read is open within the tenant.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/colors/Order%20Desk"
},
"inf:permissions": {
"href": "https://informer.example.com/api/colors/Order%20Desk/permissions"
}
},
"key": "Order Desk",
"color": "blue",
"source": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
}
Captured from the API examples test suite; ids and timestamps are normalized.

PUT /api/colors/{key}

Create or update a color. This is an upsert: it creates the color when \{key\} is new and overwrites it otherwise.

Authentication: Designer (permission.color.edit)

Path Parameters:

ParameterTypeDescription
keystringThe color's key. URL-encode spaces and other special characters

Request Body:

FieldTypeRequiredDescription
colorstringYesA valid 3- or 6-digit hex (e.g. #fff or #43A047)

The validHex pre-block validates color against the regex ^#([0-9a-f]{6}|[0-9a-f]{3})$ (case-insensitive). A bare material name such as blue is rejected with 400.

Example Request:

{ "color": "#43A047" }

Response:

Responds 200 with the upserted color. The route opens a transaction.

Create or update a colorPUT /api/colors/Fulfilled
PUT /api/colors/{key} is an UPSERT (db.upsert): it creates the color when {key} is new and overwrites it otherwise. Requires the designer role (permission.color.edit -> color:write -> isDesigner()). Payload is { color } and `color` MUST be a valid hex — the validHex pre-block rejects a bare material name (e.g. "red") with 400. Returns 200 with the upserted color. Opens a route transaction. Here {key} "Fulfilled" is new, so this creates it.
Request body
{
"color": "#43A047"
}
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/colors/Fulfilled"
}
},
"tenant": "acme",
"key": "Fulfilled",
"color": "#43A047",
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z",
"source": null
}
Captured from the API examples test suite; ids and timestamps are normalized.
Hex only on the write path

The write path accepts only a hex color. A bare material name such as blue fails the validHex pre-block and the route answers 400 (Invalid color format). This is the opposite of how colors are typically seeded in code and of what GET /api/colors-list happily renders.

Reject a non-hex colorPUT /api/colors/Fulfilled
PUT /api/colors/{key} only accepts a hex `color`. A bare material name such as "blue" fails the validHex pre-block (highchart-colors.validateHex) and the route answers 400 (boom.badRequest "Invalid color format"). Use a 3- or 6-digit hex like "#fff" or "#43A047" instead.
Request body
{
"color": "blue"
}
Response · 400
{
"statusCode": 400,
"error": "Bad Request",
"message": "Invalid color format"
}
Captured from the API examples test suite; ids and timestamps are normalized.

DELETE /api/colors/{key}

Delete a color.

Authentication: Designer (permission.color.delete)

Path Parameters:

ParameterTypeDescription
keystringThe color's key. URL-encode spaces and other special characters

Response:

Responds 200 with an empty body (Informer's db.remove convention), not 204. The route opens a transaction.

Delete a colorDELETE /api/colors/Scratch
DELETE /api/colors/{key} requires the designer role (permission.color.delete -> color:write -> isDesigner()) and removes the row (db.remove). It is IDEMPOTENT — deleting a non-existent key still answers 200. Returns 200 with an EMPTY body (Informer convention), NOT 204. Opens a route transaction.
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.
Idempotent delete

Deleting a non-existent key still answers 200.


GET /api/colors/{key}/permissions

Get the per-color capability map for the current caller.

Authentication: Required (read is open within the tenant)

Path Parameters:

ParameterTypeDescription
keystringThe color's key. The color is resolved first (404 if missing)

Response:

A plain object of { <permission>: boolean } for every two-argument permission driver in the color group, here edit and delete. The booleans reflect what the current caller may do (both true for a Designer or superuser). This is a capability map for the current caller, not a stored ACL.

Get a color’s permissionsGET /api/colors/Order%20Desk/permissions
GET /api/colors/{key}/permissions resolves the color (color.lookup -> 404 if missing) then returns the per-color capability MAP for the `color` group: a plain object of { <permission>: boolean } for every 2-arg permission driver in the group — here { edit, delete }. The booleans reflect what the CURRENT caller may do (true here because admin is a designer/superuser). It is NOT a stored ACL.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/colors/Order%20Desk/permissions"
}
},
"edit": true,
"delete": true,
"write": true
}
Captured from the API examples test suite; ids and timestamps are normalized.

POST /api/colors-export

Build a downloadable JSON file of the palette.

Authentication: Designer

Request Body:

FieldTypeRequiredDescription
sourcestringNoFilter to colors tagged with that source
progressstringNoUI/streaming progress hint only

Query Parameters:

ParameterTypeDescription
downloadbooleanWhen true, streams the file directly instead of returning the Download resource

Example Request:

{}

Response:

Responds 201 Created with the created Download resource metadata (each exported entry is { key, color }) and a Location header pointing at the download lookup. The file bytes are fetched separately via the returned download link, not inlined in this response.

Export colorsPOST /api/colors-export
POST /api/colors-export builds a downloadable JSON file of the palette (each entry { key, color }) and responds 201 with the created Download resource and a Location header pointing at download.lookup. Payload is { source?, progress? }: `source` filters to colors tagged with that source; `progress` is a UI/streaming hint only. The response body is the Download metadata (JSON) — the file BYTES are fetched separately via the returned download link, not inlined here. The `?download=true` query variant streams the file directly instead.
Request body
{}
Response · 201
{
"_links": {
"self": {
"href": "https://informer.example.com/api/downloads/00000000-0000-4000-8000-000000000001"
}
},
"type": "download",
"id": "00000000-0000-4000-8000-000000000001",
"filename": "colors.json",
"size": 524288,
"chunks": 1,
"user": "admin",
"created": 1700000000000
}
Captured from the API examples test suite; ids and timestamps are normalized.
The ?download=true variant is not shown

With ?download=true the route streams the export file bytes directly rather than returning Download metadata. That binary download path is not captured here.


POST /api/colors-import

Import a palette from a previously uploaded JSON file.

Authentication: Designer

Request Body:

FieldTypeRequiredDescription
uploadIdstringYesId of an already-staged upload (server.methods.upload.lookup)

The route reads the uploaded file's bytes, JSON-parses them, validates against an array schema, then bulk-upserts the colors. On success it returns plain JSON { colorsImported: <count> } with 200. A bad or unparseable upload answers 400.

Example not shown

The interesting input is the uploaded file itself, which is upload/binary plumbing that requires a staged upload Resource. This endpoint is therefore documented without a captured request/response example.