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)
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)
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")