Concepts
The media asset data model, allowed MIME types, upload modes, streaming providers, and the stream lifecycle.
This page is the reference for the shapes and enums you'll see throughout the API. Endpoint pages link back here rather than repeating them.
The media asset
A MediaAsset is echoed by almost every endpoint (under data.asset, data.items[], etc.). All
fields are always present; nullable fields are null until they apply.
| Field | Type | Notes |
|---|---|---|
id | uuid | Primary key. |
owner_user_id | string | The owning Shukr user. |
object_key | string | Storage key in R2 (unique). |
original_filename | string | As supplied at upload time. |
content_type | "place" | "post" | Business category, not a MIME type — where the media is used. |
mime_type | string | The real MIME type, e.g. image/jpeg. Must be allowed. |
file_size_bytes | number | null | Known size, when provided. |
status | "pending" | "uploaded" | "deleted" | Upload lifecycle. |
visibility | "public" | "private" | Defaults to public. |
external_ref | string | null | Your own correlation id (e.g. a place or post id). |
etag | string | null | R2 ETag after a completed upload. |
asset_kind | "file" | "stream" | File vs. video — see Overview. |
stream_provider | "bunny" | "onprem" | "cloudflare" | null | Set for stream assets. |
provider_asset_id | string | null | The provider's id for the video. |
stream_status | "pending_upload" | "processing" | "ready" | "failed" | null | Transcode lifecycle. |
playback | PlaybackManifest | null | Playback URLs once ready. |
object_upload_mode | "single_presigned_put" | "multipart_presigned_put" | "tus" | null | How the bytes are/were uploaded. |
created_at / updated_at | string | ISO timestamps. |
uploaded_at | string | null | Set when the upload completes. |
content_type is a category (place or post), not the file's MIME type. The MIME type
lives in the separate mime_type field. This trips people up — they are two different things.
Playback manifest
{
"hls": "https://cdn.example.com/vod/abc/playlist.m3u8",
"dash": "https://cdn.example.com/vod/abc/manifest.mpd",
"embed": "https://media.example.com/api/v1/playback/{assetId}/embed",
"thumbnail": "https://media.example.com/api/v1/playback/{assetId}/thumb.jpg"
}Each field is a URL or null depending on what the provider produced.
Allowed MIME types
mime_type is normalized (lower-cased, parameters like ; charset=… stripped) and must be one of:
| Group | Types |
|---|---|
| Image | image/jpeg, image/png, image/webp, image/gif, image/svg+xml |
| Audio | audio/mpeg, audio/wav, audio/ogg, audio/mp4, audio/aac, audio/webm |
| Video | video/mp4, video/webm, video/quicktime, video/x-msvideo, video/x-matroska, video/ogg |
A video MIME type creates a stream asset (routed to a provider); anything else creates a
file asset (stored in R2). An unsupported type is rejected with 400 BAD_REQUEST.
Upload modes
The upload-url response tells you which mode to use via upload.upload_mode:
| Mode | For | How you upload |
|---|---|---|
single_presigned_put | Files below the multipart threshold | One PUT to upload_url. |
multipart_presigned_put | Large files (R2) | PUT each part URL, then complete with the ETags. |
tus | Bunny video | Resumable TUS upload to the provider endpoint. |
The service switches to multipart automatically once file_size_bytes exceeds
R2_MULTIPART_THRESHOLD (default 100 MB), with a part size of R2_MULTIPART_PART_SIZE
(default 10 MB). You can force a mode with the upload_strategy field (auto | single |
multipart). See Uploads for the full flows.
Streaming providers
Video (stream) assets are delivered by one of three providers, selected via stream_provider
(falling back to the service's DEFAULT_STREAM_PROVIDER):
| Provider | Upload | Transcode & delivery |
|---|---|---|
bunny | Resumable TUS to Bunny | Bunny Stream CDN (signed HLS) |
cloudflare | Presigned R2 PUT → Cloudflare Stream | Cloudflare Stream (signed HLS/DASH) |
onprem | Presigned R2 PUT | Your own encoder → HLS/DASH served back through this service |
Stream lifecycle
A video's stream_status advances as the provider works, driven by
webhooks:
pending_upload → processing → ready
└→ failedpending_upload— asset created, waiting for the bytes.processing— provider received the video and is transcoding.ready— playback URLs are available (playbackis populated).failed— transcoding failed; inspect provider logs.
Only when stream_status is ready will playback and batch
endpoints return usable URLs.