# Uploads

> Slicer API upload endpoints — create an upload, send the file whole, in parts or via a one-time link, import from a URL, check the status and cancel.

Source: https://docs.slicer.media/en/api/reference/uploads/

An upload is a video, photo or audio file a montage is built from. First the upload is created (`createUpload`), then the file is sent in one of the ways, and after the check the status becomes `ready`. How to choose a way and handle errors — in the guide [Uploading files](https://docs.slicer.media/en/api/uploads/).

## Start an upload

`POST /v1/uploads`

- **Scopes:** `projects:write`
- **Rate limit:** default

Returns the upload and a one-time `uploadUrl`. Then either `PUT` the whole file to `uploadUrl` (no token) or to `/v1/uploads/{id}/content`, or send 8 MiB parts to `/v1/uploads/{id}/parts/{n}` and call `complete`.

### Request body `CreateUploadRequest`

| Field | Type | Description |
| --- | --- | --- |
| `fileName` _(required)_ | `string` | File name with extension: the type is detected by it — ≤ 255 |
| `size` _(required)_ | `integer` | File size, bytes — ≥ 1 |
| `mimeType` | `string` | — ≤ 128 |
| `purpose` | `string` | `source` (default) — montage material — `source` · `media` · `music` |

### Request

**curl**

```bash
curl -X POST "https://api.slicer.media/v1/uploads" \
  -H "Authorization: Bearer $SLICER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"fileName":"interview.mp4","size":104857600}'
```

**JavaScript**

```js
const response = await fetch('https://api.slicer.media/v1/uploads', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SLICER_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "fileName": "interview.mp4",
    "size": 104857600
  }),
});
const { ok, data, error } = await response.json();
```

**Python**

```python
import os
import requests

response = requests.post(
    "https://api.slicer.media/v1/uploads",
    headers={"Authorization": f"Bearer {os.environ['SLICER_TOKEN']}"},
    json={
        "fileName": "interview.mp4",
        "size": 104857600,
    },
)
body = response.json()
```

### Response

**Response fields: `CreatedUpload` (3)**

| Field | Type | Description |
| --- | --- | --- |
| `upload` _(required)_ | `Upload` |     |
| `uploadUrl` _(required)_ | `string (uri)` | One-time URL: `PUT` the whole file without a token (≤ 1 h, write-only) |
| `uploadUrlExpiresAt` _(required)_ | `string` | ISO 8601 date-time, UTC |

Response example:

```json
{
  "ok": true,
  "data": {
    "upload": {
      "id": "6f1c2a4e-8b3d-4c1a-9e2f-3b5d7a9c1e20",
      "fileName": "interview.mp4",
      "purpose": "source",
      "origin": "client",
      "sizeBytes": 104857600,
      "status": "uploading",
      "partSize": 104857600,
      "partCount": 13,
      "uploadedParts": [
        1
      ],
      "progress": 0.5,
      "media": {
        "kind": null,
        "durationSec": null,
        "width": null,
        "height": null,
        "hasAudio": null
      },
      "error": "string",
      "createdAt": "2026-10-16T09:30:00Z"
    },
    "uploadUrl": "https://api.slicer.media/v1/…",
    "uploadUrlExpiresAt": "2026-10-16T09:30:00Z"
  }
}
```

## Upload the whole file by a one-time URL

`PUT /v1/upload-links/{secret}`

- **Scopes:** no token
- **Rate limit:** default

No token: the secret in the URL is the permission. Write-only, one file, ≤ 1 h. `curl -T video.mp4 <uploadUrl>`

### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `secret` _(required)_ | `string` |     |

### Request body

File bytes (`application/octet-stream`).

### Request

**curl**

```bash
curl -X PUT "https://api.slicer.media/v1/upload-links/upl_…" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @video.mp4
```

**JavaScript**

```js
import { readFile } from 'node:fs/promises';

const response = await fetch('https://api.slicer.media/v1/upload-links/upl_…', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/octet-stream',
  },
  body: await readFile('video.mp4'),
});
const { ok, data, error } = await response.json();
```

**Python**

```python
import requests

with open("video.mp4", "rb") as file:
    response = requests.put(
        "https://api.slicer.media/v1/upload-links/upl_…",
        headers={"Content-Type": "application/octet-stream"},
        data=file,
    )
body = response.json()
```

### Response

**Response fields: `Upload` (18)**

| Field | Type | Description |
| --- | --- | --- |
| `id` _(required)_ | `string (uuid)` |     |
| `fileName` _(required)_ | `string` |     |
| `purpose` _(required)_ | `string` | — `source` · `media` · `music` |
| `origin` _(required)_ | `string` | — `client` · `import` · `server` |
| `sizeBytes` _(required)_ | `integer` |     |
| `status` _(required)_ | `string` | — `uploading` · `processing` · `ready` · `failed` · `aborted` |
| `partSize` _(required)_ | `integer` | Part size for `PUT …/parts/{n}`, bytes (8 MiB) |
| `partCount` _(required)_ | `integer` |     |
| `uploadedParts` _(required)_ | `integer[]` |     |
| `progress` _(required)_ | `number \| null` | Import progress 0–1 while downloading by URL — 0–1 |
| `media` _(required)_ | `object \| null` |     |
| `media.kind` _(required)_ | `string` | — `video` · `image` · `audio` |
| `media.durationSec` _(required)_ | `number` |     |
| `media.width` _(required)_ | `integer` |     |
| `media.height` _(required)_ | `integer` |     |
| `media.hasAudio` _(required)_ | `boolean` |     |
| `error` _(required)_ | `string \| null` |     |
| `createdAt` _(required)_ | `string` | ISO 8601 date-time, UTC |

Response example:

```json
{
  "ok": true,
  "data": {
    "id": "6f1c2a4e-8b3d-4c1a-9e2f-3b5d7a9c1e20",
    "fileName": "interview.mp4",
    "purpose": "source",
    "origin": "client",
    "sizeBytes": 104857600,
    "status": "uploading",
    "partSize": 104857600,
    "partCount": 13,
    "uploadedParts": [
      1
    ],
    "progress": 0.5,
    "media": {
      "kind": "video",
      "durationSec": 42.5,
      "width": 1,
      "height": 1,
      "hasAudio": true
    },
    "error": "string",
    "createdAt": "2026-10-16T09:30:00Z"
  }
}
```

## Upload the whole file

`PUT /v1/uploads/{id}/content`

- **Scopes:** `projects:write`
- **Rate limit:** default

Body — the file bytes, `Content-Length` must equal the declared size. The upload is completed automatically.

### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` _(required)_ | `string (uuid)` |     |

### Request body

File bytes (`application/octet-stream`).

### Request

**curl**

```bash
curl -X PUT "https://api.slicer.media/v1/uploads/6f1c2a4e-8b3d-4c1a-9e2f-3b5d7a9c1e20/content" \
  -H "Authorization: Bearer $SLICER_TOKEN" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @video.mp4
```

**JavaScript**

```js
import { readFile } from 'node:fs/promises';

const response = await fetch('https://api.slicer.media/v1/uploads/6f1c2a4e-8b3d-4c1a-9e2f-3b5d7a9c1e20/content', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${process.env.SLICER_TOKEN}`,
    'Content-Type': 'application/octet-stream',
  },
  body: await readFile('video.mp4'),
});
const { ok, data, error } = await response.json();
```

**Python**

```python
import os
import requests

with open("video.mp4", "rb") as file:
    response = requests.put(
        "https://api.slicer.media/v1/uploads/6f1c2a4e-8b3d-4c1a-9e2f-3b5d7a9c1e20/content",
        headers={"Authorization": f"Bearer {os.environ['SLICER_TOKEN']}", "Content-Type": "application/octet-stream"},
        data=file,
    )
body = response.json()
```

### Response

**Response fields: `Upload` (18)**

| Field | Type | Description |
| --- | --- | --- |
| `id` _(required)_ | `string (uuid)` |     |
| `fileName` _(required)_ | `string` |     |
| `purpose` _(required)_ | `string` | — `source` · `media` · `music` |
| `origin` _(required)_ | `string` | — `client` · `import` · `server` |
| `sizeBytes` _(required)_ | `integer` |     |
| `status` _(required)_ | `string` | — `uploading` · `processing` · `ready` · `failed` · `aborted` |
| `partSize` _(required)_ | `integer` | Part size for `PUT …/parts/{n}`, bytes (8 MiB) |
| `partCount` _(required)_ | `integer` |     |
| `uploadedParts` _(required)_ | `integer[]` |     |
| `progress` _(required)_ | `number \| null` | Import progress 0–1 while downloading by URL — 0–1 |
| `media` _(required)_ | `object \| null` |     |
| `media.kind` _(required)_ | `string` | — `video` · `image` · `audio` |
| `media.durationSec` _(required)_ | `number` |     |
| `media.width` _(required)_ | `integer` |     |
| `media.height` _(required)_ | `integer` |     |
| `media.hasAudio` _(required)_ | `boolean` |     |
| `error` _(required)_ | `string \| null` |     |
| `createdAt` _(required)_ | `string` | ISO 8601 date-time, UTC |

Response example:

```json
{
  "ok": true,
  "data": {
    "id": "6f1c2a4e-8b3d-4c1a-9e2f-3b5d7a9c1e20",
    "fileName": "interview.mp4",
    "purpose": "source",
    "origin": "client",
    "sizeBytes": 104857600,
    "status": "uploading",
    "partSize": 104857600,
    "partCount": 13,
    "uploadedParts": [
      1
    ],
    "progress": 0.5,
    "media": {
      "kind": "video",
      "durationSec": 42.5,
      "width": 1,
      "height": 1,
      "hasAudio": true
    },
    "error": "string",
    "createdAt": "2026-10-16T09:30:00Z"
  }
}
```

## Upload a part

`PUT /v1/uploads/{id}/parts/{n}`

- **Scopes:** `projects:write`
- **Rate limit:** upload parts

Parts are numbered from 1; every part except the last is exactly `partSize` bytes. Parts may go in parallel and be retried.

### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` _(required)_ | `string (uuid)` |     |
| `n` _(required)_ | `integer` | — ≥ 1 |

### Request body

File bytes (`application/octet-stream`).

### Request

**curl**

```bash
curl -X PUT "https://api.slicer.media/v1/uploads/6f1c2a4e-8b3d-4c1a-9e2f-3b5d7a9c1e20/parts/1" \
  -H "Authorization: Bearer $SLICER_TOKEN" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @video.mp4
```

**JavaScript**

```js
import { readFile } from 'node:fs/promises';

const response = await fetch('https://api.slicer.media/v1/uploads/6f1c2a4e-8b3d-4c1a-9e2f-3b5d7a9c1e20/parts/1', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${process.env.SLICER_TOKEN}`,
    'Content-Type': 'application/octet-stream',
  },
  body: await readFile('video.mp4'),
});
const { ok, data, error } = await response.json();
```

**Python**

```python
import os
import requests

with open("video.mp4", "rb") as file:
    response = requests.put(
        "https://api.slicer.media/v1/uploads/6f1c2a4e-8b3d-4c1a-9e2f-3b5d7a9c1e20/parts/1",
        headers={"Authorization": f"Bearer {os.environ['SLICER_TOKEN']}", "Content-Type": "application/octet-stream"},
        data=file,
    )
body = response.json()
```

### Response

**Response fields: `object` (2)**

| Field | Type | Description |
| --- | --- | --- |
| `part` _(required)_ | `integer` |     |
| `etag` _(required)_ | `string` |     |

Response example:

```json
{
  "ok": true,
  "data": {
    "part": 1,
    "etag": "string"
  }
}
```

## Complete a multipart upload

`POST /v1/uploads/{id}/complete`

- **Scopes:** `projects:write`
- **Rate limit:** default

### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` _(required)_ | `string (uuid)` |     |

### Request

**curl**

```bash
curl -X POST "https://api.slicer.media/v1/uploads/6f1c2a4e-8b3d-4c1a-9e2f-3b5d7a9c1e20/complete" \
  -H "Authorization: Bearer $SLICER_TOKEN"
```

**JavaScript**

```js
const response = await fetch('https://api.slicer.media/v1/uploads/6f1c2a4e-8b3d-4c1a-9e2f-3b5d7a9c1e20/complete', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SLICER_TOKEN}`,
  },
});
const { ok, data, error } = await response.json();
```

**Python**

```python
import os
import requests

response = requests.post(
    "https://api.slicer.media/v1/uploads/6f1c2a4e-8b3d-4c1a-9e2f-3b5d7a9c1e20/complete",
    headers={"Authorization": f"Bearer {os.environ['SLICER_TOKEN']}"},
)
body = response.json()
```

### Response

**Response fields: `Upload` (18)**

| Field | Type | Description |
| --- | --- | --- |
| `id` _(required)_ | `string (uuid)` |     |
| `fileName` _(required)_ | `string` |     |
| `purpose` _(required)_ | `string` | — `source` · `media` · `music` |
| `origin` _(required)_ | `string` | — `client` · `import` · `server` |
| `sizeBytes` _(required)_ | `integer` |     |
| `status` _(required)_ | `string` | — `uploading` · `processing` · `ready` · `failed` · `aborted` |
| `partSize` _(required)_ | `integer` | Part size for `PUT …/parts/{n}`, bytes (8 MiB) |
| `partCount` _(required)_ | `integer` |     |
| `uploadedParts` _(required)_ | `integer[]` |     |
| `progress` _(required)_ | `number \| null` | Import progress 0–1 while downloading by URL — 0–1 |
| `media` _(required)_ | `object \| null` |     |
| `media.kind` _(required)_ | `string` | — `video` · `image` · `audio` |
| `media.durationSec` _(required)_ | `number` |     |
| `media.width` _(required)_ | `integer` |     |
| `media.height` _(required)_ | `integer` |     |
| `media.hasAudio` _(required)_ | `boolean` |     |
| `error` _(required)_ | `string \| null` |     |
| `createdAt` _(required)_ | `string` | ISO 8601 date-time, UTC |

Response example:

```json
{
  "ok": true,
  "data": {
    "id": "6f1c2a4e-8b3d-4c1a-9e2f-3b5d7a9c1e20",
    "fileName": "interview.mp4",
    "purpose": "source",
    "origin": "client",
    "sizeBytes": 104857600,
    "status": "uploading",
    "partSize": 104857600,
    "partCount": 13,
    "uploadedParts": [
      1
    ],
    "progress": 0.5,
    "media": {
      "kind": "video",
      "durationSec": 42.5,
      "width": 1,
      "height": 1,
      "hasAudio": true
    },
    "error": "string",
    "createdAt": "2026-10-16T09:30:00Z"
  }
}
```

## Import a file by URL

`POST /v1/uploads/import`

- **Scopes:** `projects:write`
- **Rate limit:** default

Downloads a public https file in the background. Poll `GET /v1/uploads/{id}` or wait for the `upload.ready` webhook.

### Request body `ImportUploadRequest`

| Field | Type | Description |
| --- | --- | --- |
| `url` _(required)_ | `string (uri)` | Public https URL of the file — ≤ 2048 |
| `fileName` | `string` | Name with extension if the URL has none — ≤ 255 |
| `purpose` | `string` | — `source` · `media` · `music` |

### Request

**curl**

```bash
curl -X POST "https://api.slicer.media/v1/uploads/import" \
  -H "Authorization: Bearer $SLICER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hook"}'
```

**JavaScript**

```js
const response = await fetch('https://api.slicer.media/v1/uploads/import', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SLICER_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "url": "https://example.com/hook"
  }),
});
const { ok, data, error } = await response.json();
```

**Python**

```python
import os
import requests

response = requests.post(
    "https://api.slicer.media/v1/uploads/import",
    headers={"Authorization": f"Bearer {os.environ['SLICER_TOKEN']}"},
    json={
        "url": "https://example.com/hook",
    },
)
body = response.json()
```

### Response

**Response fields: `Upload` (18)**

| Field | Type | Description |
| --- | --- | --- |
| `id` _(required)_ | `string (uuid)` |     |
| `fileName` _(required)_ | `string` |     |
| `purpose` _(required)_ | `string` | — `source` · `media` · `music` |
| `origin` _(required)_ | `string` | — `client` · `import` · `server` |
| `sizeBytes` _(required)_ | `integer` |     |
| `status` _(required)_ | `string` | — `uploading` · `processing` · `ready` · `failed` · `aborted` |
| `partSize` _(required)_ | `integer` | Part size for `PUT …/parts/{n}`, bytes (8 MiB) |
| `partCount` _(required)_ | `integer` |     |
| `uploadedParts` _(required)_ | `integer[]` |     |
| `progress` _(required)_ | `number \| null` | Import progress 0–1 while downloading by URL — 0–1 |
| `media` _(required)_ | `object \| null` |     |
| `media.kind` _(required)_ | `string` | — `video` · `image` · `audio` |
| `media.durationSec` _(required)_ | `number` |     |
| `media.width` _(required)_ | `integer` |     |
| `media.height` _(required)_ | `integer` |     |
| `media.hasAudio` _(required)_ | `boolean` |     |
| `error` _(required)_ | `string \| null` |     |
| `createdAt` _(required)_ | `string` | ISO 8601 date-time, UTC |

Response example:

```json
{
  "ok": true,
  "data": {
    "id": "6f1c2a4e-8b3d-4c1a-9e2f-3b5d7a9c1e20",
    "fileName": "interview.mp4",
    "purpose": "source",
    "origin": "client",
    "sizeBytes": 104857600,
    "status": "uploading",
    "partSize": 104857600,
    "partCount": 13,
    "uploadedParts": [
      1
    ],
    "progress": 0.5,
    "media": {
      "kind": "video",
      "durationSec": 42.5,
      "width": 1,
      "height": 1,
      "hasAudio": true
    },
    "error": "string",
    "createdAt": "2026-10-16T09:30:00Z"
  }
}
```

## Upload status

`GET /v1/uploads/{id}`

- **Scopes:** `projects:read`
- **Rate limit:** default

### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` _(required)_ | `string (uuid)` |     |

### Request

**curl**

```bash
curl "https://api.slicer.media/v1/uploads/6f1c2a4e-8b3d-4c1a-9e2f-3b5d7a9c1e20" \
  -H "Authorization: Bearer $SLICER_TOKEN"
```

**JavaScript**

```js
const response = await fetch('https://api.slicer.media/v1/uploads/6f1c2a4e-8b3d-4c1a-9e2f-3b5d7a9c1e20', {
  headers: {
    Authorization: `Bearer ${process.env.SLICER_TOKEN}`,
  },
});
const { ok, data, error } = await response.json();
```

**Python**

```python
import os
import requests

response = requests.get(
    "https://api.slicer.media/v1/uploads/6f1c2a4e-8b3d-4c1a-9e2f-3b5d7a9c1e20",
    headers={"Authorization": f"Bearer {os.environ['SLICER_TOKEN']}"},
)
body = response.json()
```

### Response

**Response fields: `Upload` (18)**

| Field | Type | Description |
| --- | --- | --- |
| `id` _(required)_ | `string (uuid)` |     |
| `fileName` _(required)_ | `string` |     |
| `purpose` _(required)_ | `string` | — `source` · `media` · `music` |
| `origin` _(required)_ | `string` | — `client` · `import` · `server` |
| `sizeBytes` _(required)_ | `integer` |     |
| `status` _(required)_ | `string` | — `uploading` · `processing` · `ready` · `failed` · `aborted` |
| `partSize` _(required)_ | `integer` | Part size for `PUT …/parts/{n}`, bytes (8 MiB) |
| `partCount` _(required)_ | `integer` |     |
| `uploadedParts` _(required)_ | `integer[]` |     |
| `progress` _(required)_ | `number \| null` | Import progress 0–1 while downloading by URL — 0–1 |
| `media` _(required)_ | `object \| null` |     |
| `media.kind` _(required)_ | `string` | — `video` · `image` · `audio` |
| `media.durationSec` _(required)_ | `number` |     |
| `media.width` _(required)_ | `integer` |     |
| `media.height` _(required)_ | `integer` |     |
| `media.hasAudio` _(required)_ | `boolean` |     |
| `error` _(required)_ | `string \| null` |     |
| `createdAt` _(required)_ | `string` | ISO 8601 date-time, UTC |

Response example:

```json
{
  "ok": true,
  "data": {
    "id": "6f1c2a4e-8b3d-4c1a-9e2f-3b5d7a9c1e20",
    "fileName": "interview.mp4",
    "purpose": "source",
    "origin": "client",
    "sizeBytes": 104857600,
    "status": "uploading",
    "partSize": 104857600,
    "partCount": 13,
    "uploadedParts": [
      1
    ],
    "progress": 0.5,
    "media": {
      "kind": "video",
      "durationSec": 42.5,
      "width": 1,
      "height": 1,
      "hasAudio": true
    },
    "error": "string",
    "createdAt": "2026-10-16T09:30:00Z"
  }
}
```

## Cancel an upload

`DELETE /v1/uploads/{id}`

- **Scopes:** `projects:write`
- **Rate limit:** default

### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` _(required)_ | `string (uuid)` |     |

### Request

**curl**

```bash
curl -X DELETE "https://api.slicer.media/v1/uploads/6f1c2a4e-8b3d-4c1a-9e2f-3b5d7a9c1e20" \
  -H "Authorization: Bearer $SLICER_TOKEN"
```

**JavaScript**

```js
const response = await fetch('https://api.slicer.media/v1/uploads/6f1c2a4e-8b3d-4c1a-9e2f-3b5d7a9c1e20', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.SLICER_TOKEN}`,
  },
});
const { ok, data, error } = await response.json();
```

**Python**

```python
import os
import requests

response = requests.delete(
    "https://api.slicer.media/v1/uploads/6f1c2a4e-8b3d-4c1a-9e2f-3b5d7a9c1e20",
    headers={"Authorization": f"Bearer {os.environ['SLICER_TOKEN']}"},
)
body = response.json()
```

### Response

**Response fields: `object` (2)**

| Field | Type | Description |
| --- | --- | --- |
| `id` _(required)_ | `string (uuid)` |     |
| `deleted` _(required)_ | `boolean` |     |

Response example:

```json
{
  "ok": true,
  "data": {
    "id": "6f1c2a4e-8b3d-4c1a-9e2f-3b5d7a9c1e20",
    "deleted": true
  }
}
```

## Schemas

`Upload`

| Field | Type | Description |
| --- | --- | --- |
| `id` _(required)_ | `string (uuid)` |     |
| `fileName` _(required)_ | `string` |     |
| `purpose` _(required)_ | `string` | — `source` · `media` · `music` |
| `origin` _(required)_ | `string` | — `client` · `import` · `server` |
| `sizeBytes` _(required)_ | `integer` |     |
| `status` _(required)_ | `string` | — `uploading` · `processing` · `ready` · `failed` · `aborted` |
| `partSize` _(required)_ | `integer` | Part size for `PUT …/parts/{n}`, bytes (8 MiB) |
| `partCount` _(required)_ | `integer` |     |
| `uploadedParts` _(required)_ | `integer[]` |     |
| `progress` _(required)_ | `number \| null` | Import progress 0–1 while downloading by URL — 0–1 |
| `media` _(required)_ | `object \| null` |     |
| `media.kind` _(required)_ | `string` | — `video` · `image` · `audio` |
| `media.durationSec` _(required)_ | `number` |     |
| `media.width` _(required)_ | `integer` |     |
| `media.height` _(required)_ | `integer` |     |
| `media.hasAudio` _(required)_ | `boolean` |     |
| `error` _(required)_ | `string \| null` |     |
| `createdAt` _(required)_ | `string` | ISO 8601 date-time, UTC |

`CreatedUpload`

| Field | Type | Description |
| --- | --- | --- |
| `upload` _(required)_ | `Upload` |     |
| `uploadUrl` _(required)_ | `string (uri)` | One-time URL: `PUT` the whole file without a token (≤ 1 h, write-only) |
| `uploadUrlExpiresAt` _(required)_ | `string` | ISO 8601 date-time, UTC |
