Skip to main content
An upload through the API goes down the exact path a dashboard upload takes: the file is encoded, the player publishes itself when encoding finishes, and the poster and transcript follow. You send the bytes and poll one status field. There is no publish call to forget. Every call here needs a token with videos:write, and the status poll needs videos:read. Send it as Authorization: Bearer tplt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (or the X-API-Key header). See Authentication.

The whole flow

1

Create the video

curl
201 Created
folder_id is optional and must be a folder in your workspace (see Folders). plain_mode is optional: true makes the video a silent looping clip with nothing on top of it (see Plain mode).
2

Send the file

Under 100 MB, one multipart request is enough:
curl
Anything larger, or anything sent over a connection you do not trust, goes through the resumable protocol below. Both answer 202 Accepted with the status object.
3

Poll the status

curl
Poll every 10 to 15 seconds. A short clip is usually ready within one to three minutes; an hour-long source can take 20 minutes or more.
4

Paste the embed

When status is ready, the response carries embed.html (the standard player) and embed.plain_html (the plain mode snippet). Paste either into your page.

The status object

Every upload call and GET /v1/videos/{video}/status return the same shape.
200 OK
string
One of four values.
  • waiting_for_upload: the video exists and no file has arrived.
  • processing: a file is on its way to the encoder or being encoded.
  • ready: every orientation you uploaded has finished encoding. embed is filled in.
  • error: an orientation failed. Its error says why, in a sentence you can show a person.
object
landscape and portrait, each null until a file is sent for it, then { status, progress, error }. progress is a percentage and moves in steps, not smoothly.
object | null
null until status is ready, because the player script does not exist before then and an embed would render an empty box.
The player publishes itself within about a minute of ready. If you load the embed the very second the status flips and see an empty box, reload once. Nothing is broken.

Resumable upload

For large files. Send the file in chunks, so a dropped connection costs one chunk instead of the whole file.
1

Init

integer
required
The video’s numeric id, from POST /v1/videos.
string
required
Max 255 characters.
integer
required
The file size in bytes, from 1 up to 5,368,709,120 (5 GB).
string
required
One of video/mp4, video/quicktime, video/x-m4v, video/x-msvideo, video/webm, video/ogg, video/3gpp, video/x-matroska.
string
default:"landscape"
landscape, or portrait for the mobile version of the same video.
curl
201 Created
The session lives 24 hours. Call /complete before expires_at, or the chunks expire and you start again from init.
2

Send chunks

PATCH the raw bytes of each chunk to upload_url, with Content-Type: application/offset+octet-stream and an Upload-Offset header holding the number of bytes already sent (start at 0). Send chunks of chunk_size (8 MB); the last one is whatever is left.
curl
200 OK
Send the returned offset as the next chunk’s Upload-Offset.
Re-sending a chunk you already sent is safe. The server sees an offset behind its own, writes nothing, and returns 200 with the real offset. So after any network error, resend the same chunk.
Lost track of where you were (your process crashed)? Ask:
curl
200 OK
3

Complete

curl
202 Accepted
Complete checks that every byte arrived and that the file really is a video (the bytes are checked, not the mime_type you declared), then hands it to the encoder and fires the video.uploaded webhook. From here, poll the status.If bytes are missing you get 409 INCOMPLETE with the real offset, and the session stays open: send the rest and call complete again.

Abort

curl
200 OK
Deletes whatever chunks you sent. It returns {"deleted": true} for a session that already expired too.

Single-shot upload

integer
required
The video’s numeric id.
file
required
Multipart form field. Same types as the resumable protocol. One request can carry at most 3,000 MB: a larger body is refused with 413 before it reaches TrackPlay.
string
default:"landscape"
landscape or portrait.
curl
202 Accepted
A dropped single-shot request costs you the whole file. Use single-shot for clips under 100 MB and the resumable protocol for anything bigger.

Replacing the file

Upload to a video that already has a file in that orientation and the new file replaces the old one. The video keeps its id, embed code, settings and analytics. If the running time changed, the transcript is read again from the new file.

Folders

File uploads into a folder so you can find them in the dashboard. A folder changes where a video is listed and nothing about how it plays.
curl
201 Created
GET /v1/folders (scope videos:read) lists them with a videos_count. Pass parent_id to nest one folder in another. Move an existing video with PATCH /v1/videos/{video} and { "folder_id": 42 }.

Limits

Errors

Every error has the shape {"error": {"code": "...", "message": "..."}}. Chunk and complete errors also carry the server’s real offset, so you always know where to resume.
The token is missing, wrong, expired or revoked.
Uploads need videos:write.
No video with that id in the token’s workspace.
The session does not exist, expired, or belongs to another workspace.
A chunk had no Upload-Offset header, carried zero bytes, or went past the size you declared at init.
Your Upload-Offset is ahead of what the server has, so a chunk was lost. Resend from the offset in the response.
Complete was called before every byte arrived. Send the rest from offset.
The bytes are not a video, whatever the declared type said. The session is closed.
Laravel’s shape: {"message": "...", "errors": {"size": ["..."]}}. Usually a file over 5 GB or a type that is not on the list.
The folder_id is not a folder in your workspace.
The server cannot take a file that size right now. Retry later. Nothing was saved.
status is error and the orientation’s error explains it, for example a corrupt or truncated file. Upload a fresh copy to the same video.
Rate limited. Wait for the time in the Retry-After header.