Set a brand's logo or icon
The brands.images.set operation: PUT /api/v1/brands/{id}/images/{variant} in the REST API and the brands_images_set tool in the MCP server.
/api/v1/brands/{id}/images/{variant}Uploads a brand's logo or icon. The icon is the square mark checkout shows next to the brand's name, and the
brand's image in the console; the logo is the wide mark the customer portal shows. Send the image
base64-encoded in content: PNG, JPEG or WebP of at most 512 KB and 1024 pixels a side, the icon square and the
logo between square and four times as wide as it is tall. The type is read from the bytes; SVG and GIF are
refused.
An uploaded image wins over the typed branding.logoUrl / iconUrl (brands.update), which keep working when
nothing is uploaded. Brand images are public: customers see them on checkout and the portal, so they are served
to anyone at branding.logoFile.url / iconFile.url once the brand is in test or live mode. Uploading the same
image again changes nothing. Needs brands:write. Dry-run it to check an image without storing it.
At a glance
- REST:
PUT /api/v1/brands/{id}/images/{variant} - MCP tool:
brands_images_set - Operation ID:
brands.images.set - Scope: Organization, or one of its legal entities or brands
- Permission:
brands:write - Risk tier:
write - Dry run: Supported (send
Ownrecord-Dry-Run: true) - Idempotency:
Idempotency-Keyoptional - Modes: Test and live
- Authentication: API key with the
brands:writescope - Since: M13
Request
Path parameters
| Field | Type | Description |
|---|---|---|
id | brd_… ID | |
variant | one of logo, icon | logo: the wide mark on the portal (and later invoices and emails); icon: the square mark on checkout. |
Body
A JSON object (Content-Type: application/json):
| Field | Type | Required | Description |
|---|---|---|---|
content | string | Yes | The image's bytes, base64-encoded: PNG, JPEG or WebP of at most 512 KB and 1024 pixels a side. An icon is square (256 × 256 from the console); a logo is between square and four times as wide as it is tall (within 600 × 200 from the console). |
Response
200 OK with a JSON object:
| Field | Type | Description |
|---|---|---|
id | brd_… ID | |
object | "brand" | |
orgId | org_… ID | |
entityId | ent_… ID | |
slug | string | |
name | string | |
status | one of draft, test_only, live, suspended | |
isPlatformBrand | boolean | |
branding | object | |
branding.logoUrl | string or null | |
branding.iconUrl | string or null | |
branding.logoFile | object or null | |
branding.iconFile | object or null | |
branding.primaryColor | string or null | |
branding.accentColor | string or null | |
branding.supportEmail | string or null | |
branding.supportUrl | string or null | |
branding.termsUrl | string or null | |
branding.privacyUrl | string or null | |
statementDescriptor | string | |
statementDescriptorSuffix | string or null | |
invoiceSeries | object | |
invoiceSeries.invoice | string | |
invoiceSeries.creditNote | string | |
billingSettings | object | |
billingSettings.dunning | object | |
billingSettings.dunning.retryOffsetsDays | array of integer | |
billingSettings.dunning.finalAction | one of unpaid, canceled | |
billingSettings.entitlementGraceDays | integer | |
billingSettings.sendInvoice | object | |
billingSettings.sendInvoice.graceDays | integer | |
billingSettings.cardExpiryReminders | boolean | |
billingSettings.pausedEntitlements | boolean | |
billingSettings.paymentBehavior | one of allow_incomplete, pending_if_incomplete | |
policies | object | |
policies.breakageRecognition | one of at_expiry, manual | at_expiry (the default) or manual (held as expired credits until recognised). |
policies.supportRefundLimits | array of object | The support role's refund limit per currency (M8); a currency not listed has the default of 100 of it. |
policies.supportRefundLimits[].amount | string | Integer micros as a string. |
policies.supportRefundLimits[].currency | string | |
policies.disputeWalletFreeze | boolean | Freeze the customer's wallets when a dispute opens (default true; §19.2, M8). |
policies.taxIdUnavailable | one of charge_as_b2c, block | What a sale does when a tax ID couldn't be checked (§9.1 step 1; M13): charge_as_b2c (the default) or block. |
portalSettings | object | |
portalSettings.allowPlanChanges | boolean | Customers may switch plans along planChangeMatrix (no row: no switch). |
portalSettings.planChangeMatrix | array of object | One row per price: a subscription item on fromPriceId may switch to any of toPriceIds (same mode and currency). |
portalSettings.planChangeMatrix[].fromPriceId | price_… ID | |
portalSettings.planChangeMatrix[].toPriceIds | array of price_… ID | |
portalSettings.allowCancel | boolean | Customers may cancel their subscriptions. |
portalSettings.cancelBehavior | one of period_end, immediate | A portal cancellation ends the subscription at the period end (default) or at once. |
portalSettings.collectCancelReason | boolean | The portal asks why the customer cancels. |
portalSettings.allowQuantityChanges | boolean | Customers may change the quantity (seats) of licensed items. |
checkoutSettings | object | |
checkoutSettings.locales | array of string | The brand's languages, its default first. A default, not a restriction: a customer's own language comes first. |
checkoutSettings.collectPhone | boolean | |
checkoutSettings.requireBusinessDetails | one of auto, always, never | |
createdAt | date-time | |
updatedAt | date-time |
Examples
Upload the brand's icon for checkout
PUT /api/v1/brands/brd_01m4tx5nj8emwbyf7y9vcf47x4/images/icon
Authorization: Bearer or_test_…
Content-Type: application/json
{
"content": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
}
Response (200):
{
"id": "brd_01m4tx5nj8emwbyf7y9vcf47x4",
"object": "brand",
"orgId": "org_01m4tx5nj7ekd958x53w4bntcg",
"entityId": "ent_01m4tx5nj8emwbyf7y9vcf47x2",
"slug": "acme-app",
"name": "Acme App",
"status": "test_only",
"isPlatformBrand": false,
"branding": {
"logoUrl": null,
"iconUrl": null,
"logoFile": null,
"iconFile": {
"fileId": "file_01m4tx5nj8emwbyf7y9vcf47yc",
"url": "/media/file_01m4tx5nj8emwbyf7y9vcf47yc/63ef318d96b5d0d0ceba6e04a4e622b1158335cdc67c49e27839132c6f655058.png"
},
"primaryColor": null,
"accentColor": null,
"supportEmail": null,
"supportUrl": null,
"termsUrl": null,
"privacyUrl": null
},
"statementDescriptor": "ACME APP",
"statementDescriptorSuffix": null,
"invoiceSeries": {
"invoice": "ACM",
"creditNote": "ACM-CN"
},
"billingSettings": {
"dunning": {
"retryOffsetsDays": [1, 3, 5, 7],
"finalAction": "unpaid"
},
"entitlementGraceDays": 7,
"sendInvoice": {
"graceDays": 14
},
"cardExpiryReminders": true,
"pausedEntitlements": false,
"paymentBehavior": "allow_incomplete"
},
"policies": {
"breakageRecognition": "at_expiry",
"supportRefundLimits": [],
"disputeWalletFreeze": true,
"taxIdUnavailable": "charge_as_b2c"
},
"portalSettings": {
"allowPlanChanges": true,
"planChangeMatrix": [],
"allowCancel": true,
"cancelBehavior": "period_end",
"collectCancelReason": true,
"allowQuantityChanges": false
},
"checkoutSettings": {
"locales": ["en", "hr"],
"collectPhone": false,
"requireBusinessDetails": "auto"
},
"createdAt": "2026-10-13T12:00:00.000Z",
"updatedAt": "2026-10-14T09:30:00.000Z"
}
Errors
| Code | HTTP status | When | What to do |
|---|---|---|---|
validation_failed | 400 | content isn't base64, or is far larger than an image may be (param names the field). | Fix the fields listed in details.issues and send the request again. |
image_type_unsupported | 400 | The file isn't a still PNG, JPEG or WebP image (read from its bytes): SVG, GIF and animations are refused. | Export the image as PNG, JPEG or WebP (a still image, not an animation) and upload it again. The type is read from the file's bytes, not from its name. |
image_too_large | 400 | The image is larger than 512 KB. | Resize or compress the image to at most 512 KB. Avatars and icons are shown at 256 × 256 pixels and logos within 600 × 200, so a small file loses nothing. |
image_dimensions | 400 | The image is wider or taller than 1024 pixels, or not square (avatars and icons), or a logo outside 1:1 to 4:1. | Use a square image of at most 1024 × 1024 pixels for avatars and icons, and a logo between square and four times as wide as it is tall, at most 1024 pixels a side. |
upstream_timeout | 502 | The file store didn't answer in time; nothing was recorded. Retry. | Retry the request with the same Idempotency-Key after a short wait. |
resource_not_found | 404 | No brand with this ID is visible to the request. | Check the ID and that your credentials are scoped to the right organization, entity, brand and mode. |
version_conflict | 409 | The brand changed at the same time; retry. | Fetch the object again and retry the change. |
permission_denied | 403 | You can't change this brand's settings. | Ask an organization owner or admin for a role or scope that allows this action. |
It can also return these common errors: api_version_unsupported, invalid_json, unsupported_media_type, authentication_required, api_key_invalid, idempotency_key_reused, idempotency_in_progress, approval_required, approval_rejected, approval_expired, stale_approval, approval_policy_denied, rate_limited, internal_error.