Outcome
Upload or replace bytes for a registered Brand asset.
Before you start
- Verify whoami and the pinned credential context, then inspect the exact discovered action before preparing input.
- Discover a granted Brand workspace. Pass its resourceType/workspace identifier separately from input; a copied fixture identifier grants no access.
- The declared permission is brand:write. The active credential, application scope, resource binding and runtime authority must all authorize the request.
- Review the exact mutation and recovery, including destructive scope, and execute only after the intended effect is authorized.
Discover the target
topolo whoami --json
topolo actions get app_topolo_brand.brand_assets.upload --detail --json
topolo resources topolo-brand --json
topolo workspaces list topolo-brand --json
Request
PUT /api/brand-assets/{assetId}/content
Resource context is the selected Brand workspace. CLI/SDK action input includes path identifiers; direct HTTP JSON bodies exclude path parameters. Read the reviewed plan for the exact request.
Input fields
| Field | Required | Meaning |
|---|---|---|
assetId | Yes | Registered Brand asset identifier. Upload accepts pending or ready assets; download requires stored ready content. |
contentBase64 | Yes | Canonical base64 encoding of non-empty bytes, at most 25 MiB after decoding. |
contentType | Yes | Uploaded MIME type; must match the registered asset MIME type. |
fileName | Yes | Non-empty source filename supplied with the upload envelope. |
Exact published input schema:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"assetId": {
"type": "string",
"minLength": 1,
"description": "Registered Brand asset identifier. Upload accepts pending or ready assets; download requires stored ready content."
},
"contentBase64": {
"type": "string",
"minLength": 1,
"pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$",
"description": "Canonical base64 encoding of non-empty bytes, at most 25 MiB after decoding."
},
"contentType": {
"type": "string",
"minLength": 1,
"description": "Uploaded MIME type; must match the registered asset MIME type."
},
"fileName": {
"type": "string",
"minLength": 1,
"description": "Non-empty source filename supplied with the upload envelope."
}
},
"required": [
"assetId",
"contentBase64",
"contentType",
"fileName"
],
"additionalProperties": false
}
Effects
Writes non-empty content up to 25 MiB into the registered object key and marks the asset ready with an audit. An archived or missing asset is refused.
Interpret the response
asset is the ready metadata record. contentType must match registered MIME type and contentBase64 must decode correctly. The input filename is accepted by the envelope; the canonical storage key belongs to the registered record. A storage failure returns 502 and does not prove readiness.
Response identifier contract:
{
"kind": "brand_asset",
"identifierSource": "response",
"idPath": "$.asset.id"
}
Worked examples
These complete synthetic responses were captured from the local worker over canonical SQL migrations. Auth and R2 dependencies are simulated; notification delivery is not exercised. Set BRAND_WORKSPACE_ID from granted workspace discovery. Save reviewed input as payload.json, validate and inspect the unchanged plan. The final mutation command and MCP confirm flag assume the intended effect has already been authorized.
Minimal example
Input · payload.json:
{
"assetId": "brand_asset_dd192c95-76da-4c2e-a61c-8b164686806b",
"contentBase64": "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHJlY3Qgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2Ii8+PC9zdmc+",
"contentType": "image/svg+xml",
"fileName": "fixture.svg"
}
CLI:
topolo actions validate app_topolo_brand.brand_assets.upload --data @payload.json --json
topolo actions plan app_topolo_brand.brand_assets.upload --resource-type workspace --resource-id "$BRAND_WORKSPACE_ID" --data @payload.json --json
topolo actions call app_topolo_brand.brand_assets.upload --resource-type workspace --resource-id "$BRAND_WORKSPACE_ID" --data @payload.json --confirm --json
MCP tool and arguments:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_brand.brand_assets.upload",
"input": {
"assetId": "brand_asset_dd192c95-76da-4c2e-a61c-8b164686806b",
"contentBase64": "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHJlY3Qgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2Ii8+PC9zdmc+",
"contentType": "image/svg+xml",
"fileName": "fixture.svg"
},
"resourceType": "workspace",
"resourceId": "<identifier returned by resource discovery>",
"confirm": true
}
}
Captured local response · HTTP 200:
{
"asset": {
"id": "brand_asset_dd192c95-76da-4c2e-a61c-8b164686806b",
"brandKitId": "brand_c794be50-0306-4933-8de8-5609340bed9e",
"kind": "logo",
"name": "Fixture logo",
"mimeType": "image/svg+xml",
"storageKey": "org_documentation_fixture/brand_c794be50-0306-4933-8de8-5609340bed9e/brand_asset_dd192c95-76da-4c2e-a61c-8b164686806b",
"license": "Fixture-only artwork",
"altText": "Fixture identity",
"status": "ready",
"createdAt": "2026-10-03T11:23:59.064Z",
"updatedAt": "2026-10-03T11:23:59.067Z"
}
}
asset is the ready metadata record. contentType must match registered MIME type and contentBase64 must decode correctly. The input filename is accepted by the envelope; the canonical storage key belongs to the registered record. A storage failure returns 502 and does not prove readiness.
Workflow example
Input · payload.json:
{
"assetId": "brand_asset_87bffee4-7249-419b-b4c1-8edeeb732d55",
"contentBase64": "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHJlY3Qgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2Ii8+PC9zdmc+",
"contentType": "image/svg+xml",
"fileName": "fixture.svg"
}
CLI:
topolo actions validate app_topolo_brand.brand_assets.upload --data @payload.json --json
topolo actions plan app_topolo_brand.brand_assets.upload --resource-type workspace --resource-id "$BRAND_WORKSPACE_ID" --data @payload.json --json
topolo actions call app_topolo_brand.brand_assets.upload --resource-type workspace --resource-id "$BRAND_WORKSPACE_ID" --data @payload.json --confirm --json
MCP tool and arguments:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_brand.brand_assets.upload",
"input": {
"assetId": "brand_asset_87bffee4-7249-419b-b4c1-8edeeb732d55",
"contentBase64": "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHJlY3Qgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2Ii8+PC9zdmc+",
"contentType": "image/svg+xml",
"fileName": "fixture.svg"
},
"resourceType": "workspace",
"resourceId": "<identifier returned by resource discovery>",
"confirm": true
}
}
Captured local response · HTTP 200:
{
"asset": {
"id": "brand_asset_87bffee4-7249-419b-b4c1-8edeeb732d55",
"brandKitId": "brand_55886012-13a4-4e95-ad31-3fe2b52cdf20",
"kind": "logo",
"name": "Fixture logo",
"mimeType": "image/svg+xml",
"storageKey": "org_documentation_fixture/brand_55886012-13a4-4e95-ad31-3fe2b52cdf20/brand_asset_87bffee4-7249-419b-b4c1-8edeeb732d55",
"license": "Fixture-only artwork",
"altText": "Fixture identity",
"status": "ready",
"createdAt": "2026-10-03T11:23:59.106Z",
"updatedAt": "2026-10-03T11:23:59.107Z"
}
}
asset is the ready metadata record. contentType must match registered MIME type and contentBase64 must decode correctly. The input filename is accepted by the envelope; the canonical storage key belongs to the registered record. A storage failure returns 502 and does not prove readiness.
workflow email logo example
Input · payload.json:
{
"assetId": "brand_asset_dce3c24f-65dd-4579-913f-9ffbf74619c9",
"contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aM1cAAAAASUVORK5CYII=",
"contentType": "image/png",
"fileName": "fixture-email.png"
}
CLI:
topolo actions validate app_topolo_brand.brand_assets.upload --data @payload.json --json
topolo actions plan app_topolo_brand.brand_assets.upload --resource-type workspace --resource-id "$BRAND_WORKSPACE_ID" --data @payload.json --json
topolo actions call app_topolo_brand.brand_assets.upload --resource-type workspace --resource-id "$BRAND_WORKSPACE_ID" --data @payload.json --confirm --json
MCP tool and arguments:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_brand.brand_assets.upload",
"input": {
"assetId": "brand_asset_dce3c24f-65dd-4579-913f-9ffbf74619c9",
"contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aM1cAAAAASUVORK5CYII=",
"contentType": "image/png",
"fileName": "fixture-email.png"
},
"resourceType": "workspace",
"resourceId": "<identifier returned by resource discovery>",
"confirm": true
}
}
Captured local response · HTTP 200:
{
"asset": {
"id": "brand_asset_dce3c24f-65dd-4579-913f-9ffbf74619c9",
"brandKitId": "brand_55886012-13a4-4e95-ad31-3fe2b52cdf20",
"kind": "logo",
"name": "Fixture email logo",
"mimeType": "image/png",
"storageKey": "org_documentation_fixture/brand_55886012-13a4-4e95-ad31-3fe2b52cdf20/brand_asset_dce3c24f-65dd-4579-913f-9ffbf74619c9",
"license": null,
"altText": "Fixture email identity",
"status": "ready",
"createdAt": "2026-10-03T11:23:59.114Z",
"updatedAt": "2026-10-03T11:23:59.114Z"
}
}
asset is the ready metadata record. contentType must match registered MIME type and contentBase64 must decode correctly. The input filename is accepted by the envelope; the canonical storage key belongs to the registered record. A storage failure returns 502 and does not prove readiness.
Verify the result
Require status:ready, download the same identifier and compare bytes or an independently computed hash. Published frozen copies belong to exact versions; replacing original bytes does not guarantee a previously frozen publication changes.
Errors and recovery
refused · HTTP 404
{
"input": {
"assetId": "missing-asset",
"contentBase64": "eA==",
"contentType": "image/png",
"fileName": "fixture.png"
},
"response": {
"error": "brand_asset_not_found"
}
}
invalid base64 · HTTP 400
{
"input": {
"assetId": "brand_asset_24d8ac1c-77dd-466e-94f0-8cd0178d9314",
"contentBase64": "!!!!",
"contentType": "image/png",
"fileName": "fixture.png"
},
"response": {
"error": "invalid_base64_content"
}
}
mime mismatch · HTTP 415
{
"input": {
"assetId": "brand_asset_24d8ac1c-77dd-466e-94f0-8cd0178d9314",
"contentBase64": "eA==",
"contentType": "image/jpeg",
"fileName": "fixture.jpg"
},
"response": {
"error": "content_type_mismatch",
"expected": "image/png"
}
}
-
auth_error(HTTP 401): CLI/SDK authentication classification when Auth returns a non-code message: restore the authorized credential, then verify whoami and context. Preserve the exact service message. -
permission_denied(HTTP 403): Use the exact permission and granted resource required by the reviewed action; organization membership alone is insufficient. -
workspace_required(HTTP 400): Discover and select a granted Brand workspace before preparing the request. -
brand_storage_unavailable(HTTP 503): The Brand storage bindings are unavailable. Preserve the request identifier and report the service fault; do not retry an unverified mutation. -
brand_asset_not_found(HTTP 404): Discover the exact parent/resource again and inspect its lifecycle status. Missing, inaccessible or immutable resources can share this refusal. -
invalid_base64_content(HTTP 400): Encode the complete non-empty bytes using canonical base64 and validate before retrying. -
content_type_mismatch(HTTP 415): Match contentType to the registered MIME type or register the intended asset metadata separately. -
brand_asset_too_large(HTTP 413): Use no more than 25 MiB of decoded bytes. -
brand_asset_processing_failed(HTTP 502): Preserve the result and read asset state/content before retrying the approved upload; stored bytes and SQL readiness are separate.
Recovery boundary
If replacing content, retain the prior bytes and metadata and separately approve an upload restoring those bytes. Check download after an unknown outcome before retrying.
Related actions
app_topolo_brand.brand_assets.download: Download authenticated content for a ready Brand asset.app_topolo_brand.brand_kit_versions.create: Create a draft Brand version from a complete typed snapshot.
Evidence
Four real local SQLite stores use all canonical migrations; Auth identity/grants and archival plus R2 object storage are explicitly simulated. Notifications are disabled in fixtures. No notification delivery, live mutation, gateway credential parity or deployed environment proof is claimed. Fixture identifiers and timestamps are synthetic and must be replaced by exact authorized resource discovery.