
# Upload a branding asset

`POST /v1/branding/assets` · permission `branding:write`

Uploads an image for the hosted login page: a logo, a favicon or a background image. The response `id` is the value you put in the theme fields `logo`, `favicon` or `background_image` (see [Update branding](./update.md)); the image appears on the login page after you publish.

Every image is decoded in an isolated process and re-encoded as PNG, at most 1024 × 1024 pixels, with all metadata removed. A favicon also gets 32 px and 180 px versions. The `id` is the SHA-256 of the processed PNG, so uploading the same image again for the same field returns the existing asset with `200 OK`.

Accepted formats are PNG, JPEG and WebP, and ICO for favicons only. SVG is not accepted. Font upload is not available.

An environment can hold at most 200 assets and 50 MB in total.

## Request

The body is `multipart/form-data`, at most 1 MiB for the file.

| Part | Type | Required | Description |
|---|---|---|---|
| `kind` | string | yes | `logo`, `favicon` or `background_image`. |
| `file` | file | yes | The image. At most 1 MiB and 8192 pixels on a side. |

With an `Idempotency-Key`, a retry must send exactly the same bytes, including the multipart boundary.

## Response

`201 Created` for a new asset, `200 OK` when the same image was already uploaded for the same field.

```json
{
  "id": "3f0c…e91a",
  "kind": "favicon",
  "url": "https://acme.eu1.keenauth.app/assets/3f0c…e91a.png",
  "width": 256,
  "height": 256,
  "byte_size": 48213,
  "source_format": "ico",
  "favicon_32_url": "https://acme.eu1.keenauth.app/assets/9b1d…04c2.png",
  "favicon_180_url": "https://acme.eu1.keenauth.app/assets/c7e2…5a10.png",
  "created_at": "2026-10-19T20:30:00Z"
}
```

`byte_size` is the size of all stored PNGs of the asset and counts toward the quota.

## Errors

| HTTP | code | When |
|---|---|---|
| 400 | `malformed_request` | The multipart body cannot be read, or has parts other than one `kind` and one `file`. |
| 401 | `unauthenticated` | The credential is missing, invalid or revoked. |
| 403 | `insufficient_permissions` | The credential lacks the required permission. |
| 403 | `quota_exceeded` | The environment already has 200 assets or 50 MB. |
| 413 | `payload_too_large` | The file is larger than 1 MiB. |
| 415 | `unsupported_media_type` | The body is not `multipart/form-data`, or the file is not an accepted format (`unsupported_media_type`, `svg_not_allowed`, `ico_favicon_only` in `errors[]`). |
| 422 | `validation_failed` | `kind` or `file` is missing or invalid (`/kind font_upload_disabled` for fonts), or the image cannot be used: `invalid_image`, `image_too_large`, `image_dimensions_too_large`, `image_output_too_large`, `image_processing_limit`. |
| 422 | `idempotency_key_reuse` | The same `Idempotency-Key` was used with a different body. |
| 429 | `rate_limited` | The rate limit was exceeded; retry after the `Retry-After` period. |
| 503 | `service_unavailable` | Asset storage is temporarily unavailable; retry after the `Retry-After` period. |

## Example

```bash
curl -X POST https://api.eu.keenauth.com/v1/branding/assets \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -F kind=logo \
  -F file=@logo.png
```

With the CLI:

```bash
keenauth branding assets upload --kind logo --file logo.png
```
