# Notes and Colors

Not everything on a Playbook board is a file. Two kinds of asset carry their content inline instead of pointing at one:

* A **note** is a sticky-note asset holding prose — a brief, a caption, a to-do, context for the assets around it. It has a background colour and a card height.
* A **colour swatch** holds a single colour. Two or more swatches grouped together become a **palette**.

Both are ordinary assets: they live on a board, take tags and statuses, appear in search, and are fetched, updated and deleted through the same endpoints as any other asset. What makes them different is that there are no bytes to fetch, so **they are created synchronously** — the response is the finished asset, with no `is_skeleton` polling step of the kind [uploading](/docs/guides/upload.md#async-ingest-semantics) requires.

## Prerequisites[​](#prerequisites "Direct link to Prerequisites")

1. **Access token** — an API token with the `write` scope, sent as a Bearer token.
2. **Organization slug** (`slug`) — your workspace identifier, e.g. `coolclient-ltd`.
3. **Board token** (`collection_token`) — optional. If omitted or unknown, the asset falls through to a default board rather than erroring, so validate it first if it matters. [Fetch board tokens](/docs/guides/fetching_data.md#list-boards-in-an-organization) from the boards endpoint.

***

## Create a note[​](#create-a-note "Direct link to Create a note")

Notes share the generic asset-create endpoint with URL ingest. Sending `text` instead of `uri` is what makes the new asset a note.

### Endpoint[​](#endpoint "Direct link to Endpoint")

```
POST /v1/{slug}/assets

Authorization: Bearer YOUR_TOKEN

Content-Type: application/json
```

### Request body[​](#request-body "Direct link to Request body")

All fields live under an `asset` key.

| Field              | Type    | Required | Description                                                                                         |
| ------------------ | ------- | -------- | --------------------------------------------------------------------------------------------------- |
| `text`             | string  | yes      | The note's content. Sending this instead of `uri` is what creates a note.                           |
| `title`            | string  | no       | Title shown above the note.                                                                         |
| `description`      | string  | no       | Longer description.                                                                                 |
| `color`            | string  | no       | Background colour as a hex code — use a note palette colour, see below. Renders white when omitted. |
| `height`           | integer | no       | Card height in pixels.                                                                              |
| `collection_token` | string  | no       | Board to add the note to.                                                                           |
| `group_token`      | string  | no       | An existing **group** asset to place the note inside.                                               |
| `tags`             | array   | no       | Manual tags applied on creation.                                                                    |
| `status`           | string  | no       | Status label applied on creation.                                                                   |

`color` and `height` apply to notes only and are ignored when `uri` is present.

### Sample request[​](#sample-request "Direct link to Sample request")

```
curl -X POST https://api.playbook.com/v1/coolclient-ltd/assets \

  -H "Authorization: Bearer ACCESS_TOKEN" \

  -H "Content-Type: application/json" \

  -d '{

    "asset": {

      "text": "Ship the palette by Friday",

      "title": "Deadline",

      "color": "#F4EFD9",

      "height": 320,

      "description": "from the kickoff call",

      "collection_token": "boardToken123"

    }

}'
```

### Sample response[​](#sample-response "Direct link to Sample response")

```
HTTP/1.1 200 OK

Content-Type: application/json



{

  "data": {

    "token": "assetToken123",

    "title": "Deadline",

    "text": "Ship the palette by Friday",

    "text_type": "note",

    "average_color": "#F4EFD9",

    "primary_height": 320,

    "description": "from the kickoff call",

    "collection_token": "boardToken123",

    "is_link": false,

    "is_skeleton": false

  }

}
```

The note's prose comes back in `text`, and `text_type` is `note`. The background colour is reported as `average_color` — the same field an image uses for its dominant colour.

### Note background colours[​](#note-background-colours "Direct link to Note background colours")

`color` is stored verbatim and is not validated, but only the note palette has a dark-mode counterpart. Playbook matches the stored string **exactly, including case**, against:

`#ffffff` · `#F1F3F7` · `#ECEAF2` · `#F4EBE0` · `#F4EFD9` · `#E6EEE7` · `#F1E5E5` · `#ECEAE6`

An off-palette colour is hard to read in dark mode

A note always switches to light text in dark mode, but its background is only darkened when the value matches one of the colours above. Send a brand colour like `#FFE8A3` — or a palette colour in the wrong case, like `#f4efd9` — and the note keeps its light background under light text. Stick to the list, copied exactly, unless you only care about light mode.

To edit a note later, `PATCH /v1/{slug}/assets/{token}` accepts `text` alongside the usual `title` and `description`.

***

## Create colour swatches[​](#create-colour-swatches "Direct link to Create colour swatches")

### Endpoint[​](#endpoint-1 "Direct link to Endpoint")

```
POST /v1/{slug}/assets/create_colors

Authorization: Bearer YOUR_TOKEN

Content-Type: application/json
```

### Request body[​](#request-body-1 "Direct link to Request body")

All fields live under a `colors` key.

| Field              | Type   | Required | Description                                                                                                                                          |
| ------------------ | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `color_codes`      | array  | yes      | 1–100 colour values. Each is a hex code, CMYK, a two-stop gradient, or a Pantone name — see below.                                                   |
| `titles`           | array  | no       | Titles positionally aligned with `color_codes`. A `null` or missing entry falls back to the code itself; an explicit `""` is kept as an empty title. |
| `collection_token` | string | no       | Board to add the swatches to. `board_token` is accepted as an alias.                                                                                 |
| `parent_token`     | string | no       | Merge the new swatches into this existing asset or group instead of forming a palette of their own.                                                  |

### What counts as a colour value[​](#what-counts-as-a-colour-value "Direct link to What counts as a colour value")

Four forms are accepted — the same four the web app can render — and anything else is rejected with `406` before a single swatch is created:

* **A hex code** — `#RRGGBB` or the three-digit shorthand `#RGB`, any casing. `#FF0000` and `#f00` both work; `FF0000` without the `#` does not.
* **CMYK** — `CMYK(0%, 100%, 100%, 0%)`: four numbers, percent signs optional.
* **A two-stop gradient** — two hex codes joined by the arrow `→` (U+2192), e.g. `#FF0000→#0000FF`. The gradient angle is not part of the value; the web app keeps it in the asset's title as `45°`.
* **A Pantone name** — written **without the `PANTONE `prefix** and matched exactly, including case, against Playbook's Pantone list: `185 C`, `185 CP`, `20-0001 TPM`.

The `PANTONE `prefix is the common mistake

`"PANTONE 185 C"` — the way the name is normally written — is **not** a valid value. The correct form is `"185 C"`. This is enforced rather than stored, because a swatch whose value resolves to no colour is created successfully, counts toward the board's asset total, and then renders as an empty slot with nothing to show.

Values are stored verbatim — Playbook does not rewrite a hex code's casing or expand a Pantone name into its hex on write. `text` reads back exactly what you sent.

### Sample request[​](#sample-request-1 "Direct link to Sample request")

```
curl -X POST https://api.playbook.com/v1/coolclient-ltd/assets/create_colors \

  -H "Authorization: Bearer ACCESS_TOKEN" \

  -H "Content-Type: application/json" \

  -d '{

    "colors": {

      "collection_token": "boardToken123",

      "color_codes": ["#FF0000", "#00FF00", "#0000FF"],

      "titles": ["Red", "Green", null]

    }

}'
```

### Sample response[​](#sample-response-1 "Direct link to Sample response")

```
HTTP/1.1 200 OK

Content-Type: application/json



{

  "data": {

    "assets": [

      { "token": "aA1", "title": "Red",     "text": "#FF0000", "text_type": "color", "is_group": false },

      { "token": "bB2", "title": "Green",   "text": "#00FF00", "text_type": "color", "is_group": false },

      { "token": "cC3", "title": "#0000FF", "text": "#0000FF", "text_type": "color", "is_group": false }

    ],

    "group": {

      "token": "dD4",

      "is_group": true,

      "text": "#FF0000|#00FF00|#0000FF",

      "text_type": "palette"

    }

  }

}
```

`assets` comes back in the order you sent the codes. The third swatch was given a `null` title, so it fell back to its own code.

### Palettes[​](#palettes "Direct link to Palettes")

Sending two or more codes merges them into a **palette**: a group asset whose `text_type` is `palette` and whose `text` is the member codes joined by `|`. You do not ask for a palette — grouping colour swatches produces one.

* **One code, no `parent_token`** → a single loose swatch, and `group` comes back `null`.
* **Two or more codes** → a palette, returned as `group`.
* **Any number of codes with `parent_token`** → the swatches are merged into that asset's group. If the group ends up containing anything that is not a colour, it is a plain group rather than a palette, and its `text_type` is `null`. Note that grouping moves the swatches onto the parent's board, whatever `collection_token` said.

To rearrange a palette after the fact — add more swatches to it, or break it apart — use the grouping endpoints described in [Advanced Asset Management](/docs/guides/asset_management.md): `POST /v1/{slug}/assets/group_assets` and `POST /v1/{slug}/assets/ungroup_assets`, exposed over MCP as `group_assets` and `ungroup_assets`.

One thing to know before ungrouping: **a group left with fewer than two assets is dissolved.** Its last asset is freed as well and the group asset itself is deleted, so ungrouping one swatch from a two-swatch palette leaves you with two loose swatches and no palette. The response lists only the assets you named, not every asset that changed — re-read the group if you need its state.

### Errors[​](#errors "Direct link to Errors")

Errors use the standard envelope, `{ "errors": [{ "message": "...", "extensions": { "code": "..." } }] }`.

| Status | Message                                 | Cause                                                                                           |
| ------ | --------------------------------------- | ----------------------------------------------------------------------------------------------- |
| 406    | `color_codes cannot be empty`           | `color_codes` was missing or an empty array.                                                    |
| 406    | `color_codes must be non-blank strings` | An entry was empty, whitespace, or not a string (a JSON number or boolean counts).              |
| 422    | `color_codes limit is 100`              | More than 100 codes in one call.                                                                |
| 403    | `Not authorized`                        | The token lacks the `write` scope, or you cannot write to the board.                            |
| 404    | `Not found`                             | `parent_token` does not name a live asset in this workspace — a trashed asset counts as absent. |

The whole call runs in one transaction, so a rejected batch leaves no partial palette behind.

Swatches do **not** count against your workspace's asset ceiling: that limit counts stored files, and a swatch has no file. A workspace at its file limit can still create colours.

***

## Reading notes and swatches back[​](#reading-notes-and-swatches-back "Direct link to Reading notes and swatches back")

Every asset endpoint reports text assets through the same three fields:

| Field           | Meaning                                                                                  |
| --------------- | ---------------------------------------------------------------------------------------- |
| `text_type`     | `note`, `color`, `palette`, or `null` for an ordinary file or link.                      |
| `text`          | The note's prose, the swatch's colour value, or a palette's member codes joined by `\|`. |
| `average_color` | A note's background colour. (For an image this is its dominant colour instead.)          |

Text assets have no bytes, so `display_url`, `thumbnails`, `size` and `media_type` are empty for them. Render a note or a swatch from `text` and `average_color`.

***

## An image's own colours[​](#an-images-own-colours "Direct link to An image's own colours")

Everything above is about colours somebody *authored*. An image also has colours of its own, and those are a separate field.

When Playbook renders an asset's preview it extracts the image's primary colours and stores them. Every asset endpoint returns them as `colors`: an array of `#RRGGBB` strings, most dominant first, up to six. These are the small swatches the web app draws underneath an asset.

```
{

  "token": "3EiEw6UGeLArdBVVxo4XDqQr",

  "media_type": "image/jpeg",

  "average_color": "#888579",

  "colors": ["#0B0B0B", "#2A3A2E", "#5A5648", "#A9A396", "#D8D5C6", "#8E8B7C"]

}
```

Three things worth knowing before you build on it:

* **`colors` is always an array, and it is empty for anything without a rendered preview** — notes, swatches, links, and file types that never produced one. An empty array means the image was never analysed, not that it has no colours. It never comes back `null`.
* **`colors` and `average_color` are not the same thing.** `average_color` is a single blended value, and on a note it carries the note's background colour instead. `colors` is the palette.
* **You cannot set `colors`.** It is derived at preview time and read-only; to put a colour on a board, create a swatch with `create_colors` as described above.

The two halves of this page compose: read `colors` from the images on a board, then post the codes you like back to that same board with `create_colors` to save the palette as real swatches. No image download and no colour analysis of your own is involved.

***

## Over MCP[​](#over-mcp "Direct link to Over MCP")

Both operations are available to AI assistants through the hosted [MCP server](/docs/guides/mcp.md), so you can create them in natural language instead of writing calls:

| Tool            | What it does                                                                           |
| --------------- | -------------------------------------------------------------------------------------- |
| `create_note`   | Add a note to a board, with optional background colour and card height. Needs `write`. |
| `create_colors` | Add 1–100 swatches; two or more come back merged into a palette. Needs `write`.        |

`create_colors` returns `assets`, `group` (null when no palette was formed) and `count`. `get_asset` carries `text`, `text_type` and `average_color`, so an assistant can read back what it created. The list and search tools return a leaner projection that keeps `text_type` — enough to tell a note from a swatch from a file — but not the `text` itself; fetch that with `get_asset`.

`get_asset` also carries the `colors` array described above, which is what lets an assistant build a palette from a board: list the board, read each image's `colors`, then hand the codes it picks to `create_colors`. Those colours are on the detail response only — list and search rows omit them, so ask for an asset by token.
