Branding
Portal and widget branding, custom CSS and import from your site.
Set up the SDK client used in the examples
API key or Dashboard session:
import { UpVoiced } from '@upvoiced/sdk';
const apiKey = process.env['UPVOICED_API_KEY']; // uv_live_… (Settings → API keys)
const workspace = process.env['UPVOICED_WORKSPACE_ID']; // sent as X-Workspace
if (!apiKey || !workspace) throw new Error('Set UPVOICED_API_KEY and UPVOICED_WORKSPACE_ID.');
const upvoiced = new UpVoiced({ apiKey, workspace });curl examples read $UPVOICED_API_KEY and $UPVOICED_WORKSPACE_IDfrom your shell. MCP examples are the tools/call request an MCP client sends to the MCP server with the same API key.
GET/v1/workspace/branding
Get branding
| Name | Type | Required |
|---|---|---|
x-workspace | string (uuid) | Yes |
Response 200: 32 fields
| Name | Type | Required |
|---|---|---|
preset | "ink" | "ocean" | "forest" | "citrus" | "plum" | "mono" | Yes |
accent | stringpattern ^#[0-9a-fA-F]{6}$ | Yes |
scheme | "light" | "dark" | "auto" | Yes |
font | "system" | "source-sans" | "inter" | "geist" | "manrope" | "source-serif" | "fraunces" | "ibm-plex" | "jetbrains-mono" | Yes |
radius | "none" | "sm" | "md" | "lg" | "full" | Yes |
density | "comfortable" | "compact" | Yes |
surface | "flat" | "elevated" | Yes |
show_powered_by | boolean | Yes |
layout | object | Yes |
layout.portal_nav | array of "board" | "roadmap" | "changelog" | "wall"items ≤ 4 | Yes |
layout.portal_header | "logo-left" | "centered" | "minimal" | Yes |
layout.post_card | "compact" | "comfortable" | Yes |
layout.roadmap_card | "title-only" | "with-excerpt" | Yes |
layout.changelog | "timeline" | "cards" | Yes |
layout.wall_default | "grid" | "masonry" | "carousel" | "list" | "spotlight" | Yes |
layout.widget_launcher | "icon" | "pill" | "tab" | Yes |
layout.widget_position | "bottom-right" | "bottom-left" | "right-middle" | "left-middle" | Yes |
logo | object | null | Yes |
logo.id | string (uuid) | Yes |
logo.url | string (uri) | Yes |
logo.mime | "image/png" | "image/svg+xml" | "image/jpeg" | "image/webp" | Yes |
logo.bytes | integervalue ≤ 2097152value > 0 | Yes |
favicon | object | null | Yes |
favicon.id | string (uuid) | Yes |
favicon.url | string (uri) | Yes |
favicon.mime | "image/png" | "image/svg+xml" | "image/jpeg" | "image/webp" | Yes |
favicon.bytes | integervalue ≤ 2097152value > 0 | Yes |
og_image | object | null | Yes |
og_image.id | string (uuid) | Yes |
og_image.url | string (uri) | Yes |
og_image.mime | "image/png" | "image/svg+xml" | "image/jpeg" | "image/webp" | Yes |
og_image.bytes | integervalue ≤ 2097152value > 0 | Yes |
Errors: 400, 401, 403, 404, 409, 413, 422, 429, 500, 503, all in the standard error shape.
curl 'https://api.upvoiced.com/v1/workspace/branding' \
-H "Authorization: Bearer $UPVOICED_API_KEY" \
-H "X-Workspace: $UPVOICED_WORKSPACE_ID"const data = await upvoiced.get('/v1/workspace/branding');No MCP tool for this operation. The MCP server covers the everyday reads and writes.
PUT/v1/workspace/branding
Update branding fields; plan-gated (owner/admin)
| Name | Type | Required |
|---|---|---|
x-workspace | string (uuid) | Yes |
| Name | Type | Required |
|---|---|---|
preset | "ink" | "ocean" | "forest" | "citrus" | "plum" | "mono" | No |
accent | stringpattern ^#[0-9a-fA-F]{6}$ | No |
scheme | "light" | "dark" | "auto" | No |
font | "system" | "source-sans" | "inter" | "geist" | "manrope" | "source-serif" | "fraunces" | "ibm-plex" | "jetbrains-mono" | No |
radius | "none" | "sm" | "md" | "lg" | "full" | No |
density | "comfortable" | "compact" | No |
surface | "flat" | "elevated" | No |
show_powered_by | boolean | No |
layout | object | No |
layout.portal_nav | array of "board" | "roadmap" | "changelog" | "wall"items ≤ 4 | Yes |
layout.portal_header | "logo-left" | "centered" | "minimal" | Yes |
layout.post_card | "compact" | "comfortable" | Yes |
layout.roadmap_card | "title-only" | "with-excerpt" | Yes |
layout.changelog | "timeline" | "cards" | Yes |
layout.wall_default | "grid" | "masonry" | "carousel" | "list" | "spotlight" | Yes |
layout.widget_launcher | "icon" | "pill" | "tab" | Yes |
layout.widget_position | "bottom-right" | "bottom-left" | "right-middle" | "left-middle" | Yes |
logo_asset_id | string (uuid) | null | No |
favicon_asset_id | string (uuid) | null | No |
og_image_asset_id | string (uuid) | null | No |
custom_css | stringlength ≤ 20480 | No |
Response 200: 32 fields
| Name | Type | Required |
|---|---|---|
preset | "ink" | "ocean" | "forest" | "citrus" | "plum" | "mono" | Yes |
accent | stringpattern ^#[0-9a-fA-F]{6}$ | Yes |
scheme | "light" | "dark" | "auto" | Yes |
font | "system" | "source-sans" | "inter" | "geist" | "manrope" | "source-serif" | "fraunces" | "ibm-plex" | "jetbrains-mono" | Yes |
radius | "none" | "sm" | "md" | "lg" | "full" | Yes |
density | "comfortable" | "compact" | Yes |
surface | "flat" | "elevated" | Yes |
show_powered_by | boolean | Yes |
layout | object | Yes |
layout.portal_nav | array of "board" | "roadmap" | "changelog" | "wall"items ≤ 4 | Yes |
layout.portal_header | "logo-left" | "centered" | "minimal" | Yes |
layout.post_card | "compact" | "comfortable" | Yes |
layout.roadmap_card | "title-only" | "with-excerpt" | Yes |
layout.changelog | "timeline" | "cards" | Yes |
layout.wall_default | "grid" | "masonry" | "carousel" | "list" | "spotlight" | Yes |
layout.widget_launcher | "icon" | "pill" | "tab" | Yes |
layout.widget_position | "bottom-right" | "bottom-left" | "right-middle" | "left-middle" | Yes |
logo | object | null | Yes |
logo.id | string (uuid) | Yes |
logo.url | string (uri) | Yes |
logo.mime | "image/png" | "image/svg+xml" | "image/jpeg" | "image/webp" | Yes |
logo.bytes | integervalue ≤ 2097152value > 0 | Yes |
favicon | object | null | Yes |
favicon.id | string (uuid) | Yes |
favicon.url | string (uri) | Yes |
favicon.mime | "image/png" | "image/svg+xml" | "image/jpeg" | "image/webp" | Yes |
favicon.bytes | integervalue ≤ 2097152value > 0 | Yes |
og_image | object | null | Yes |
og_image.id | string (uuid) | Yes |
og_image.url | string (uri) | Yes |
og_image.mime | "image/png" | "image/svg+xml" | "image/jpeg" | "image/webp" | Yes |
og_image.bytes | integervalue ≤ 2097152value > 0 | Yes |
Errors: 400, 401, 403, 404, 409, 413, 422, 429, 500, 503, all in the standard error shape.
curl -X PUT 'https://api.upvoiced.com/v1/workspace/branding' \
-H "Authorization: Bearer $UPVOICED_API_KEY" \
-H "X-Workspace: $UPVOICED_WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{
"preset": "ink"
}'const data = await upvoiced.put('/v1/workspace/branding', { body: { preset: 'ink' } });No MCP tool for this operation. The MCP server covers the everyday reads and writes.
GET/v1/workspace/branding/custom-css
Custom CSS draft, stored sanitized output and whether the plan applies it (owner/admin; Growth+)
| Name | Type | Required |
|---|---|---|
x-workspace | string (uuid) | Yes |
Response 200: 4 fields
| Name | Type | Required |
|---|---|---|
draft | stringlength ≤ 20480 | Yes |
css | stringlength ≤ 65536 | Yes |
entitled | boolean | Yes |
applied | boolean | Yes |
Errors: 400, 401, 403, 404, 409, 413, 422, 429, 500, 503, all in the standard error shape.
curl 'https://api.upvoiced.com/v1/workspace/branding/custom-css' \
-H "Authorization: Bearer $UPVOICED_API_KEY" \
-H "X-Workspace: $UPVOICED_WORKSPACE_ID"const data = await upvoiced.get('/v1/workspace/branding/custom-css');No MCP tool for this operation. The MCP server covers the everyday reads and writes.
POST/v1/workspace/branding/custom-css/lint
Lint a custom CSS draft with the parser-based sanitizer without saving it (owner/admin; Growth+)
| Name | Type | Required |
|---|---|---|
x-workspace | string (uuid) | Yes |
idempotency-key | stringpattern ^[A-Za-z0-9_.:-]{8,128}$ | No |
| Name | Type | Required |
|---|---|---|
css | stringlength ≤ 20480 | Yes |
Response 200: 10 fields
| Name | Type | Required |
|---|---|---|
ok | boolean | Yes |
bytes | integervalue ≥ 0 | Yes |
issues | array of objectitems ≤ 100 | Yes |
issues[].severity | "error" | "warning" | Yes |
issues[].code | "too_large" | "output_too_large" | "parse_error" | "import_not_allowed" | "at_rule_not_allowed" | "nesting_not_allowed" | "selector_not_allowed" | "selector_outside_root" | "attribute_selector_not_allowed" | "too_complex" | "property_not_allowed" | "value_not_allowed" | "function_not_allowed" | "expression_not_allowed" | "url_not_allowed" | "position_fixed_outside_root" | "focus_outline_removed" | "important_removed" | Yes |
issues[].message | stringlength 1–300 | Yes |
issues[].line | integervalue ≥ 1 | Yes |
issues[].column | integervalue ≥ 1 | Yes |
css | stringlength ≤ 65536 | Yes |
preview_css | stringlength ≤ 65536 | Yes |
Errors: 400, 401, 403, 404, 409, 413, 422, 429, 500, 503, all in the standard error shape.
curl -X POST 'https://api.upvoiced.com/v1/workspace/branding/custom-css/lint' \
-H "Authorization: Bearer $UPVOICED_API_KEY" \
-H "X-Workspace: $UPVOICED_WORKSPACE_ID" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"css": "Example css"
}'const data = await upvoiced.post('/v1/workspace/branding/custom-css/lint', { body: { css: 'Example css' } });No MCP tool for this operation. The MCP server covers the everyday reads and writes.
POST/v1/workspace/branding/reset
Reset branding to the Ocean defaults (owner/admin)
| Name | Type | Required |
|---|---|---|
x-workspace | string (uuid) | Yes |
idempotency-key | stringpattern ^[A-Za-z0-9_.:-]{8,128}$ | No |
Response 200: 32 fields
| Name | Type | Required |
|---|---|---|
preset | "ink" | "ocean" | "forest" | "citrus" | "plum" | "mono" | Yes |
accent | stringpattern ^#[0-9a-fA-F]{6}$ | Yes |
scheme | "light" | "dark" | "auto" | Yes |
font | "system" | "source-sans" | "inter" | "geist" | "manrope" | "source-serif" | "fraunces" | "ibm-plex" | "jetbrains-mono" | Yes |
radius | "none" | "sm" | "md" | "lg" | "full" | Yes |
density | "comfortable" | "compact" | Yes |
surface | "flat" | "elevated" | Yes |
show_powered_by | boolean | Yes |
layout | object | Yes |
layout.portal_nav | array of "board" | "roadmap" | "changelog" | "wall"items ≤ 4 | Yes |
layout.portal_header | "logo-left" | "centered" | "minimal" | Yes |
layout.post_card | "compact" | "comfortable" | Yes |
layout.roadmap_card | "title-only" | "with-excerpt" | Yes |
layout.changelog | "timeline" | "cards" | Yes |
layout.wall_default | "grid" | "masonry" | "carousel" | "list" | "spotlight" | Yes |
layout.widget_launcher | "icon" | "pill" | "tab" | Yes |
layout.widget_position | "bottom-right" | "bottom-left" | "right-middle" | "left-middle" | Yes |
logo | object | null | Yes |
logo.id | string (uuid) | Yes |
logo.url | string (uri) | Yes |
logo.mime | "image/png" | "image/svg+xml" | "image/jpeg" | "image/webp" | Yes |
logo.bytes | integervalue ≤ 2097152value > 0 | Yes |
favicon | object | null | Yes |
favicon.id | string (uuid) | Yes |
favicon.url | string (uri) | Yes |
favicon.mime | "image/png" | "image/svg+xml" | "image/jpeg" | "image/webp" | Yes |
favicon.bytes | integervalue ≤ 2097152value > 0 | Yes |
og_image | object | null | Yes |
og_image.id | string (uuid) | Yes |
og_image.url | string (uri) | Yes |
og_image.mime | "image/png" | "image/svg+xml" | "image/jpeg" | "image/webp" | Yes |
og_image.bytes | integervalue ≤ 2097152value > 0 | Yes |
Errors: 400, 401, 403, 404, 409, 413, 422, 429, 500, 503, all in the standard error shape.
curl -X POST 'https://api.upvoiced.com/v1/workspace/branding/reset' \
-H "Authorization: Bearer $UPVOICED_API_KEY" \
-H "X-Workspace: $UPVOICED_WORKSPACE_ID" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{}'const data = await upvoiced.post('/v1/workspace/branding/reset', { body: {} });No MCP tool for this operation. The MCP server covers the everyday reads and writes.
POST/v1/workspace/branding/import
Suggest colours, font, logo and favicon from a site via the SSRF-safe fetcher; writes nothing (owner/admin)
| Name | Type | Required |
|---|---|---|
x-workspace | string (uuid) | Yes |
idempotency-key | stringpattern ^[A-Za-z0-9_.:-]{8,128}$ | No |
| Name | Type | Required |
|---|---|---|
url | stringlength 3–2048 | Yes |
Response 200: 23 fields
| Name | Type | Required |
|---|---|---|
url | string (uri)length ≤ 2048 | Yes |
name | string | nulllength ≤ 80 | Yes |
accent | string | nullpattern ^#[0-9a-fA-F]{6}$ | Yes |
colors | array of objectitems ≤ 8 | Yes |
colors[].hex | stringpattern ^#[0-9a-fA-F]{6}$ | Yes |
colors[].sources | array of "theme-color" | "manifest-theme" | "manifest-background" | "mask-icon" | "tile-color" | "css-variable" | "css-button" | "css" | "icon" | "logo"items 1–10 | Yes |
colors[].neutral | boolean | Yes |
font | object | null | Yes |
font.font | "system" | "source-sans" | "inter" | "geist" | "manrope" | "source-serif" | "fraunces" | "ibm-plex" | "jetbrains-mono" | Yes |
font.family | stringlength ≤ 64 | Yes |
font.source | "css-body" | "css-variable" | "css-heading" | "font-face" | "css" | Yes |
detected_fonts | array of stringitems ≤ 8 | Yes |
assets | array of objectitems ≤ 8 | Yes |
assets[].url | string (uri)length ≤ 2048 | Yes |
assets[].kind | "logo-img" | "apple-touch-icon" | "manifest-icon" | "mask-icon" | "icon" | "og-image" | Yes |
assets[].roles | array of "logo" | "favicon"items 1–2 | Yes |
assets[].mime | "image/png" | "image/svg+xml" | "image/jpeg" | "image/webp" | Yes |
assets[].bytes | integervalue ≤ 2097152value > 0 | Yes |
assets[].width | integer | nullvalue > 0 | Yes |
assets[].height | integer | nullvalue > 0 | Yes |
assets[].sha256 | stringpattern ^[0-9a-f]{64}$ | Yes |
assets[].preview | string | nulllength ≤ 800000pattern ^data:image\/(png|jpeg|webp|svg\+xml);base64,[A-Za-z0-9+/=]+$ | Yes |
warnings | array of "manifest_unavailable" | "stylesheet_unavailable" | "stylesheets_truncated" | "asset_unavailable" | "svg_rejected" | "unsupported_image" | "deadline_reached"items ≤ 10 | Yes |
Errors: 400, 401, 403, 404, 409, 413, 422, 429, 500, 503, all in the standard error shape.
curl -X POST 'https://api.upvoiced.com/v1/workspace/branding/import' \
-H "Authorization: Bearer $UPVOICED_API_KEY" \
-H "X-Workspace: $UPVOICED_WORKSPACE_ID" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/upvoiced"
}'const data = await upvoiced.post('/v1/workspace/branding/import', { body: { url: 'https://example.com/upvoiced' } });No MCP tool for this operation. The MCP server covers the everyday reads and writes.
POST/v1/workspace/branding/import/asset
Re-fetch a chosen logo/favicon, validate or sanitize it and store it as a ready branding asset (owner/admin)
| Name | Type | Required |
|---|---|---|
x-workspace | string (uuid) | Yes |
idempotency-key | stringpattern ^[A-Za-z0-9_.:-]{8,128}$ | No |
| Name | Type | Required |
|---|---|---|
url | stringlength 8–2048 | Yes |
role | "logo" | "favicon" | Yes |
sha256 | stringpattern ^[0-9a-f]{64}$ | No |
Response 201: 4 fields
| Name | Type | Required |
|---|---|---|
id | string (uuid) | Yes |
url | string (uri) | Yes |
mime | "image/png" | "image/svg+xml" | "image/jpeg" | "image/webp" | Yes |
bytes | integervalue ≤ 2097152value > 0 | Yes |
Errors: 400, 401, 403, 404, 409, 413, 422, 429, 500, 503, all in the standard error shape.
curl -X POST 'https://api.upvoiced.com/v1/workspace/branding/import/asset' \
-H "Authorization: Bearer $UPVOICED_API_KEY" \
-H "X-Workspace: $UPVOICED_WORKSPACE_ID" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/upvoiced",
"role": "logo"
}'const data = await upvoiced.post('/v1/workspace/branding/import/asset', {
body: { url: 'https://example.com/upvoiced', role: 'logo' },
});No MCP tool for this operation. The MCP server covers the everyday reads and writes.