Skip to content

Videos API

This is the API reference for video resources. For an end-user-friendly walkthrough, see the Videos guide.

Video uploads return immediately with status: processing; transcoding (HLS variant generation, thumbnail extraction) happens asynchronously via Bunny.net (overseas) or Aliyun VOD (mainland). Poll GET /v1/videos/{id} until status: ready to get the playback URL.

Endpoint summary

MethodPathAuthTierDescription
POST/v1/videosBeareruploadUpload a new video
GET/v1/videosBearerreadList videos with cursor pagination
GET/v1/videos/{id}Bearer (or public if isPublic)readGet one video’s metadata + playback URL
PATCH/v1/videos/{id}BearermutationUpdate title / tags / isPublic / thumbnail
DELETE/v1/videos/{id}BearermutationDelete a video and all transcoded variants
GET/v1/videos/{id}/statusBearerreadLightweight status poll (returns just {status, progress})
POST/v1/videos/{id}/thumbnailBearer (multipart)mutationReplace auto-thumbnail with a custom image

The unified /v1/media endpoint (v0.12+) covers cross-type listing; videos can also be listed via that endpoint with ?type=video.


Upload

Terminal window
curl -X POST https://api.picora.me/v1/videos \
-H "Authorization: Bearer sk_live_YOUR_KEY" \
-F file=@trip.mp4 \
-F title="Italy 2026 trip" \
-F tags='["travel","2026"]' \
-F isPublic=true

Request

FieldTypeRequiredDescription
filebinary (multipart)yesVideo file. Accepted formats: MP4, MOV, MKV, WebM, AVI
titlestringnoDisplay title (defaults to filename)
tagsstring[] (JSON)noUp to 10 tags, ≤ 32 chars each
isPublicbooleannoDefault true; false requires signed URLs to play

Per-plan size and duration limits:

PlanMax file sizeMax duration
pro500 MB30 minutes
pro_plus2 GB2 hours

Video uploads require pro or pro_plus plan; trial accounts get 403 PLAN_REQUIRED.

Response (201, status=processing)

{
"success": true,
"data": {
"id": "abc123def45",
"title": "Italy 2026 trip",
"filename": "trip.mp4",
"sizeBytes": 104857600,
"status": "processing",
"isPublic": true,
"tags": ["travel", "2026"],
"createdAt": "2026-04-27T08:00:00.000Z",
"updatedAt": "2026-04-27T08:00:00.000Z"
}
}

The response is sent as soon as the original file is stored, before transcoding finishes. Continue with status polling below.

Errors

CodeStatusWhen
PLAN_REQUIRED403Trial / none plan trying to upload video
QUOTA_EXCEEDED (meta.type=media_storage)403Combined video+audio storage exhausted
VALIDATION_ERROR422File format unsupported / file too large / duration too long / tags malformed
RATE_LIMITED429Upload tier rate limit hit (60/min)

List

Terminal window
curl "https://api.picora.me/v1/videos?limit=20&tag=travel" \
-H "Authorization: Bearer sk_live_YOUR_KEY"

Query parameters

ParamTypeDefaultDescription
cursorstring""Pagination cursor; from previous nextCursor
limitint201-50
tagstringFilter by tag (single, exact match)
statusenumprocessing / ready / failed; multiple via comma
isPublicboolFilter by visibility

Response

{
"success": true,
"data": {
"items": [
{
"id": "abc123def45",
"title": "Italy 2026 trip",
"thumbnail_url": "https://media.picora.me/thumbnails/abc123def45.jpg",
"duration_seconds": 245,
"sizeBytes": 104857600,
"status": "ready",
"isPublic": true,
"tags": ["travel", "2026"],
"createdAt": "2026-04-27T08:00:00.000Z"
}
],
"nextCursor": "eyJpZCI6Il..."
}
}

Note: playback_url is not included in list responses — only single-item GET /v1/videos/{id} includes it (it requires reading per-video edge config and adds latency).


Get one

Terminal window
curl https://api.picora.me/v1/videos/abc123def45 \
-H "Authorization: Bearer sk_live_YOUR_KEY"

Response (status=ready)

{
"success": true,
"data": {
"id": "abc123def45",
"title": "Italy 2026 trip",
"filename": "trip.mp4",
"sizeBytes": 104857600,
"duration_seconds": 245,
"playback_url": "https://video.picora.me/abc123def45/playlist.m3u8",
"direct_url": "https://video.picora.me/abc123def45/original.mp4",
"thumbnail_url": "https://media.picora.me/thumbnails/abc123def45.jpg",
"status": "ready",
"isPublic": true,
"tags": ["travel", "2026"],
"createdAt": "2026-04-27T08:00:00.000Z",
"updatedAt": "2026-04-27T08:05:23.000Z"
}
}

playback_url is the HLS .m3u8 playlist — the recommended way to embed. direct_url is the original MP4, useful for downloads but lacks adaptive bitrate.

Anonymous access (public videos)

If isPublic: true, GET /v1/videos/{id} works without the Authorization header. Use this for sharing links to non-authenticated viewers.

For private videos, generate a signed URL via /v1/videos/{id}/sign (TTL configurable, default 1 hour).


Status polling {#get-status}

Lightweight endpoint to poll transcoding progress without fetching full metadata:

Terminal window
curl https://api.picora.me/v1/videos/abc123def45/status \
-H "Authorization: Bearer sk_live_YOUR_KEY"

Response

{
"success": true,
"data": {
"status": "processing",
"progress": 0.42,
"eta_seconds": 120
}
}

progress is 0.0–1.0. eta_seconds is best-effort, may be null if Bunny / Aliyun doesn’t report.

Poll every 5–10 seconds (don’t hammer; transcoding is minutes-scale). When status becomes ready, fetch full metadata via GET /v1/videos/{id} for the playback_url.

failed status

When transcoding fails (corrupt source, unsupported codec, etc.), status: "failed" with an error reason in the full metadata. The original file is retained for 7 days for support investigation, then auto-deleted.


Update

Terminal window
curl -X PATCH https://api.picora.me/v1/videos/abc123def45 \
-H "Authorization: Bearer sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Updated title",
"isPublic": false,
"tags": ["family"]
}'

Updatable fields: title / isPublic / tags. Cannot update file content (would require re-upload + new transcoding).


Replace thumbnail

Terminal window
curl -X POST https://api.picora.me/v1/videos/abc123def45/thumbnail \
-H "Authorization: Bearer sk_live_YOUR_KEY" \
-F file=@cover.jpg

Recommended size: 1280 × 720 (16:9), JPEG / PNG / WebP, ≤ 2 MB. Custom thumbnails count against your image storage quota (typical impact <50 KB).

To revert to auto-extracted thumbnail (taken at the 5-second mark), DELETE /v1/videos/{id}/thumbnail.


Delete

Terminal window
curl -X DELETE https://api.picora.me/v1/videos/abc123def45 \
-H "Authorization: Bearer sk_live_YOUR_KEY"

Effects:

  • Database row removed from med_media
  • Bunny.net / Aliyun VOD asked to delete all transcoded variants and original (asynchronous, may take minutes)
  • quota:media_storage:{userId} and quota:video_storage:{userId} decremented
  • Thumbnail file in object storage removed
  • CDN cache purged (best-effort)

Deletion is permanent — there’s no trash / restore.


Bandwidth degradation behavior

When the user’s monthly video bandwidth quota is exhausted, playback (not the API) auto-degrades:

Bandwidth usedStatusAPI behaviorPlayback behavior
0–100%oknormalAll quality variants served
100–120%degradednormal API responses with degraded status flagHLS playlist served only contains 360p tracks
> 120%suspendednormal API responses with suspended status flagplayback_url returns HTTP 451 — server side; clients receive friendly JSON body

This degradation is implemented at the playback edge, not the API layer. See Bandwidth & degradation for the user-facing perspective.


Webhooks

Bunny.net / Aliyun VOD send webhooks to /webhooks/bunny and /webhooks/aliyun-vod when transcoding completes. The Picora API verifies provider signatures, then:

  • Sets status: ready in the database
  • Populates playback_url and duration_seconds
  • Generates and stores the auto-thumbnail
  • Increments quota:video_storage:{userId} (storage was reserved at upload time; this is a no-op typically)

Webhook delivery failures are detected by a hourly reconciliation cron — see admin observability.