Brand kits (REST)
Full CRUD-ish lifecycle for brand kits. All endpoints under/v1/brand-kits.
Endpoints
Default kit
The first brand kit created for a team becomes the default automatically.POST /v1/tasks with no brand_kit_id uses the default. At most one default per team. Promote another kit with PATCH /brand-kits/{id} and {"is_default": true} — it clears whichever kit held the default, in the same transaction as any field writes in the same body, so a correct-and-promote cannot half-land. is_default: false is a 400: demoting without naming a replacement would leave the team with no default at all, so promote the replacement instead. Omit the field to leave the flag alone.
List
default_theme_canvas_id is the prefixed cvs_… ID of the kit’s saved default slides theme canvas (null if none). Returned on reads and writable via PATCH (see below). Fresh-canvas design tasks auto-apply it when present, and so does a blank POST /v1/canvases with category: "slides" and this kit’s brand_kit_id — the create response reports the attached theme as theme_canvas_id (null when the kit has no usable theme; the create still succeeds).
Cursor-paginated; iterate with ?cursor=. See pagination.md.
Create from URL
stripe.com) or full URL (https://stripe.com). Cached — re-calling with the same URL returns the cached result quickly.
If the URL can’t be scraped (blocked by robots, 404, login-required): 422 scraping_user_error.
Update (partial)
colors overwrites the entire array — not a merge. To add one color, fetch the existing array, append, and PATCH the full list:
fonts, brand_values, brand_aesthetic, brand_tone_of_voice.
Setting the default theme canvas. PATCH default_theme_canvas_id with a cvs_… ID (a bare UUID is also accepted) to point the kit at a slides theme canvas; send null to clear it:
template_type='theme' on the same team — anything else returns 400 invalid_theme_canvas. Omitting the field leaves the current value unchanged.
Promoting the team default. PATCH is_default: true to make this the team’s default kit, clearing whichever kit held it. It applies in the same transaction as any field writes in the same body, so correcting a value and promoting the kit is one call that cannot half-land:
is_default: false returns 400. Demoting without naming a replacement leaves the team with no default at all, so promote the replacement kit instead. Omitting the field leaves the flag unchanged.
Returns the updated record.
Add images (logos / references / assets)
file_id from POST /v1/uploads. Attaches the uploaded file to the kit in the named role. Role here is about the kit (what this image represents in the brand) — different from the role field on task attachments (source / reference / asset / import).
Returns the updated brand-kit record.
List and remove images
total always equals returned.
Read-back roles are logo or reference only. logo rows come from the kit’s logos; reference rows come from the design-assets bucket, which is shared by the reference and asset add-time roles — so an image attached with role: "asset" reads back as reference. Filtering the list for role == "asset" always returns nothing.
image_id is the id field (bki_…) from GET /brand-kits/{id}/images — not file_id, and not anything in the add-image response, which returns the whole kit record without image IDs. Returns 204 No Content. A repeat DELETE on the same id returns 404 rather than erroring; a wrong-prefix id (e.g. a file_ one) fails path validation with 422 before the handler runs.
Delete
204 No Content. Soft delete — historical tasks that referenced this kit continue to show it in their audit record. New design tasks with brand_kit_id pointing at a deleted kit get 404 not_found.
If the deleted kit was the team’s default, there is no new default automatically. The team operates brand-kit-less until another kit is promoted (PATCH /brand-kits/{id} with {"is_default": true}) or a new kit is created.
In-task usage
Passbrand_kit_id explicitly on POST /v1/tasks to override the default:
skip_brand_kit: true to apply no kit:
skip_brand_kit overrides brand_kit_id when both are present.
Common wrong guesses
- Merging array updates client-side without reading first. You’ll overwrite the kit’s colors / fonts with just the new entries. Always read, append, PATCH.
- Sending
is_default: falseto clear a default.400. The field is promote-only — setis_default: trueon the kit you want instead, which clears the incumbent for you. - Expecting
POST /v1/brand-kitsto return immediately. The call is synchronous: it returns the completed kit, but blocks for 10–30s — budget for that in your client timeout. - Passing
brand_kit_idfor a deleted kit.404. Check the kit exists before submitting the task. - Filtering the images list for
role: "asset". Reads only ever returnlogoorreference—assetandreferenceshare one bucket, soassetattachments come back asreference. - Passing a
file_id toDELETE /brand-kits/{id}/images/{image_id}. The path wants thebki_id fromGET /brand-kits/{id}/images; afile_id fails path validation with422.404is the answer for a well-formedbki_id that isn’t attached. - Mixing up the task-attachment
roleand the brand-kit-imagerole. Taskroleissource/reference/asset/import. Brand-kit imageroleislogo/reference/asset. Similar words, different values. - Pointing
default_theme_canvas_idat a deck instead of a theme. It must be atemplate_type='theme'canvas on the same team, not a regular design/deck canvas. Anything else is400 invalid_theme_canvas.
Upstream
docs.moda.app/api/brand-kits/listBrandKitsdocs.moda.app/api/brand-kits/createBrandKitdocs.moda.app/api/brand-kits/updateBrandKitdocs.moda.app/api/brand-kits/addBrandKitImagedocs.moda.app/api/brand-kits/listBrandKitImagesdocs.moda.app/api/brand-kits/removeBrandKitImagedocs.moda.app/api/brand-kits/deleteBrandKit