Skip to content

Uploading recordings

After a live session, you can attach a recording to its event series so learners can watch it back on demand. Recordings are hosted via Mux, and the upload is a four-step flow: request an upload URL, push the file straight to Mux, confirm, then poll until transcoding finishes.

Recordings attach to an event series, not to an individual event — so a recording becomes available across the series' occurrences.

Flow at a glance

sequenceDiagram
    participant Your app
    participant PlusPlus API
    participant Mux
    Your app->>PlusPlus API: POST /event-series/{id}/recordings/uploads/
    PlusPlus API-->>Your app: 201 { upload_id, upload_url }
    Your app->>Mux: PUT (binary upload)
    Mux-->>Your app: 200
    Your app->>PlusPlus API: POST .../uploads/{upload_id}/confirm/
    PlusPlus API-->>Your app: 202 { status: "processing" }
    loop until ready or errored
        Your app->>PlusPlus API: GET .../uploads/{upload_id}/status/
        PlusPlus API-->>Your app: 200 { status, playback_url }
    end

1. Request an upload URL

No request body — just identify the series:

curl -X POST https://acme.plusplus.app/api/v2/event-series/6ba7b810-…/recordings/uploads/ \
  -H "Authorization: Bearer pp_your_token_here"
{
  "upload_id": "BzQ8L7hY9JKLM01jw02HcXdYz3a4bC5dE",
  "upload_url": "https://storage.googleapis.com/video-storage-…",
  "upload_type": "mux_direct",
  "event_series_id": "6ba7b810-…",
  "expires_at": "2026-03-18T19:30:00Z",
  "status": "awaiting_upload"
}

The upload_url is a single-use, time-limited URL issued by Mux. It does not carry your bearer token. The whole session expires in about an hour, so upload promptly.

2. Upload the file directly to Mux

PUT the video straight to upload_url with a video/* content type. Your file goes to Mux, not through the PlusPlus API — so multi-GB recordings don't run through our gateway:

curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: video/mp4" \
  --data-binary @./eng-sync-mar-18.mp4

Use a streaming/resumable upload for large files.

3. Confirm the upload

Once the bytes are in Mux, tell PlusPlus to link the asset to the series. This kicks off transcoding asynchronously:

curl -X POST https://acme.plusplus.app/api/v2/event-series/6ba7b810-…/recordings/uploads/BzQ8L7hY9JKLM01jw02HcXdYz3a4bC5dE/confirm/ \
  -H "Authorization: Bearer pp_your_token_here"
{
  "upload_id": "BzQ8L7hY9JKLM01jw02HcXdYz3a4bC5dE",
  "event_series_id": "6ba7b810-…",
  "status": "processing",
  "message": "Recording upload confirmed. Transcoding in progress."
}

Confirm returns 202 Accepted. Two error cases to handle:

  • 409 Conflict — this upload was already confirmed. Treat it as success and move to polling.
  • 410 Gone — the upload session expired (~1 hour after creation). Start over from step 1 with a fresh URL.

4. Poll until ready

Poll the status endpoint until status is terminal:

curl https://acme.plusplus.app/api/v2/event-series/6ba7b810-…/recordings/uploads/BzQ8L7hY9JKLM01jw02HcXdYz3a4bC5dE/status/ \
  -H "Authorization: Bearer pp_your_token_here"
{
  "upload_id": "BzQ8L7hY9JKLM01jw02HcXdYz3a4bC5dE",
  "event_series_id": "6ba7b810-…",
  "status": "ready",
  "message": "Recording is ready for playback.",
  "playback_url": "https://stream.mux.com/abc123.m3u8?token=eyJ…",
  "asset_id": "abc123xyz789"
}

The status field moves through:

status Meaning
awaiting_upload The URL was issued; Mux hasn't received the file yet.
processing File received; Mux is transcoding. Usually seconds to a few minutes.
ready Done. playback_url (a signed M3U8) and asset_id are populated.
errored Transcoding failed. Surface message and re-upload to retry.

Poll every 3–5 seconds — or, to avoid polling entirely, subscribe to the event_type.recording_ready (and event_type.recording_errored) webhooks and react when transcoding finishes. (Recordings attached to a single event fire event.recording_ready / event.recording_errored instead.)

Playback URLs expire

playback_url is a signed URL valid for one hour. Don't cache it long-term — request the status endpoint again to mint a fresh one when you need to play the recording. After the upload session itself expires (~1 hour after creation), the status endpoint returns 404; the series and its recording remain available through GET /event-series/{public_id}/.

Common pitfalls

  • Skipping the confirm step. Uploading to Mux isn't enough — without POST …/confirm/, the recording is never linked to the series.
  • Caching playback_url. It's good for an hour. Re-fetch status to refresh it rather than storing it.
  • Attaching to an event instead of a series. Recordings live on the event series. Use the series' public_id in the path.
  • Letting the session expire mid-upload. The whole flow has a ~1-hour window. For very large files, make sure your upload finishes well inside it, then confirm immediately.
  • Treating errored as fatal. It usually means a bad codec or corrupt source. Surface message to the operator and re-run the flow from step 1.

This is the same direct-upload pattern used for native videos, with an added explicit confirm step.