Brand kit
One logo, five colour roles, two font names and one call to action, for every render that ends on your brand.
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 |
| Replace the kit | PUT /v1/brand-kit | api:write | 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
PUTalways 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.
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.
Read the kit
curl https://api.roasos.com/v1/brand-kit \
-H "Authorization: Bearer roasos_sk_..."{
"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 that produced the kit, when one produced it.
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.
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.
Replace the kit
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"
}'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.
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 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
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.
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. A brand extraction 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 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).
Next
- Teach the agent what to say: Memory.
- See what roasOS compiled: Intelligence documents.