API reference
A JSON REST API over HTTPS. Everything the app does, you can do: upload, organize, publish and read your numbers. Base URL:
https://getplayflow.co/api/v1Authentication
Send a workspace API key as a bearer token. Keys are created in Settings → API keys, are scoped to one workspace, and are shown once.
curl https://getplayflow.co/api/v1/videos \\
-H "Authorization: Bearer pf_live_9Kd2mQ..."| Prefix | Scope |
|---|---|
pf_live_ | Full read and write on one workspace. |
pf_read_ | Read-only. Safe for dashboards and reporting jobs. |
Errors
Conventional status codes, with a machine-readable code and a message written for a human to read in a log.
{
"error": {
"code": "visibility_invalid",
"message": "visibility must be private, unlisted or public.",
"field": "visibility",
"request_id": "req_7c19f2a4"
}
}| Status | Meaning |
|---|---|
400 | Malformed request, usually invalid JSON. |
401 | Missing, malformed or revoked key. |
403 | Valid key, but the role can’t do this. See Members & roles. |
404 | No such resource in this workspace. We don’t distinguish “missing” from “not yours”. |
409 | Conflict, e.g. deleting a folder that still holds video. |
422 | Understood, but a field is invalid. The field key tells you which. |
429 | Rate limited. Honour Retry-After. |
Always log request_id. Quote it to us and we can find the exact call.
Videos
List videos, newest first.
| Query | Type | Notes |
|---|---|---|
folder_id | string | Restrict to one folder. |
status | enum | uploading, processing, ready, failed. |
visibility | enum | private, unlisted, public. |
q | string | Title search. |
limit | integer | 1–100. Default 20. |
cursor | string | From next_cursor on the previous page. |
{
"data": [
{
"id": "NiU95FjGxt",
"title": "Onboarding tour",
"status": "ready",
"visibility": "public",
"duration": 272, // seconds
"width": 1920,
"height": 1080,
"size_bytes": 191365939,
"folder_id": "fld_2Kd8",
"thumbnail_url": "https://getplayflow.co/t/NiU95FjGxt.jpg",
"share_url": "https://getplayflow.co/v/NiU95FjGxt",
"embed_url": "https://getplayflow.co/e/NiU95FjGxt",
"created_at": "2026-09-08T14:02:11Z"
}
],
"next_cursor": "eyJpZCI6Ik5pVTk1"
}Fetch one video, including renditions[] and analytics totals.
Update title, description, visibility or folder_id. Send only what changes.
{ "visibility": "unlisted" }Queue deletion. Reversible for 30 days, then the file goes. Returns 202.
Folders
List folders as a flat array with parent_id; nest client-side.
Create a folder. name required, parent_id optional.
Delete an empty folder. 409 if it still holds video.
Analytics
Workspace totals for a period. from and to are ISO dates; default is the last 30 days.
{
"plays": 12480,
"unique_viewers": 7904,
"watch_time_seconds": 1483200,
"avg_completion": 0.64
}Per-video plays, watch time and completion, optionally by day with interval=day.
Uploads
Two steps. Ask us for a ticket, then send the bytes straight to storage, so the file never passes through your server.
Create an upload ticket. Returns a resumable upload_url and the video_id the file will become.
{
"video_id": "eD5sYb7PkL",
"upload_url": "https://upload.getplayflow.co/t/9Kd2...",
"expires_at": "2026-09-10T21:00:00Z"
}PUT the file to upload_url in chunks. Interrupted uploads resume from the last acknowledged byte. Poll GET /videos/{id} until status is ready, or take a webhook instead of polling.
Rate limits
| Endpoint group | Limit |
|---|---|
| Reads | 600 / minute |
| Writes | 120 / minute |
| Upload tickets | 60 / minute |
Every response carries X-RateLimit-Remaining and X-RateLimit-Reset. On a 429, wait for Retry-After. Retrying immediately just extends the window.
Versioning
The version is in the path. Within v1 we only ever add fields, so treat unknown fields as ignorable rather than validating strictly. Anything that would break an integration ships as v2 with at least six months of overlap.