All content API endpoints require an API key with full, upload, or admin scope. Pass it as a Bearer token:
curl -H "Authorization: Bearer bk_your_api_key" \
http://localhost:4100/api/v1/content
Register content
Creates a new content entry or upserts if id already exists.
Request body:
{
"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):
{
"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
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):
{
"items": [{ "id": "my-video-001", "name": "My Video", "..." : "..." }],
"total": 1
}
Get content
Response (200):
{
"content": { "id": "my-video-001", "name": "My Video", "..." : "..." }
}
Returns 404 if the content does not exist.
Update content
PATCH /api/v1/content/:id
Request body (all fields optional):
{
"name": "Updated Name",
"sizeBytes": 1048576,
"manifestKey": "manifest.m3u8",
"metadata": { "duration": 180 }
}
Response (200): Updated content object.
Delete content
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
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:
{
"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):
{
"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-..." }
]
}
When a manifest file (.m3u8 or .mpd) is included in the presign request, the server automatically updates the content’s manifestKey field.
Error responses
All error responses follow the format:
{ "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