---
title: "Upload an image for a media panel."
method: POST
path: "/v1/orgs/{orgId}/media"
tags: ["dashboards"]
---

# Upload an image for a media panel.

`POST /v1/orgs/{orgId}/media`

## Request

- Base URL: `http://localhost:8080`
- URL: `http://localhost:8080/v1/orgs/{orgId}/media`
- Auth: one of:
  - API key in cookie `dvt_session`
  - HTTP bearer

Uploads an image to use as the `src` of a `media` panel element. The image is validated
(PNG, JPEG, or WebP, max 5 MB, max 4096×4096), re-encoded (JPEG stays JPEG, PNG/WebP become
PNG — stripping EXIF/metadata/active content), and stored in the public asset bucket under
a content-addressed, immutable key (`media/<orgId>/<hash>.<ext>`). The returned URL is a
dvt-hosted https asset (e.g. `https://assets.dvt.dev/media/...`) that passes the media
sanitizer and CSP, so it can be placed directly in a media panel's `src`.

Requires dashboard edit access (`dashboard:write`) — any editor, not just owners. Uploaded
images are public-read by anyone holding the (unguessable) URL.

**Unavailable in `DVT_MODE=snowflake`** (returns 503): that deployment mode has no
durable, re-resolvable object store wired to this endpoint yet, only a filesystem
backend whose signed URLs expire after 24h — persisting one into a dashboard spec would
break the panel once it expires. A follow-up will store the object key (not a signed
URL) and re-resolve a fresh one per dashboard load, the same pattern branding logos
already use. Branding logo upload/serving is unaffected and works normally in this mode.

## Response `200`

Image uploaded successfully. Returns the hosted URL and metadata.

- object
  - `data` object, required
    - `url` string, required: dvt-hosted https URL to place in a media panel's `src`.
    - `key` string, required: R2 object key (content-addressed, immutable).
    - `width` integer, required: Decoded pixel width.
    - `height` integer, required: Decoded pixel height.
    - `bytes` integer, required: Size of the stored (re-encoded) image in bytes.
  - `meta` Meta, required: Response metadata.
    - `requestId` string, required: Correlates with the X-Request-Id header.
    - `total` integer: Item count, present on collection responses.
    - `nextCursor` string, nullable: Opaque keyset-pagination cursor, present on paginated responses (e.g. GET /v1/dashboards) when more rows exist beyond this page. Pass it back as the `cursor` query param to fetch the next page; absent or null on the last page.
    - `roleResolveDegraded` boolean: Snowflake mode only (DVT-2260), on GET /v1/dashboards: present and true when the caller's live Snowflake role resolution failed or was unavailable — role-shared dashboards may be missing from this page. Omitted (not merely false) whenever resolution succeeded or the role-share arm does not apply (off snowflake mode, an org admin/owner, or off the ingress listener).

## Other responses

- `401`: Missing, invalid, or expired session.
- `403`: The caller's role does not permit this action.
- `404`: Resource does not exist.
- `413`: Payload too large — image exceeds 5 MB.
- `415`: Unsupported media type — only PNG, JPEG, and WebP are accepted.
- `422`: Validation error — image dimensions out of range or image could not be decoded.
- `503`: Image upload is not configured (public asset bucket not wired), OR the deployment is running `DVT_MODE=snowflake`, where this endpoint is temporarily disabled (see the operation description above).

---

[API](https://skmtc.dev/dvt/apis/dvt-rest-api.md) · [All operations](https://skmtc.dev/dvt/apis/dvt-rest-api/llms.txt) · [OpenAPI document](https://skmtc.dev/dvt/apis/dvt-rest-api/revisions/00016f504165?raw)
