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 inyyyyMMddform (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 (
Zor±hh:mm), e.g. jobcreationDate/startDate/finishDate, uploadexpiresAt, and thesincequery 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
namesceneshottakedescriptioncircledratinginPointcameratimecodefpsdate—yyyyMMddstring only (e.g.20260422); not an ISO 8601 timestampdurationreelNamewhitePointKelvinwhitePointCcShiftshutterfilterndFilterlensfocalLengthfocusDistanceasasensorFpsfStoptStopfileSizelibraryPath
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
idtype— see Job types.state— see Job state.namereadFlag— responses use0or1; PATCH may send a boolean.remainingremainingSecondsbytesPerSecondprogress— 0.0 through 1.0pathcreationDatestartDatefinishDate
Job state
state must be one of:
scheduledrunningsucceededfailedabortedpausedsuspendedunscheduledwillSuspendwillResumedoNotQueueexecutingCompletionunknown
Job types
type is usually one of:
CopyJobOffloadJobRenderJobRelinkJobVerifyJobUploadJobDailiesUploadJobunknown
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 (.jpgfiles)_audio/: Replacement audio for an existing clip (.m4a) - see Replacing a clip's audio
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.jpgextension) - 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).m4a: Replacement audio (in_audio/) - AAC, stereo, MP4 container, duration must match the existing clip exactly
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.
Replacing a clip's audio
Request a signed URL with path = _audio/{baseName}.m4a, where {baseName} matches an existing clip's name (e.g. _audio/A001C001_260422_R3CK.m4a). Once the file lands, the backend probes it, validates it, and remuxes the existing video with the new audio.
The file must satisfy:
- AAC codec, stereo (2 channels), MP4 container with
.m4aextension - Duration matches the existing clip's video duration within 1 ms
After a successful remux the clip's video URL changes, so clients automatically pick up the new audio without browser or CDN caching delays. If validation fails, the upload is rejected and the existing clip is left untouched.
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")