API

Quickstart

Version 2026-11-01OpenAPI 3.1Download openapi.json

Take a small video from your computer to a playable URL.

You will create a video, upload its bytes, and retrieve a player URL. Use a small, valid MP4 named trailer.mp4 for your first request.

Create your key

Open Developers, create a key, and save it when it appears. Set TODAYFILES_API_KEY in your server environment. Keep the key out of your browser code and source control.

Every request uses Authorization: Bearer followed by that key. Your first integration needs Node 20, Python 3, PHP 8.1, Go, or curl with Python 3 for JSON handling.

Create and upload your video

Create a video with an upload source. Replace the sample idempotency key with a unique value for this request. Keep it the same if you retry.

curl
response=$(curl --fail-with-body --request POST 'https://api.todayfiles.com/v1/videos' \
  --header "Authorization: Bearer $TODAYFILES_API_KEY" \
  --header 'Idempotency-Key: replace-with-a-unique-request-key' \
  --header 'Content-Type: application/json' \
  --data '{"title":"Launch trailer","source":{"type":"upload"}}')
printf '%s\n' "$response"
Node
const response = await fetch("https://api.todayfiles.com/v1/videos", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.TODAYFILES_API_KEY,
    "Idempotency-Key": "replace-with-a-unique-request-key",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"title":"Launch trailer","source":{"type":"upload"}}),
});
if (!response.ok) throw new Error(await response.text());
const result = response.status === 204 ? null : await response.json();
console.log(result);
Python
import json
import os
import requests

response = requests.request(
    "POST", "https://api.todayfiles.com/v1/videos",
    headers={
        "Authorization": "Bearer " + os.environ["TODAYFILES_API_KEY"],
        "Idempotency-Key": "replace-with-a-unique-request-key",
    },
    json=json.loads("{\"title\":\"Launch trailer\",\"source\":{\"type\":\"upload\"}}"),
    timeout=30,
)
response.raise_for_status()
print(response.json() if response.content else None)
PHP
<?php
$client = curl_init("https://api.todayfiles.com/v1/videos");
curl_setopt_array($client, [
    CURLOPT_CUSTOMREQUEST => "POST",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('TODAYFILES_API_KEY'),
        'Idempotency-Key: replace-with-a-unique-request-key',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => "{\"title\":\"Launch trailer\",\"source\":{\"type\":\"upload\"}}",
]);
$body = curl_exec($client);
if ($body === false) throw new RuntimeException(curl_error($client));
$status = curl_getinfo($client, CURLINFO_RESPONSE_CODE);
curl_close($client);
if ($status >= 400) throw new RuntimeException($body);
$result = $body === '' ? null : json_decode($body, true, 512, JSON_THROW_ON_ERROR);
print_r($result);
Go
package main

import (
    "encoding/json"
    "io"
    "net/http"
    "os"
    "strings"
    "time"
)

func main() {
    request, err := http.NewRequest("POST", "https://api.todayfiles.com/v1/videos", strings.NewReader("{\"title\":\"Launch trailer\",\"source\":{\"type\":\"upload\"}}"))
    if err != nil { panic(err) }
    request.Header.Set("Authorization", "Bearer " + os.Getenv("TODAYFILES_API_KEY"))
    request.Header.Set("Idempotency-Key", "replace-with-a-unique-request-key")
    request.Header.Set("Content-Type", "application/json")
    response, err := (&http.Client{Timeout: 30 * time.Second}).Do(request)
    if err != nil { panic(err) }
    defer response.Body.Close()
    body, err := io.ReadAll(io.LimitReader(response.Body, 1 << 20))
    if err != nil { panic(err) }
    if response.StatusCode >= 400 { panic(string(body)) }
    var result any
    if len(body) > 0 {
        if err := json.Unmarshal(body, &result); err != nil { panic(err) }
    }
    if err := json.NewEncoder(os.Stdout).Encode(result); err != nil { panic(err) }
}

The response contains video.id, the status waiting_for_upload, and an upload object. Read upload.max_bytes before sending the file. Pass upload.tus.metadata to your tus client unchanged; the library encodes it for the protocol.

The following snippets continue from that response. curl keeps the JSON in response; Node uses result, Python uses response.json(), and PHP uses $result in the same program. When combining the PHP snippets, keep only the first <?php opening tag. For Go, save the creation program and the upload program separately, then pipe the creation program's JSON output into the upload program. Install the tus client for your language first, as shown in the upload guide, which also covers durable resume, chunk sizes and expired targets.

curl
set -euo pipefail
FILE=trailer.mp4
readarray -t target < <(printf '%s' "$response" | python3 -c '
import sys, json, base64
u = json.load(sys.stdin)["upload"]
print(u["tus"]["endpoint"])
print(",".join(k+" "+base64.b64encode(v.encode()).decode()
               for k,v in u["tus"]["metadata"].items()))
print(u["max_bytes"])
')
size=$(stat -c %s "$FILE")
(( size <= target[2] )) || { echo 'File exceeds max_bytes'; exit 1; }
if [[ -z ${UPLOAD_URL:-} ]]; then
  UPLOAD_URL=$(curl --fail-with-body --silent --show-error \
    --request POST "${target[0]}" --output /dev/null \
    --header 'Tus-Resumable: 1.0.0' --header "Upload-Length: $size" \
    --header "Upload-Metadata: ${target[1]}" \
    --write-out '%header{location}')
  printf 'Resume URL: %s\n' "$UPLOAD_URL"
fi
offset=$(curl --fail-with-body --silent --show-error --head \
  "$UPLOAD_URL" --header 'Tus-Resumable: 1.0.0' \
  --output /dev/null --write-out '%header{upload-offset}')
while (( offset < size )); do
  count=$(( size - offset < 16777216 ? size - offset : 16777216 ))
  dd if="$FILE" iflag=skip_bytes,count_bytes skip="$offset" count="$count" status=none |
    curl --fail-with-body --silent --show-error --request PATCH "$UPLOAD_URL" \
      --header 'Tus-Resumable: 1.0.0' \
      --header 'Content-Type: application/offset+octet-stream' \
      --header "Upload-Offset: $offset" --data-binary @- --output /dev/null
  offset=$((offset + count))
done
Node
import fs from "node:fs";
import * as tus from "tus-js-client";

const filePath = "trailer.mp4";
const target = result.upload;
if (fs.statSync(filePath).size > target.max_bytes) throw new Error("File exceeds max_bytes");
const storage = new tus.FileUrlStorage(".upload-urls");
// Persist URL records without the temporary metadata values.
const urlStorage = {
  findAllUploads: () => storage.findAllUploads(),
  findUploadsByFingerprint: fp => storage.findUploadsByFingerprint(fp),
  removeUpload: key => storage.removeUpload(key),
  addUpload: (fp, { metadata, ...record }) => storage.addUpload(fp, record),
};
await new Promise((resolve, reject) => {
  const upload = new tus.Upload(fs.createReadStream(filePath), {
    endpoint: target.tus.endpoint,
    metadata: target.tus.metadata,
    chunkSize: target.tus.chunk_size.recommended,
    retryDelays: [0, 1000, 3000, 5000, 10000, 30000, 60000],
    storeFingerprintForResuming: true,
    removeFingerprintOnSuccess: true,
    urlStorage,
    onError: reject,
    onSuccess: resolve,
  });
  upload.findPreviousUploads().then(previous => {
    const match = previous.find(p => p.uploadUrl.startsWith(target.tus.endpoint));
    if (match) upload.resumeFromPreviousUpload(match);
    upload.start();
  }).catch(reject);
});
Python
import os
from tusclient import client
from tusclient.storage.filestorage import FileStorage

target = response.json()["upload"]
path = "trailer.mp4"
if os.path.getsize(path) > target["max_bytes"]:
    raise ValueError("File exceeds max_bytes")
# This store keeps fingerprints and upload URLs, without temporary metadata.
urls = FileStorage(".upload-urls.json")
try:
    uploader = client.TusClient(target["tus"]["endpoint"]).uploader(
        path, metadata=target["tus"]["metadata"],
        chunk_size=target["tus"]["chunk_size"]["recommended"],
        retries=6, retry_delay=10, store_url=True, url_storage=urls,
    )
    uploader.upload()
finally:
    urls.close()
PHP
<?php
require 'vendor/autoload.php';

$target = $result['upload'];
$path = 'trailer.mp4';
if (filesize($path) > $target['max_bytes']) throw new RuntimeException('File exceeds max_bytes');
$uploader = new \TusPhp\Tus\Client($target['tus']['endpoint']);
$uploader->setApiPath($target['tus']['endpoint']);
$uploader->setKey($result['video']['id'])->file($path);
// Replace the library's default metadata after file() sets it.
$uploader->setMetadata($target['tus']['metadata']);
do {
    $offset = $uploader->upload($target['tus']['chunk_size']['recommended']);
} while ($offset < filesize($path));
Go
package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "os"
    "time"
    tus "github.com/eventials/go-tus"
    "github.com/eventials/go-tus/memorystore"
)

func main() {
    var created struct {
        Upload struct {
            MaxBytes int64 `json:"max_bytes"`
            TUS struct {
                Endpoint string `json:"endpoint"`
                Metadata map[string]string `json:"metadata"`
            } `json:"tus"`
        } `json:"upload"`
    }
    if err := json.NewDecoder(os.Stdin).Decode(&created); err != nil { panic(err) }
    file, err := os.Open("trailer.mp4")
    if err != nil { panic(err) }
    defer file.Close()
    info, err := file.Stat()
    if err != nil { panic(err) }
    if info.Size() > created.Upload.MaxBytes { panic("File exceeds max_bytes") }
    urls, err := memorystore.NewMemoryStore()
    if err != nil { panic(err) }
    config := &tus.Config{ChunkSize: 16 << 20, Resume: true, Store: urls,
        HttpClient: &http.Client{Timeout: 90 * time.Second}}
    client, err := tus.NewClient(created.Upload.TUS.Endpoint, config)
    if err != nil { panic(err) }
    upload, err := tus.NewUploadFromFile(file)
    if err != nil { panic(err) }
    upload.Metadata = tus.Metadata(created.Upload.TUS.Metadata)
    uploader, err := client.CreateOrResumeUpload(upload)
    if err != nil { panic(err) }
    if err := uploader.Upload(); err != nil { panic(err) }
    fmt.Println("Bytes sent; wait for video.ready")
}

Retrieve your player URL

After the transfer finishes, your video moves through processing to ready. Request the video every five seconds or more during this first test:

curl
curl --fail-with-body --request GET 'https://api.todayfiles.com/v1/videos/h87x' \
  --header "Authorization: Bearer $TODAYFILES_API_KEY"
Node
const response = await fetch("https://api.todayfiles.com/v1/videos/h87x", {
  method: "GET",
  headers: {
    Authorization: "Bearer " + process.env.TODAYFILES_API_KEY,
  },
});
if (!response.ok) throw new Error(await response.text());
const result = response.status === 204 ? null : await response.json();
console.log(result);
Python
import json
import os
import requests

response = requests.request(
    "GET", "https://api.todayfiles.com/v1/videos/h87x",
    headers={
        "Authorization": "Bearer " + os.environ["TODAYFILES_API_KEY"],
    },
    timeout=30,
)
response.raise_for_status()
print(response.json() if response.content else None)
PHP
<?php
$client = curl_init("https://api.todayfiles.com/v1/videos/h87x");
curl_setopt_array($client, [
    CURLOPT_CUSTOMREQUEST => "GET",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('TODAYFILES_API_KEY'),
    ],
]);
$body = curl_exec($client);
if ($body === false) throw new RuntimeException(curl_error($client));
$status = curl_getinfo($client, CURLINFO_RESPONSE_CODE);
curl_close($client);
if ($status >= 400) throw new RuntimeException($body);
$result = $body === '' ? null : json_decode($body, true, 512, JSON_THROW_ON_ERROR);
print_r($result);
Go
package main

import (
    "encoding/json"
    "io"
    "net/http"
    "os"
    "time"
)

func main() {
    request, err := http.NewRequest("GET", "https://api.todayfiles.com/v1/videos/h87x", nil)
    if err != nil { panic(err) }
    request.Header.Set("Authorization", "Bearer " + os.Getenv("TODAYFILES_API_KEY"))
    response, err := (&http.Client{Timeout: 30 * time.Second}).Do(request)
    if err != nil { panic(err) }
    defer response.Body.Close()
    body, err := io.ReadAll(io.LimitReader(response.Body, 1 << 20))
    if err != nil { panic(err) }
    if response.StatusCode >= 400 { panic(string(body)) }
    var result any
    if len(body) > 0 {
        if err := json.Unmarshal(body, &result); err != nil { panic(err) }
    }
    if err := json.NewEncoder(os.Stdout).Encode(result); err != nil { panic(err) }
}

Replace h87x with your returned video id. When the status is ready, open playback.player_url or use playback.embed_html. While the video processes, playback is null. If it fails, read failure.reason.

For production, subscribe to video.ready instead of polling. The event contains the same complete video object.

What's next

Resume larger uploads, receive signed events, or explore every operation.