---
title: "Brand kit"
description: "One logo, five colour roles, two font names and one call to action, for every render that ends on your brand."
url: https://docs.roasos.com/brand/brand-kit
---

# Brand kit

The brand kit holds the look of your brand. One workspace has one kit, so the path names no id.

| Operation       | Path                | Scope       | Reference                                                   |
| --------------- | ------------------- | ----------- | ----------------------------------------------------------- |
| Read the kit    | `GET /v1/brand-kit` | `api:read`  | [`getBrandKit`](/api-reference/brand-kit/getBrandKit)       |
| Replace the kit | `PUT /v1/brand-kit` | `api:write` | [`updateBrandKit`](/api-reference/brand-kit/updateBrandKit) |

Both operations cost **0 credits**.

## How the kit is made

roasOS makes the kit for you. You do not have to do an action.

* After onboarding, roasOS reads the website you gave. It finds the logo, the colours and the font names, and it saves them as the kit. This costs 0 credits.
* If a workspace has no kit, the agent starts the same read. In the app, **Settings → Brand** also shows **Create brand kit**.
* roasOS never replaces a kit that is already there. A kit that you write with `PUT` always stays.

## Products from other brands

A product that you import from a different brand gets the kit of that brand. roasOS reads the logo, the colours and the fonts from the product page. Ads for that product use the product brand, not your workspace kit.

<Callout type="warn">
  A 0-credit door never returns a quote envelope. It needs no `Idempotency-Key`, it accepts no `quote_only`, and it accepts no `quote_id`.
</Callout>

## Read the kit

```bash
curl https://api.roasos.com/v1/brand-kit \
  -H "Authorization: Bearer roasos_sk_..."
```

```json
{
  "data": {
    "schema_version": "brand-kit@1",
    "kind": "brand_kit",
    "logo_url": "https://cdn.roasos.com/uploads/8f1c.../images/logo-light.png",
    "logo_dark_url": null,
    "colors": {
      "primary": "#4d96ff",
      "secondary": "#0b1f3a",
      "text": "#111111"
    },
    "fonts": { "heading": "Inter", "body": "Inter" },
    "cta_text": "Shop now",
    "cta_url": "https://www.acme.com/shop",
    "source_analysis_id": null,
    "updated_by": "5f2b91cc-...",
    "created_at": "2026-08-21T10:12:00.000Z",
    "updated_at": "2026-08-21T10:12:00.000Z"
  }
}
```

Each timestamp is UTC and ends with `Z`.

`source_analysis_id` is the provenance field. It names the [brand extraction](/workflows/extract-a-brand) that produced the kit, when one produced it.

<Callout>
  No door writes a value into `source_analysis_id` in this release. An extraction writes an analysis and the brand candidates behind it; it does not write your kit. Every kit therefore reads `source_analysis_id: null` today, and each `PUT` writes `null` again, because a person wrote the row.
</Callout>

<Callout type="warn">
  A workspace that never wrote a kit answers `404 brand_kit_not_found`. roasOS puts no empty kit in its place. Send `PUT /v1/brand-kit` to make the kit; the same call creates it and replaces it.
</Callout>

## Replace the kit

```bash
curl -X PUT https://api.roasos.com/v1/brand-kit \
  -H "Authorization: Bearer roasos_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "logo_url": "https://cdn.roasos.com/uploads/8f1c.../images/logo-light.png",
    "colors": { "primary": "#4d96ff", "text": "#111111" },
    "fonts": { "heading": "Inter", "body": "Inter" },
    "cta_text": "Shop now",
    "cta_url": "https://www.acme.com/shop"
  }'
```

<Callout type="warn">
  `PUT` replaces the **whole** kit. A field that you omit is cleared. There is no `PATCH`. To change one colour, send the kit that you want, with every value that you keep.
</Callout>

An empty body is legal on this door. `{}` clears every field of the kit and keeps the row. A field set to `null` reads the same as a field you omit: it clears that part of the kit. That holds for the six top-level fields and for each colour role and font slot. The [marketing brief](/brand/memory#replace-the-brief) has the opposite rule, so read each page before you write.

Read the kit first, change the object, then send it back. That is the safe way to make one small change.

## The fields

| Field           | Rule                                                                          |
| --------------- | ----------------------------------------------------------------------------- |
| `logo_url`      | a `https://cdn.roasos.com` object that your workspace owns                    |
| `logo_dark_url` | the same rule, for a dark background                                          |
| `colors`        | five roles at most: `primary`, `secondary`, `accent`, `background` and `text` |
| `fonts`         | two slots at most: `heading` and `body`                                       |
| `cta_text`      | 1 to 40 characters, and no control character                                  |
| `cta_url`       | one public `https` URL, 2,048 characters at most, with no space in it         |

roasOS resolves the host of `cta_url`. A URL that targets an internal network is `400 validation_error`, as it is on every other door that takes a website URL.

### Colours

A colour is a six-digit hex value, for example `#4d96ff`. roasOS stores it in lower case. You may send `#4D96FF`, and the kit reads back `#4d96ff`.

A value that is not six-digit hex is `400 validation_error`. `blue` is one example. The database holds the same rule in lower case, so a bad value cannot enter the kit by any path.

The role set is closed. There is no sixth role.

### Fonts store a name, and never a file

<Callout type="warn">
  roasOS hosts no font file, and it embeds none. A font slot holds a **name**, for example `Inter`. When roasOS renders your brand later, it maps that name onto a font that it bundles. The render therefore shows a similar face, and not always your exact licensed face. This is a limit of the product today.
</Callout>

A font name is 1 to 64 characters. It starts with a letter or a digit. After that first character, use letters, digits, spaces, dots, underscores and hyphens. The pattern excludes a slash and a colon, so a URL can never enter a font slot. A value outside the pattern is `400 validation_error`.

### The logo is media, and never a remote URL

Each logo must be an immutable object on `https://cdn.roasos.com` that your workspace owns. roasOS proves each key before it writes the row, and it proves `logo_url` and `logo_dark_url` separately. A valid light logo does not admit an invalid dark logo.

roasOS admits two key prefixes, and your own workspace id stands in each one.

```
uploads/<workspaceId>/...
workspaces/<workspaceId>/...
```

A Discover creative belongs to the advertiser that ran it. You may analyze one. You may never make it your logo.

Put your own file on the CDN with `POST /v1/uploads`. Refer to [Reference media and uploads](/concepts/reference-media). A [brand extraction](/workflows/extract-a-brand) mirrors the logo of your site into your workspace storage for you, so its URL is ready to send here.

A logo that the extractor mirrors has no `/v1/uploads` record. `GET /v1/uploads/{id}` finds no entry for it, because roasOS wrote the object directly. The CDN media key in the brand kit is the only reference to that object. Deletion of your workspace removes the object through the brand kit and through the analysis record that mirrored it.

## Errors

| Code                                  | Status | Cause                                                                                                 |
| ------------------------------------- | ------ | ----------------------------------------------------------------------------------------------------- |
| `validation_error`                    | 400    | the body breaks a rule above                                                                          |
| `discover_source_not_allowed`         | 400    | the URL is a [Discover](/api-reference/discover/listDiscoverItems) creative; roasOS checks this first |
| `reference_media_not_workspace_owned` | 400    | the URL is not an object that your workspace owns                                                     |
| `brand_kit_not_found`                 | 404    | your workspace has no kit yet; write one with `PUT`                                                   |
| `brand_kit_store_unavailable`         | 503    | the store did not answer, or the answer did not match the contract                                    |

Each `400` above names the field that it refused, in `error.field`. A kit with two logos proves each one on its own, so the field tells you which logo to correct.

`brand_kit_store_unavailable` publishes `retryable: false`. It is a real published shape and not a transient, so a retry loop is the wrong answer to it.

## From an agent

| Tool                      | Operation           |
| ------------------------- | ------------------- |
| `roasos_get_brand_kit`    | `GET /v1/brand-kit` |
| `roasos_update_brand_kit` | `PUT /v1/brand-kit` |

The replace rule does not change for a connected model. Refer to [Connect an AI agent (MCP)](/mcp).

## Next

* Teach the agent what to say: [Memory](/brand/memory).
* See what roasOS compiled: [Intelligence documents](/brand/intelligence).
