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.
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.
{
"_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"
}
]
}
}
Color properties:
| Field | Type | Description |
|---|---|---|
key | string | The color's key (a free-form string slug; primary key) |
color | string | The stored color value |
source | string | null | Optional 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:
| Field | Type | Description |
|---|---|---|
key | string | The color's key |
color | string | The stored color value |
source | string | null | Optional source tag |
hex | string | Concrete hex resolved from color (a material name like blue or blue:300, or a hex) |
named | string | The family:shade label for a known material hex, otherwise "" |
Rows come back in raw DB order. On any error the route answers 422.
[
{
"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"
}
]
GET /api/colors/{key}
Retrieve a single color resolved by its string key.
Authentication: Required (read is open within the tenant)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
key | string | The 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.
{
"_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"
}
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:
| Parameter | Type | Description |
|---|---|---|
key | string | The color's key. URL-encode spaces and other special characters |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
color | string | Yes | A 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.
{
"color": "#43A047"
}
{
"_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
}
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.
{
"color": "blue"
}
{
"statusCode": 400,
"error": "Bad Request",
"message": "Invalid color format"
}
DELETE /api/colors/{key}
Delete a color.
Authentication: Designer (permission.color.delete)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
key | string | The 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.
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:
| Parameter | Type | Description |
|---|---|---|
key | string | The 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.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/colors/Order%20Desk/permissions"
}
},
"edit": true,
"delete": true,
"write": true
}
POST /api/colors-export
Build a downloadable JSON file of the palette.
Authentication: Designer
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
source | string | No | Filter to colors tagged with that source |
progress | string | No | UI/streaming progress hint only |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
download | boolean | When 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.
{}
{
"_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
}
?download=true variant is not shownWith ?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:
| Field | Type | Required | Description |
|---|---|---|---|
uploadId | string | Yes | Id 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.
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.