API reference

A JSON REST API over HTTPS. Everything the app does, you can do: upload, organize, publish and read your numbers. Base URL:

Base URL
https://getplayflow.co/api/v1
The API is in private beta. Endpoints marked planned below aren’t live yet. Ask us for a key and we’ll tell you what’s ready.

Authentication

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
curl https://getplayflow.co/api/v1/videos \\
  -H "Authorization: Bearer pf_live_9Kd2mQ..."
PrefixScope
pf_live_Full read and write on one workspace.
pf_read_Read-only. Safe for dashboards and reporting jobs.
Keys are workspace-scoped by design. An agency managing eight clients holds eight keys, so a leaked key can never reach another client’s library.

Errors

Conventional status codes, with a machine-readable code and a message written for a human to read in a log.

422 Unprocessable Entity
{
  "error": {
    "code": "visibility_invalid",
    "message": "visibility must be private, unlisted or public.",
    "field": "visibility",
    "request_id": "req_7c19f2a4"
  }
}
StatusMeaning
400Malformed request, usually invalid JSON.
401Missing, malformed or revoked key.
403Valid key, but the role can’t do this. See Members & roles.
404No such resource in this workspace. We don’t distinguish “missing” from “not yours”.
409Conflict, e.g. deleting a folder that still holds video.
422Understood, but a field is invalid. The field key tells you which.
429Rate limited. Honour Retry-After.

Always log request_id. Quote it to us and we can find the exact call.

Videos

GET/videos

List videos, newest first.

QueryTypeNotes
folder_idstringRestrict to one folder.
statusenumuploading, processing, ready, failed.
visibilityenumprivate, unlisted, public.
qstringTitle search.
limitinteger1–100. Default 20.
cursorstringFrom next_cursor on the previous page.
200 OK
{
  "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"
}
No provider hostname appears anywhere in the response. Storage can move without your integration noticing. That’s deliberate.
GET/videos/{id}

Fetch one video, including renditions[] and analytics totals.

PATCH/videos/{id}

Update title, description, visibility or folder_id. Send only what changes.

Request
{ "visibility": "unlisted" }
DELETE/videos/{id}

Queue deletion. Reversible for 30 days, then the file goes. Returns 202.

Folders

GET/folders

List folders as a flat array with parent_id; nest client-side.

POST/folders

Create a folder. name required, parent_id optional.

DELETE/folders/{id}

Delete an empty folder. 409 if it still holds video.

Analytics

GET/analytics/summary

Workspace totals for a period. from and to are ISO dates; default is the last 30 days.

200 OK
{
  "plays": 12480,
  "unique_viewers": 7904,
  "watch_time_seconds": 1483200,
  "avg_completion": 0.64
}
GET/analytics/videos/{id}

Per-video plays, watch time and completion, optionally by day with interval=day.

Rollups run hourly, so today’s numbers lag by up to an hour. What each figure actually counts is in Analytics.

Uploads

Two steps. Ask us for a ticket, then send the bytes straight to storage, so the file never passes through your server.

POST/uploads

Create an upload ticket. Returns a resumable upload_url and the video_id the file will become.

200 OK
{
  "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 groupLimit
Reads600 / minute
Writes120 / minute
Upload tickets60 / 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.