> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blindcast.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Content API

> REST API reference for managing content in BlindCast Server — register, list, update, delete, and get presigned upload URLs.

All content API endpoints require an API key with `full`, `upload`, or `admin` scope. Pass it as a Bearer token:

```bash theme={null}
curl -H "Authorization: Bearer bk_your_api_key" \
  http://localhost:4100/api/v1/content
```

## Register content

```http theme={null}
POST /api/v1/content
```

Creates a new content entry or upserts if `id` already exists.

**Request body:**

```json theme={null}
{
  "name": "My Video",
  "id": "my-video-001",
  "metadata": { "duration": 120 }
}
```

| Field       | Type   | Required | Description                                                                     |
| ----------- | ------ | -------- | ------------------------------------------------------------------------------- |
| `name`      | string | Yes      | Display name                                                                    |
| `id`        | string | No       | Content ID (auto-generated UUID if omitted). Must match `[a-zA-Z0-9_-]{1,256}`. |
| `sizeBytes` | number | No       | Total size in bytes                                                             |
| `metadata`  | object | No       | Arbitrary JSON metadata                                                         |

**Response (201):**

```json theme={null}
{
  "content": {
    "id": "my-video-001",
    "name": "My Video",
    "status": "active",
    "sizeBytes": null,
    "manifestKey": null,
    "storageKey": "my-video-001/",
    "metadata": { "duration": 120 },
    "createdAt": "2026-03-02T12:00:00.000Z",
    "updatedAt": "2026-03-02T12:00:00.000Z",
    "presign_endpoint": "/api/v1/content/my-video-001/presign",
    "key_endpoint": "/keys/my-video-001"
  }
}
```

## List content

```http theme={null}
GET /api/v1/content?limit=20&offset=0&status=active
```

| Parameter | Type   | Default | Description                      |
| --------- | ------ | ------- | -------------------------------- |
| `limit`   | number | 50      | Page size                        |
| `offset`  | number | 0       | Offset for pagination            |
| `status`  | string | —       | Filter by `active` or `disabled` |

**Response (200):**

```json theme={null}
{
  "items": [{ "id": "my-video-001", "name": "My Video", "..." : "..." }],
  "total": 1
}
```

## Get content

```http theme={null}
GET /api/v1/content/:id
```

**Response (200):**

```json theme={null}
{
  "content": { "id": "my-video-001", "name": "My Video", "..." : "..." }
}
```

Returns `404` if the content does not exist.

## Update content

```http theme={null}
PATCH /api/v1/content/:id
```

**Request body** (all fields optional):

```json theme={null}
{
  "name": "Updated Name",
  "sizeBytes": 1048576,
  "manifestKey": "manifest.m3u8",
  "metadata": { "duration": 180 }
}
```

**Response (200):** Updated content object.

## Delete content

```http theme={null}
DELETE /api/v1/content/:id
```

Soft-deletes the content (sets `status` to `disabled`). Disabled content returns `404` from the key server — viewers can no longer fetch keys for it.

**Response (200):** Content object with `"status": "disabled"`.

## Presigned upload URLs

```http theme={null}
POST /api/v1/content/:id/presign
```

Generates presigned S3 PUT URLs for uploading encrypted segments. Keys are automatically prefixed with the content ID.

**Request body:**

```json theme={null}
{
  "keys": [
    "manifest.m3u8",
    { "key": "seg-0.ts", "contentType": "video/MP2T" },
    { "key": "seg-1.ts", "contentType": "video/MP2T" }
  ]
}
```

| Field  | Type  | Description                                                 |
| ------ | ----- | ----------------------------------------------------------- |
| `keys` | array | List of S3 keys (strings or `{ key, contentType }` objects) |

Each key can be a plain string (defaults to `video/MP2T`) or an object with explicit `contentType`.

**Allowed content types:** `video/MP2T`, `video/mp4`, `application/vnd.apple.mpegurl`

**Response (200):**

```json theme={null}
{
  "urls": [
    { "key": "manifest.m3u8", "url": "https://s3.../my-video-001/manifest.m3u8?X-Amz-..." },
    { "key": "seg-0.ts", "url": "https://s3.../my-video-001/seg-0.ts?X-Amz-..." },
    { "key": "seg-1.ts", "url": "https://s3.../my-video-001/seg-1.ts?X-Amz-..." }
  ]
}
```

<Note>
  When a manifest file (`.m3u8` or `.mpd`) is included in the presign request, the server automatically updates the content's `manifestKey` field.
</Note>

## Error responses

All error responses follow the format:

```json theme={null}
{ "error": "ERROR_CODE" }
```

| Code               | HTTP Status | Description                                   |
| ------------------ | ----------- | --------------------------------------------- |
| `NOT_FOUND`        | 404         | Content does not exist                        |
| `VALIDATION_ERROR` | 400         | Invalid input (bad ID format, missing fields) |
| `CONFLICT`         | 409         | ID conflict on create                         |
| `UNAUTHORIZED`     | 401         | Missing or invalid API key                    |
| `FORBIDDEN`        | 403         | Insufficient API key scope                    |
| `INTERNAL_ERROR`   | 500         | Server error                                  |

## Next steps

* [API Keys](/server/api-keys) — manage API keys and scopes
* [Admin Dashboard](/server/admin-dashboard) — manage content from the browser
