Skip to content

API

The o/DAILIES HTTP API is JSON over HTTPS: read and update clip and job metadata, and request signed upload URLs. Each request includes your project and token in the path as documented below.

  • Base URL: https://odailies.com/api/v2
  • Only use property names listed in this document in PATCH bodies; unknown keys return 400.
  • Clip date: stored as a string in yyyyMMdd form (UTC calendar day, e.g. 20260422). This matches the o/DAILIES clients and the contract documented here; other formats (including ISO 8601 datetimes) are rejected with 400.
  • Other date/time fields: ISO 8601 with a timezone (Z or ±hh:mm), e.g. job creationDate / startDate / finishDate, upload expiresAt, and the since query parameter on jobs.
  • Clips (without jobs) are also available on /api/v1; see Legacy.

Authentication

Create an API token in the o/DAILIES web app (Settings). Each token can include:

Permission What it allows
Read List clip metadata; GET jobs for the project
Write Update clip metadata; PATCH jobs (some state changes may send a push; see Push notifications)
Upload Get signed upload URLs (Upload)

Clips

Endpoints

Bulk:

GET /api/v2/clips/{project_id}/{token}
PATCH /api/v2/clips/{project_id}/{token}

Single clip (the path segment is the clip name):

GET /api/v2/clips/{project_id}/{clip_name}/{token}
PATCH /api/v2/clips/{project_id}/{clip_name}/{token}

List clips (GET)

Returns every clip, sorted by name.

{
  "data": [
    {
      "name": "A001C001_260422_R3CK",
      "scene": "22",
      "shot": "4",
      "take": "1",
      "description": "",
      "circled": false,
      "rating": 0,
      "inPoint": null,
      "camera": "A",
      "timecode": "10:00:00:00",
      "fps": 23.976,
      "date": "20260422",
      "duration": 12.5,
      "reelName": "A001",
      "libraryPath": "Project/Clips"
    }
  ]
}

Objects may include more fields; see Clip properties. Omitted or null fields are simply not set in storage.

Update clips (PATCH)

Request body (array of partial clip objects, each with at least name):

{
  "data": [
    { "name": "A001C001_260422_R3CK", "scene": "22", "circled": true }
  ]
}

Response:

{
  "data": {
    "updated": 1,
    "projectId": "your_project_id",
    "names": ["A001C001_260422_R3CK"]
  }
}

Duplicate name values in a single request return 400.

Clip properties

  • name
  • scene
  • shot
  • take
  • description
  • circled
  • rating
  • inPoint
  • camera
  • timecode
  • fps
  • dateyyyyMMdd string only (e.g. 20260422); not an ISO 8601 timestamp
  • duration
  • reelName
  • whitePointKelvin
  • whitePointCcShift
  • shutter
  • filter
  • ndFilter
  • lens
  • focalLength
  • focusDistance
  • asa
  • sensorFps
  • fStop
  • tStop
  • fileSize
  • libraryPath

Jobs

Endpoints

Bulk:

GET /api/v2/jobs/{project_id}/{token}
GET /api/v2/jobs/{project_id}/{token}?since=2026-01-01T00:00:00Z
PATCH /api/v2/jobs/{project_id}/{token}

Single job:

GET /api/v2/jobs/{project_id}/{job_id}/{token}
PATCH /api/v2/jobs/{project_id}/{job_id}/{token}

since (optional, ISO 8601) returns only jobs with activity at or after that instant. Results are newest first.

List jobs (GET)

{
  "data": [
    {
      "id": "2ACA5667-FDE2-4581-91A7-DA4A23F49B3F",
      "type": "CopyJob",
      "name": "B_0011_1DYJ_hde",
      "state": "succeeded",
      "readFlag": 0,
      "remaining": "",
      "remainingSeconds": null,
      "bytesPerSecond": null,
      "progress": 1,
      "path": null,
      "creationDate": "2026-04-22T11:02:25Z",
      "startDate": "2026-04-22T11:02:26Z",
      "finishDate": "2026-04-22T11:07:07Z"
    }
  ]
}

Update jobs (PATCH)

Request (each object needs id and at least one updatable field):

{
  "data": [
    { "id": "2ACA5667-FDE2-4581-91A7-DA4A23F49B3F", "state": "running", "readFlag": 1 }
  ]
}

Response:

{
  "data": {
    "updated": 1,
    "projectId": "your_project_id",
    "ids": ["2ACA5667-FDE2-4581-91A7-DA4A23F49B3F"]
  }
}

Job properties

  • id
  • type — see Job types.
  • state — see Job state.
  • name
  • readFlag — responses use 0 or 1; PATCH may send a boolean.
  • remaining
  • remainingSeconds
  • bytesPerSecond
  • progress — 0.0 through 1.0
  • path
  • creationDate
  • startDate
  • finishDate

Job state

state must be one of:

  • scheduled
  • running
  • succeeded
  • failed
  • aborted
  • paused
  • suspended
  • unscheduled
  • willSuspend
  • willResume
  • doNotQueue
  • executingCompletion
  • unknown

Job types

type is usually one of:

  • CopyJob
  • OffloadJob
  • RenderJob
  • RelinkJob
  • VerifyJob
  • UploadJob
  • DailiesUploadJob
  • unknown

Push notifications

Some transitions send a push to the user linked to the token. In-progress states usually do not; completed or failed outcomes often do (server rules follow the same idea as the app’s sync behaviour).


Upload

Request a time-limited signed URL with GET:

GET https://odailies.com/api/v2/upload/{project_id}/{token}/{path}

{path} is the object key after your user prefix in the upload bucket, and may include slashes (e.g. _thumbnails/ClipName.jpg). A single final path segment with URL-encoded slash (e.g. _thumbnails%2FClipName.jpg) is also accepted.

Thumbnails in particular must be addressed as a folder and file_thumbnails/ClipName.jpg where ClipName matches the clip’s name. Do not use a single flat filename like _thumbnails_ClipName.jpg (no slash): that does not sit under the _thumbnails/ directory, so the upload pipeline will not treat it as a clip thumbnail.

Example response (values are real at request time; url is a time-limited signed URL whose host is whatever the API returns):

{
  "url": "https://…",
  "expiresAt": "2026-12-30T08:34:05Z"
}

expiresAt is ISO 8601 in UTC. Use PUT with the returned url to upload the file body (Content-Type: application/octet-stream is typical for raw bytes).

The signed URL is valid for 15 minutes.

Note

If an upload has started and transferred at least ~100KB before expiration, it will be allowed to complete.

Info

Make sure your video files meet our specifications before uploading.

Available directories

The path parameter should follow the o/DAILIES directory structure:

  • _graded/: For your graded dailies clips
  • _log/: For LOG variants of clips - allows users with "Advanced Video options" to switch video sources (learn more)
  • _thumbnails/: For thumbnail images (.jpg files)

See the uploading documentation for more details about file handling.

File naming

o/DAILIES uses filenames as unique identifiers to link related files together. All files related to the same clip must share the same base filename:

  • The graded clip in _graded/ defines the base filename
  • Its LOG variant in _log/ must use the exact same filename
  • Its thumbnail in _thumbnails/ must use the same filename (with .jpg extension)
  • Any metadata (like ALE files) must reference the same filename

Example:

_graded/A001C001_260422_R3CK.mp4
_log/A001C001_260422_R3CK.mp4
_thumbnails/A001C001_260422_R3CK.jpg

Supported file types

  • .mp4: Video files (in _graded/ or _log/) - see video file specifications
  • .jpg: Thumbnail images (in _thumbnails/)
  • .ale: Metadata files (processed and automatically removed after import)

Thumbnails

Request a signed URL with path = _thumbnails/{baseName}.jpg, where {baseName} is the same base filename as the clip’s name (e.g. _thumbnails/A001C001_260422_R3CK.jpg). The HTTP request can use a normal slash between _thumbnails and the file name, as in the File naming examples.

Upload a .jpg when you want a preview before the video was uploaded. After _graded/ is processed, a preview is often generated from the video after some delay, so supplying _thumbnails/ yourself is the way to have a preview as soon as the project has data.


Error handling

Code Meaning
200 Success
400 Bad request (format, parameters, or body)
401 Unauthorized (invalid token)
403 Forbidden (insufficient permissions)
405 Method not allowed
500 Server error

Error body:

{
  "error": {
    "message": "Error description"
  }
}

Code example (Python)

The snippet below is a small client: list clips, patch clips, list/update jobs, and upload a file with a signed URL. Replace your_project_id and your_token with real values from Settings.

import json
from urllib.parse import quote

import requests


class oDailiesClient:
    """Clips, jobs, and signed upload URLs."""

    def __init__(self, project_id: str, token: str):
        self.project_id = project_id
        self.token = token
        self.base_url = "https://odailies.com/api/v2"
        self.clips_bulk = f"{self.base_url}/clips/{self.project_id}/{self.token}"
        self.jobs_bulk = f"{self.base_url}/jobs/{self.project_id}/{self.token}"
        self.upload_endpoint = f"{self.base_url}/upload"

    def get_clips(self) -> list:
        response = requests.get(self.clips_bulk)
        response.raise_for_status()
        return response.json()["data"]

    def get_jobs(self, since_iso: str | None = None) -> list:
        url = self.jobs_bulk
        if since_iso:
            url = f"{url}?since={quote(since_iso, safe='')}"
        response = requests.get(url)
        response.raise_for_status()
        return response.json()["data"]

    def update_jobs(self, jobs: list) -> dict:
        response = requests.patch(
            self.jobs_bulk,
            json={"data": jobs},
            headers={"Content-Type": "application/json"},
        )
        response.raise_for_status()
        return response.json()

    def update_clips(self, clips: list) -> dict:
        response = requests.patch(
            self.clips_bulk,
            json={"data": clips},
            headers={"Content-Type": "application/json"},
        )
        response.raise_for_status()
        return response.json()

    def upload_file(self, file_path: str, destination: str):
        """Upload a file using signed URL."""
        response = requests.get(
            f"{self.upload_endpoint}/{self.project_id}/{self.token}/{destination}"
        )
        response.raise_for_status()
        upload_url = response.json()["url"]

        with open(file_path, "rb") as f:
            response = requests.put(
                upload_url,
                data=f,
                headers={"Content-Type": "application/octet-stream"},
            )
            response.raise_for_status()


client = oDailiesClient("your_project_id", "your_token")

clips = client.get_clips()
print(json.dumps(clips, indent=2))

client.update_clips(
    [
        {
            "name": "A001C001_260422_R3CK",
            "scene": "22",
            "shot": "4",
            "take": "1",
        },
        {
            "name": "A001C002_260422_R3CK",
            "scene": "22",
            "shot": "4",
            "take": "2",
            "circled": True,
        },
    ]
)

client.upload_file("A001C001_260422_R3CK.mp4", "_graded/A001C001_260422_R3CK.mp4")
client.upload_file("A001C001_260422_R3CK.mp4", "_log/A001C001_260422_R3CK.mp4")
client.upload_file("A001C001_260422_R3CK.jpg", "_thumbnails/A001C001_260422_R3CK.jpg")
client.upload_file("metadata.ale", "metadata.ale")