# Uploads

### Presign a media upload

`POST /accounts/{accountId}/uploads/presign`

**Required role:** editor

Validate scope/type/size + target ownership and return a short-lived presigned PUT URL plus the eventual CDN URL. The `accountAvatar` and `accountBanner` scopes additionally require the admin role.

**Parameters**

- `accountId` _(path)_ · `string` · required

**Request body**

- `scope` · `"accountAvatar" | "accountBanner" | "appImage"` · required — Which media slot is being uploaded; resolves to a server-owned key prefix.
- `contentType` · `string` · required — The exact Content-Type the client will PUT (signed into the URL).
- `sizeBytes` · `integer` · required — Byte size of the file; checked against the scope cap.
- `sha256` · `string` · required
- `targetId` · `string` · optional — The owning entity id (publishedAppId for appImage).

**Returns**

- `200` · `PresignUploadResultDto`

Attributes of `PresignUploadResultDto`:

- `uploadUrl` · `string <uri>` · read-only — Presigned PUT URL — the client uploads the bytes directly here.
- `key` · `string` · read-only — Server-issued object key; pass it back verbatim to commit.
- `finalUrl` · `string <uri>` · read-only — The CDN URL the object will be delivered from after commit.
- `expiresAt` · `string <date-time>` · read-only — When the presigned upload URL expires.

**Example request**

```bash
curl https://api.koo.io/accounts/:accountId/uploads/presign \
  -X POST \
  -H "Authorization: Bearer kc_your_api_token" \
  -H "Content-Type: application/json" \
  -d '{
      "scope": "accountAvatar",
      "contentType": "example",
      "sizeBytes": 0,
      "sha256": "example"
    }'
```

**Example response** (`200`)

```json
{
  "uploadUrl": "https://example.com",
  "key": "example",
  "finalUrl": "https://example.com",
  "expiresAt": "2026-01-01T00:00:00.000Z"
}
```

### Commit a media upload

`POST /accounts/{accountId}/uploads/commit`

**Required role:** editor

HEAD-verify the uploaded object and write its CDN URL onto the target entity. Rejects any key outside the requester’s scope. The `accountAvatar` and `accountBanner` scopes additionally require the admin role.

**Parameters**

- `accountId` _(path)_ · `string` · required

**Request body**

- `key` · `string` · required — The key returned by presign.
- `scope` · `"accountAvatar" | "accountBanner" | "appImage"` · required — Which media slot is being uploaded; resolves to a server-owned key prefix.

**Returns**

- `200` · `CommitUploadResultDto`

Attributes of `CommitUploadResultDto`:

- `finalUrl` · `string <uri>` · read-only — The committed CDN URL written to the entity.

**Example request**

```bash
curl https://api.koo.io/accounts/:accountId/uploads/commit \
  -X POST \
  -H "Authorization: Bearer kc_your_api_token" \
  -H "Content-Type: application/json" \
  -d '{
      "key": "example",
      "scope": "accountAvatar"
    }'
```

**Example response** (`200`)

```json
{
  "finalUrl": "https://example.com"
}
```
