Skip to content

Uploading a video

Videos in PlusPlus are hosted via Mux. The API exposes the upload as a chained flow: create the content item, get a direct upload URL, upload the file, then poll until the asset is ready.

Flow at a glance

sequenceDiagram
    participant Your app
    participant PlusPlus API
    participant Mux
    Your app->>PlusPlus API: POST /videos
    PlusPlus API-->>Your app: 201 { public_id }
    Your app->>PlusPlus API: POST /videos/{public_id}/uploads
    PlusPlus API-->>Your app: 201 { upload_url }
    Your app->>Mux: PUT (binary upload)
    Mux-->>Your app: 200
    Your app->>PlusPlus API: GET /videos/{public_id}
    PlusPlus API-->>Your app: 200 { playback: { ready: true } }

1. Create the video record

curl -X POST https://acme.plusplus.app/public_api/v2/videos \
  -H "Authorization: Bearer pp_your_token_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Onboarding: Day One",
    "duration_minutes": 12,
    "is_hidden": true
  }'
{
  "data": {
    "public_id": "01HXY8C5FZ7N4S0M2D3J6Q7K9P",
    "name": "Onboarding: Day One",
    "is_hidden": true,
    "playback": null
  }
}

Record the public_id — you'll use it for every subsequent step.

2. Request a direct upload URL

curl -X POST https://acme.plusplus.app/public_api/v2/videos/01HXY8C5FZ7N4S0M2D3J6Q7K9P/uploads \
  -H "Authorization: Bearer pp_your_token_here"
{
  "data": {
    "upload_id": "upl_abcdef",
    "upload_url": "https://storage.googleapis.com/mux-uploads/...",
    "expires_at": "2026-04-29T13:00:00Z"
  }
}

The upload_url is a single-use, time-limited URL issued by Mux. It does not require the bearer token.

3. Upload the file directly to Mux

curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: video/mp4" \
  --data-binary @path/to/onboarding.mp4

Use a streaming upload for files larger than a few hundred MB.

4. Wait for the asset to be ready

Mux transcoding is asynchronous. Poll GET /videos/{public_id} until playback.ready is true:

curl https://acme.plusplus.app/public_api/v2/videos/01HXY8C5FZ7N4S0M2D3J6Q7K9P \
  -H "Authorization: Bearer pp_your_token_here"
{
  "data": {
    "public_id": "01HXY8C5FZ7N4S0M2D3J6Q7K9P",
    "playback": {
      "ready": true,
      "playback_id": "abc123",
      "duration_seconds": 720
    }
  }
}

Or, subscribe to the video.updated webhook and react when the playback fields appear — no polling needed.

5. Publish

When you're satisfied, flip visibility:

curl -X PATCH https://acme.plusplus.app/public_api/v2/videos/01HXY8C5FZ7N4S0M2D3J6Q7K9P \
  -H "Authorization: Bearer pp_your_token_here" \
  -H "Content-Type: application/json" \
  -d '{ "is_hidden": false }'

The video is now visible in the catalog and can be assigned to learners or added to tracks.

Common pitfalls

  • The upload URL expired. They're valid for ~1 hour. Request a new one with POST /videos/{public_id}/uploads and try again.
  • Polling forever. Long videos take minutes to transcode. Switch to webhooks if your worker can't sit on a poll loop.
  • Setting playback directly. The playback object is read-only — Mux owns it.