All write endpoints (POST / PUT / PATCH / DELETE) require the API_KEY env var when set; pass it as Authorization: Bearer <key>.
POST /v1/uploads/presign
Generates a presigned URL for direct upload — to S3/R2 by default, or directly to Cloudflare Stream when the profile has delivery: stream.
Request body:
{
"key_base": "unique-file-id",
"ext": "jpg",
"mime": "image/jpeg",
"size_bytes": 1024000,
"kind": "image",
"profile": "avatar",
"multipart": "auto"
}| Field | Notes |
|---|---|
key_base |
Unique identifier. Ignored for delivery: stream profiles — Stream assigns the UID. |
ext |
File extension (used in storage_path template). |
mime |
Validated against profile allowed_mimes. |
size_bytes |
Validated against profile size_max_bytes. For Stream, also chooses POST (≤200MB) vs TUS. |
kind |
image or video. Must match the profile's kind. |
profile |
Name from storage-config.yaml. |
multipart |
auto / force / off. Ignored for Stream delivery. |
Response — single PUT (R2):
{
"object_key": "originals/avatars/ab/unique-file-id.jpg",
"upload": {
"single": {
"method": "PUT",
"url": "https://presigned-s3-url",
"headers": { "Content-Type": "image/jpeg", "If-None-Match": "*" },
"expires_at": "2024-01-01T12:00:00Z"
}
}
}Response — multipart (R2):
{
"object_key": "originals/avatars/ab/unique-file-id.jpg",
"upload": {
"multipart": {
"upload_id": "abc123xyz",
"part_size": 8388608,
"parts": [
{
"part_number": 1,
"method": "PUT",
"url": "https://presigned-s3-part-url-1",
"headers": { "Content-Type": "image/jpeg" },
"expires_at": "2024-01-01T12:00:00Z"
}
],
"complete": {
"method": "POST",
"url": "https://your-api/v1/uploads/.../complete/abc123xyz",
"headers": { "Content-Type": "application/json" },
"expires_at": "2024-01-01T12:00:00Z"
},
"abort": {
"method": "DELETE",
"url": "https://your-api/v1/uploads/.../abort/abc123xyz",
"headers": {},
"expires_at": "2024-01-01T12:00:00Z"
}
}
}
}Response — Cloudflare Stream:
{
"object_key": "8f2a3c4d5e6f7890abcdef1234567890",
"upload": {
"stream": {
"method": "POST",
"url": "https://upload.videodelivery.net/...",
"uid": "8f2a3c4d5e6f7890abcdef1234567890",
"expires_at": "2024-01-01T12:00:00Z"
}
}
}For delivery: stream, object_key is the Stream video UID — use it as the asset's stable handle for probe + delete. method is "POST" for ≤200MB, "TUS" for larger.
POST /v1/uploads/{object_key}/complete/{upload_id}
{
"parts": [
{ "part_number": 1, "etag": "\"d41d8cd98f00b204e9800998ecf8427e\"" },
{ "part_number": 2, "etag": "\"098f6bcd4621d373cade4e832627b4f6\"" }
]
}Response: { "status": "completed", "object_key": "..." }
DELETE /v1/uploads/{object_key}/abort/{upload_id}
Response: { "status": "aborted", "upload_id": "..." }
POST /v1/assets/{profile}/{key_base}/probe
Validates an uploaded video against the profile's constraint fields. Required: profile must be kind: video. Returns 200 for both pass and fail — ok is the gate, not the status code.
- R2 profiles — mediaflow runs
ffprobeover a presigned GET URL. - Stream profiles —
key_baseis the Stream UID; mediaflow reads metadata fromGET /accounts/{id}/stream/{uid}. Returns202with{ok: false, ready: false, state: "queued"|"inprogress"}if Stream is still encoding.
Pass:
{
"ok": true,
"ready": true,
"video": { "duration_seconds": 42.18, "width": 1920, "height": 1080, "codec": "h264" },
"reasons": []
}Fail:
{
"ok": false,
"video": { "duration_seconds": 67.4, "width": 854, "height": 480 },
"reasons": [
{ "code": "duration_exceeded", "limit": 45, "actual": 67.4 },
{ "code": "width_too_low", "limit": 1280, "actual": 854 }
]
}Reason codes: duration_exceeded, width_too_low, height_too_low, width_too_high, height_too_high, codec_not_allowed, no_video_stream.
Other status codes: 404 (asset/UID not found), 422 (profile is not kind: video), 502 (ffprobe crash or Stream API error).
DELETE /v1/assets/{profile}/{key_base}
R2 profiles: deletes the original + every object under thumb_folder/{key_base}*.
Stream profiles: key_base is the UID; mediaflow calls Stream's DELETE /stream/{uid}.
R2 response:
{ "status": "deleted", "profile": "avatar", "key_base": "abc123", "objects_deleted": 4 }Stream response:
{ "status": "deleted", "profile": "trailer", "uid": "8f2a3c4d5e6f7890..." }GET /thumb/{type}/{image_id}?width=512
POST /thumb/{type}/{image_id}
Generates and serves thumbnails. POST requires API_KEY auth.
GET parameters:
type: profile name (avatar, photo, banner, …)image_id: unique identifierwidth: pixels — defaults to the profile'sdefault_size
GET /originals/{type}/{image_id}
Serves the original from storage.
GET /health
Returns OK.