API documentation
Manage your videos, ask questions, and build applications with the Platform API. Explore the operations below for request details, responses, and examples.
No matching documentation. Try a different search.
Overview
The Platform API (https://platform.endlessai.com/api/v1) lets you manage projects, upload and process videos, run AI agent analysis, manage documents and alerts, and stream real-time events.
https://platform.endlessai.com/api/v1
All list endpoints return data in an envelope:
{
"data": [...],
"meta": { "total": 42, "returned": 20, "hasMore": true }
}Authentication
Use a project API key or a personal access token. The signed-in app uses its session cookie.
X-Api-Key: svk_...
API keys are project-bound. All requests are automatically scoped to the key's project.
Authorization: Bearer <user-token-or-api-key>
Accepts personal access tokens (svt_), project keys (svk_), and session JWTs. User tokens can access the projects their owner is permitted to use.
?access_token=<jwt>
For EventSource/SSE connections that can't set custom headers. Use a user token or session JWT; project keys must be sent in a header.
Roles & Permissions
Read operations: any project member. Write operations (POST, PUT, DELETE): requires owner, editor, admin, or api role.
# Using API Key header
curl -H "X-Api-Key: svk_abc123" \
https://platform.endlessai.com/api/v1/projects
# Using Bearer token
curl -H "Authorization: Bearer eyJhbG..." \
https://platform.endlessai.com/api/v1/projectsimport requests
API_KEY = "svk_abc123"
BASE = "https://platform.endlessai.com/api/v1"
resp = requests.get(f"{BASE}/projects",
headers={"X-Api-Key": API_KEY})
resp.raise_for_status()
print(resp.json())const API_KEY = "svk_abc123";
const BASE = "https://platform.endlessai.com/api/v1";
const res = await fetch(`${BASE}/projects`, {
headers: { "X-Api-Key": API_KEY }
});
const data = await res.json();Errors & Responses
All errors return JSON with an error field:
{
"error": "Human-readable error message"
}Project key API
A short set of calls scoped to the project your key belongs to — no project ID in the path. Add videos, images and text, then check their status and moderation result. Requires a project key (svk_).
GET/api/v1/meCheck your credential
Check your credential
Shows who the request runs as. project_id is set for a project key, user_id for a user token. Works with any credential.
{
"auth_method": "api_key",
"project_id": "PROJECT_ID",
"user_id": null,
"capabilities": "project"
}POST/api/v1/videosAdd a video and start its analysis
Add a video and start its analysis
Upload a file (multipart field file) or send JSON with sourceType: "url" and a url. Use sourceType: "rtsp" to add a live RTSP or HLS camera. Optional: name, includeInQA (default true). Returns 201.
To make retries safe, send your own external_id with a source_id: a repeat returns the existing video with duplicate: true, and a different content_hash for the same ID returns 409.
# Upload a file
curl -X POST https://platform.endlessai.com/api/v1/videos \
-H "X-Api-Key: YOUR_KEY" \
-F "file=@/path/to/video.mp4" \
-F "name=Loading dock"
# Add from a URL
curl -X POST https://platform.endlessai.com/api/v1/videos \
-H "X-Api-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"sourceType": "url", "url": "https://example.com/video.mp4"}'import requests
with open("video.mp4", "rb") as f:
resp = requests.post(
f"{BASE}/videos",
headers={"X-Api-Key": API_KEY},
files={"file": ("video.mp4", f, "video/mp4")},
data={"name": "Loading dock"},
)
video_id = resp.json()["videoId"]const res = await fetch(`${BASE}/videos`, {
method: "POST",
headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
sourceType: "url",
url: "https://example.com/video.mp4",
name: "Loading dock",
}),
});
const { videoId } = await res.json();// Response (201 Created)
{
"videoId": "VIDEO_ID",
"projectId": "PROJECT_ID",
"status": "pending",
"sourceType": "offline"
}GET/api/v1/videos/:videoId/statusGet a video's processing status
Get a video's processing status
Poll until status is completed. processing lists the progress of each processing step, or is null when none is available.
{
"videoId": "VIDEO_ID",
"status": "completed",
"includeInQA": true,
"processing": { "status": "completed", "progress": 100, "tasks": [ ... ] }
}DELETE/api/v1/videos/:videoIdDelete a video
Delete a video
Removes the video and cancels any processing still queued.
POST/api/v1/mediaAdd an image or text for moderation
Add an image or text for moderation
Upload a file (multipart field file), send inline text (up to 200 KB), or send sourceType: "url" with a url. For a URL without a file extension, add mediaType (image or text). Videos go to POST /videos instead.
role: content (default, moderated), or knowledge / reference (described and used as context, never moderated).
// Request
{ "text": "Message body to check", "name": "ticket-4812.txt" }
// Response (201 Created)
{
"mediaId": "MEDIA_ID",
"projectId": "PROJECT_ID",
"mediaType": "text",
"status": "uploaded",
"analysisQueued": true
}GET/api/v1/media/:mediaId/statusGet a media item's processing status
Get a media item's processing status
status moves from uploaded to processing, then analyzed (no moderation needed), completed or error. decision is set once moderated is true.
{
"mediaId": "MEDIA_ID",
"mediaType": "image",
"role": "content",
"status": "completed",
"analyzed": true,
"moderated": true,
"decision": "flag",
"error": null
}GET/api/v1/media/:mediaId/moderationGet a media item's moderation result
Get a media item's moderation result
decision is allow, flag or ban. A human review, when recorded, takes priority and appears in human_review. status stays pending until moderation has run.
{
"mediaId": "MEDIA_ID",
"mediaType": "image",
"status": "completed",
"decision": "flag",
"confidence": 82,
"reasoning": "A weapon is visible in the foreground.",
"flags": ["weapons"],
"categories_scanned": ["weapons", "violence", "nudity"],
"human_review": null,
"completedAt": "2026-10-02T09:00:12.000Z"
}Projects
Projects are the top-level container for videos, documents, alerts, and agent sessions.
GET/api/v1/projectsList user's projects
List user's projects
Send the request to https://platform.endlessai.com/api/v1/projects using your project key or user token.
POST/api/v1/projectsCreate project
Create project
Request body:
{ "name": "My Project", "description": "optional" }GET/api/v1/projects/:projectIdGet project details
Get project details
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId using your project key or user token.
PUT/api/v1/projects/:projectIdBetaUpdate project
Update project
Beta: this operation may change before it is final.
Request body (partial update):
{ "name": "New Name", "description": "Updated desc" }DELETE/api/v1/projects/:projectIdDelete project
Delete project
Returns 204 No Content on success.
GET/api/v1/projects/:projectId/configGet project configuration
Get project configuration
Returns the project configuration, including analysis and detection settings.
POST/api/v1/projects/from-template/:templateIdBetaCreate project from template
Create project from template
Beta: this operation may change before it is final.
Send the request to https://platform.endlessai.com/api/v1/projects/from-template/:templateId using your project key or user token.
PUT/api/v1/projects/:projectId/stageBetaUpdate project stage
Update project stage
Beta: this operation may change before it is final.
Body: { "stage": "production" } — stages: created, calibrating, production
GET/api/v1/projects/:projectId/checklistBetaGet setup checklist
Get setup checklist
Beta: this operation may change before it is final.
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/checklist using your project key or user token.
Videos
Upload, process, and manage video files within a project.
GET/api/v1/projects/:projectId/videosList videos
List videos
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/videos using your project key or user token.
POST/api/v1/projects/:projectId/videos/uploadUpload video file
Upload video file
Multipart form-data. Field name: video. Max size: 1GB.
curl -X POST https://platform.endlessai.com/api/v1/projects/PROJECT_ID/videos/upload \
-H "X-Api-Key: YOUR_KEY" \
-F "video=@/path/to/video.mp4"import requests
with open("video.mp4", "rb") as f:
resp = requests.post(
f"{BASE}/projects/{PROJECT_ID}/videos/upload",
headers={"X-Api-Key": API_KEY},
files={"video": ("video.mp4", f, "video/mp4")},
)
print(resp.json())import { openAsBlob } from "node:fs";
const form = new FormData();
form.append("video", await openAsBlob("video.mp4", { type: "video/mp4" }));
const res = await fetch(
`${BASE}/projects/${projectId}/videos/upload`,
{ method: "POST", headers: { "X-Api-Key": API_KEY }, body: form }
);
console.log(await res.json());POST/api/v1/projects/:projectId/videos/upload-from-urlImport video from URL
Import video from URL
Downloads the video in the background. Returns 202 Accepted.
{ "url": "https://example.com/video.mp4", "name": "Remote Video" }GET/api/v1/projects/:projectId/videos/:videoIdGet video details
Get video details
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/videos/:videoId using your project key or user token.
PUT/api/v1/projects/:projectId/videos/:videoIdUpdate video metadata
Update video metadata
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/videos/:videoId using your project key or user token.
DELETE/api/v1/projects/:projectId/videos/:videoIdDelete video
Delete video
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/videos/:videoId using your project key or user token.
GET/api/v1/projects/:projectId/videos/:videoId/statusGet processing status
Get processing status
Poll this endpoint until status is completed.
{
"videoId": "VIDEO_ID",
"status": "completed",
"includeInQA": true
}POST/api/v1/projects/:projectId/videos/:videoId/processBetaProcess a video
Process a video
Beta: this operation may change before it is final.
Queues analysis using the project's processing settings. Poll the video status for completion.
GET/api/v1/projects/:projectId/videos/:videoId/progressPoll processing progress
Poll processing progress
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/videos/:videoId/progress using your project key or user token.
GET/api/v1/projects/:projectId/videos/:videoId/summaryGet video summary
Get video summary
Returns the video's summary. Use .../smart-scan/summary for its Overview scan summary.
GET/api/v1/projects/:projectId/videos/:videoId/thumbnailGet thumbnail URL
Get thumbnail URL
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/videos/:videoId/thumbnail using your project key or user token.
POST/api/v1/projects/:projectId/videos/:videoId/reprocessReprocess with specific tasks
Reprocess with specific tasks
Call processing-capabilities first to find the tasks available for this video.
{ "tasks": ["generate_summary"] }POST/api/v1/projects/:projectId/videosRegister a video by name or URL
Register a video by name or URL
Creates the video record without starting processing. To import and process a video from a URL, use upload-from-url. Send name or url. Returns 201. Needs write access.
{ "name": "Gate camera 2026-10-01", "url": "https://example.com/gate.mp4" }GET/api/v1/projects/:projectId/videos/:videoId/transcode/estimateCheck whether a video needs converting for playback
Check whether a video needs converting for playback
needed is true when converting would let the video play in every supported browser. Conversion does not use tokens.
{
"video_id": "VIDEO_ID",
"needed": true,
"verdict": "needs_transcode",
"in_progress": false,
"billed": false,
"estimate": { "duration_sec": 1740, "minutes": 29, "resolution_factor": 1 }
}POST/api/v1/projects/:projectId/videos/:videoId/transcodeConvert a video for playback
Convert a video for playback
Queues the conversion and returns 202; follow progress through the video status. 409 when there is nothing to convert or a conversion is already running. Needs write access.
// Response (202 Accepted)
{ "queued": true, "billed": false, "estimate": { "duration_sec": 1740, "minutes": 29, "resolution_factor": 1 } }Sources & Events
Register where your media comes from (a camera, a lane, a system), send in external events such as transactions or sensor triggers, link them to videos, and browse videos by source, time and your own metadata.
GET/api/v1/projects/:projectId/sourcesList sources
List sources
Sources come back in creation order, each with video_count and event_count.
POST/api/v1/projects/:projectId/sourcesRegister a source
Register a source
Safe to call on every start-up: an existing source with the same external_key is returned unchanged (200, source_created: false); a new one answers 201. kind is one of api (default), upload, url_import, rtsp, connector, edge_bridge, external_pipeline or event_feed. Set config.timezone so day and hour groups follow the source's local time. Requires write access.
{
"external_key": "plaza-6-lane-4",
"kind": "event_feed",
"label": "Plaza 6, lane 4",
"config": { "timezone": "America/New_York" }
}// Response (201)
{
"id": "src_SOURCE_ID",
"external_key": "plaza-6-lane-4",
"kind": "event_feed",
"label": "Plaza 6, lane 4",
"config": { "timezone": "America/New_York" },
"status": "active",
"created_at": "2026-10-01T09:00:00Z",
"source_created": true
}PATCH/api/v1/projects/:projectId/sources/:sourceIdUpdate a source
Update a source
Updates kind, label, config or status; other fields are ignored. config is replaced as a whole. Requires write access.
POST/api/v1/projects/:projectId/eventsSend events
Send events
Creates or updates up to 1,000 events for one registered source, named by source_id or source_key. Each event needs external_id and occurred_at; re-sending the same external_id updates it instead of duplicating it. Requires write access.
curl -X POST https://platform.endlessai.com/api/v1/projects/PROJECT_ID/events \
-H "X-Api-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"source_key": "plaza-6-lane-4",
"events": [
{
"external_id": "txn-88213",
"kind": "toll_transaction",
"occurred_at": "2026-10-01T14:03:12Z",
"occurred_until": "2026-10-01T14:03:20Z",
"meta": { "vehicle_class": "2" }
}
]
}'import requests
resp = requests.post(
f"{BASE}/projects/{PROJECT_ID}/events",
headers={"X-Api-Key": API_KEY},
json={
"source_key": "plaza-6-lane-4",
"events": [
{
"external_id": "txn-88213",
"kind": "toll_transaction",
"occurred_at": "2026-10-01T14:03:12Z",
"occurred_until": "2026-10-01T14:03:20Z",
"meta": {"vehicle_class": "2"},
}
],
},
)
print(resp.json()) # {"events": [...], "created": 1, "updated": 0}const res = await fetch(`${BASE}/projects/${projectId}/events`, {
method: "POST",
headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
source_key: "plaza-6-lane-4",
events: [
{
external_id: "txn-88213",
kind: "toll_transaction",
occurred_at: "2026-10-01T14:03:12Z",
occurred_until: "2026-10-01T14:03:20Z",
meta: { vehicle_class: "2" },
},
],
}),
});
console.log(await res.json()); // { events: [...], created: 1, updated: 0 }GET/api/v1/projects/:projectId/eventsList events
List events
Oldest first. Filter with source_id and status; page forward by passing the created_at of the last event as since. limit defaults to 100 (max 500).
POST/api/v1/projects/:projectId/events/:eventId/mediaLink a video or media item to an event
Link a video or media item to an event
Give exactly one of video_id or media_id, plus an optional role (default primary). Returns 204. Requires write access.
{ "video_id": "VIDEO_ID", "role": "primary" }GET/api/v1/projects/:projectId/videos/:videoId/eventsList a video's events
List a video's events
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/videos/:videoId/events using your project key or user token.
GET/api/v1/projects/:projectId/media/:mediaId/eventsList a media item's events
List a media item's events
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/media/:mediaId/events using your project key or user token.
GET/api/v1/projects/:projectId/display-schemaGet the display schema
Get the display schema
The fields a browsing view shows for each video, and which meta.* fields can be used as filters. Returns { "fields": [] } until one is set.
PUT/api/v1/projects/:projectId/display-schemaSet the display schema
Set the display schema
Replaces the schema (at most 32 fields). type is enum, id, timestamp, number or string; set facet: true on a meta.* field to allow filtering by it. Requires write access.
{
"fields": [
{ "key": "meta.vehicle_class", "label": "Vehicle class", "type": "enum", "facet": true },
{ "key": "external_id", "label": "Transaction", "type": "id" }
],
"group_by_default": ["source", "day"]
}GET/api/v1/projects/:projectId/videos/browseBrowse videos
Browse videos
Newest capture first. Filter with source_id, from, to, tag and repeatable facet=meta.KEY:value (declared facets only). Page with limit (default 50, max 200) and offset; the response carries total and has_more.
GET/api/v1/projects/:projectId/videos/groupsCount videos by day or hour
Count videos by day or hour
Counts per source per bucket (day or hour), with ready and errors. Takes the same filters as browse.
// Response
{
"groups": [
{
"source_id": "src_SOURCE_ID",
"source_label": "Plaza 6, lane 4",
"source_kind": "event_feed",
"bucket_start": "2026-10-01T00:00:00",
"count": 214,
"ready": 210,
"errors": 1
}
],
"display_schema": { "fields": [] }
}Folder Imports
Connect a cloud folder (Google Drive, Dropbox, Box or OneDrive) and new videos in it are imported into the project automatically and processed like uploads. Stored credentials are never returned.
GET/api/v1/projects/:projectId/ingest/connectorsList folder connections
List folder connections
Also returns oauth_providers: the providers you can connect with on this deployment.
{
"data": [
{
"id": "CONNECTOR_ID",
"name": "Store cameras",
"remote_type": "drive",
"remote_path": "Exports/Store 12",
"interval_minutes": 10,
"enabled": true,
"last_sync_at": "2026-10-01T14:10:00Z",
"last_status": "ok",
"last_error": null,
"files_ingested": 37,
"config": { "configured": true, "keys": ["scope", "client_id", "client_secret", "token"] }
}
],
"oauth_providers": ["drive", "dropbox", "box", "onedrive"]
}POST/api/v1/projects/:projectId/ingest/connectors/oauth/startConnect a cloud folder
Connect a cloud folder
Returns an auth_url; open it in a browser so the folder owner can approve access. Once they approve, the connection is created with the settings you sent. provider is one of the oauth_providers from the list call. Requires write access.
curl -X POST https://platform.endlessai.com/api/v1/projects/PROJECT_ID/ingest/connectors/oauth/start \
-H "X-Api-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider": "drive",
"name": "Store cameras",
"remote_path": "Exports/Store 12",
"interval_minutes": 10
}'import requests
resp = requests.post(
f"{BASE}/projects/{PROJECT_ID}/ingest/connectors/oauth/start",
headers={"X-Api-Key": API_KEY},
json={
"provider": "drive",
"name": "Store cameras",
"remote_path": "Exports/Store 12",
"interval_minutes": 10,
},
)
print(resp.json()["auth_url"]) # open this in a browserconst res = await fetch(
`${BASE}/projects/${projectId}/ingest/connectors/oauth/start`,
{
method: "POST",
headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
provider: "drive",
name: "Store cameras",
remote_path: "Exports/Store 12",
interval_minutes: 10,
}),
}
);
const { auth_url } = await res.json();
window.open(auth_url, "_blank");POST/api/v1/projects/:projectId/ingest/connectors/:connectorId/testTest a folder connection
Test a folder connection
Checks the folder can be reached and shows what a sync would import. Nothing is imported. An unreachable folder returns ok: false with the error.
// Response
{
"ok": true,
"video_files_found": 12,
"sample": ["cam1/2026-10-01_0900.mp4", "cam1/2026-10-01_1000.mp4"]
}POST/api/v1/projects/:projectId/ingest/connectors/:connectorId/syncSync now
Sync now
Imports up to 3 new files now instead of waiting for the next interval; the response gives ingested and remaining. A file is never imported twice, even if its video was deleted. A connection also has a daily import allowance; files past it wait for the next day and are counted in waiting. Requires write access.
DELETE/api/v1/projects/:projectId/ingest/connectors/:connectorIdRemove a folder connection
Remove a folder connection
Stops syncing the folder. Videos already imported stay in the project. Requires write access.
Overview Scan
Overview scan creates a searchable summary of a video's content. Use the endpoint below to start a scan or update an existing summary.
POST/api/v1/projects/:projectId/videos/:videoId/smart-scanTrigger (or re-run) a smart scan
Trigger (or re-run) a smart scan
Body is optional. Pass dry_run: true to plan without persisting or billing, or aconfig_override object (quality, strategy, …). Returns 202 when queued.
{ "config_override": { "quality": "balanced" }, "dry_run": false }GET/api/v1/projects/:projectId/videos/:videoId/smart-scanGet scan status + last run
Get scan status + last run
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/videos/:videoId/smart-scan using your project key or user token.
GET/api/v1/projects/:projectId/videos/:videoId/smart-scan/summaryGet the Overview scan summary
Get the Overview scan summary
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/videos/:videoId/smart-scan/summary using your project key or user token.
GET/api/v1/projects/:projectId/videos/:videoId/scenesList scenes for a video
List scenes for a video
Query: min_time, max_time, source, min_importance, limit.
GET/api/v1/projects/:projectId/scenes/:sceneIdGet a single scene
Get a single scene
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/scenes/:sceneId using your project key or user token.
GET/api/v1/projects/:projectId/scenes/searchSearch video scenes by text
Search video scenes by text
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/scenes/search using your project key or user token.
GET/api/v1/projects/:projectId/frames/searchSemantic search over indexed frames
Semantic search over indexed frames
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/frames/search using your project key or user token.
GET/api/v1/projects/:projectId/videos/:videoId/smart-scan/summariesList a video's scan summaries, newest first
List a video's scan summaries, newest first
Every summary written for the video, including the rolling summaries of a live scan. Query: limit.
{
"video_id": "VIDEO_ID",
"summaries": [
{ "id": "SUMMARY_ID", "created_at": "2026-10-01T09:30:00Z", "content": "Two trucks unload at bay 2..." }
]
}GET/api/v1/projects/:projectId/videos/:videoId/utterancesGet the video's timed transcript
Get the video's timed transcript
Speech in the video, in order, with start and end times in seconds. Speakers are labelled anonymously (SPEAKER_00, SPEAKER_01, …).
{
"video_id": "VIDEO_ID",
"count": 2,
"utterances": [
{ "speaker_id": "SPEAKER_00", "t_start": 3.2, "t_end": 5.9, "text": "Bay two is clear." },
{ "speaker_id": "SPEAKER_01", "t_start": 6.4, "t_end": 8.1, "text": "Backing in now." }
]
}POST/api/v1/projects/:projectId/streams/:streamId/live-scanStart a live scan on a stream
Start a live scan on a stream
Scans a live stream continuously, writing rolling summaries and raising alerts for what you watch for. Body is optional: settings saved with live-scan/config are used. Returns 202; 409 if a scan is already running. Needs write access.
{ "watch": { "watch_for": ["person climbing the fence"] }, "summaries": true }GET/api/v1/projects/:projectId/streams/:streamId/live-scanGet live scan status
Get live scan status
state is idle when no scan is running.
{
"stream_id": "STREAM_ID",
"state": "running",
"startedAt": "2026-10-01T09:00:00.000Z",
"timeoutSec": 3600,
"lastSampleAt": "2026-10-01T09:42:10.000Z",
"stopReason": null
}DELETE/api/v1/projects/:projectId/streams/:streamId/live-scanStop a live scan
Stop a live scan
Stops the scan and turns it off for the stream, so it does not restart on its own. Needs write access.
GET/api/v1/projects/:projectId/streams/:streamId/live-scan/configGet a stream's live scan settings
Get a stream's live scan settings
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/streams/:streamId/live-scan/config using your project key or user token.
PUT/api/v1/projects/:projectId/streams/:streamId/live-scan/configUpdate a stream's live scan settings
Update a stream's live scan settings
Send only the fields to change. fps: above 0, up to 10. timeout_sec: 60–86400 seconds, or 0 to run until stopped — it keeps using tokens until you stop it. Needs write access.
{
"enabled": true,
"watch_for": ["person climbing the fence", "vehicle in the fire lane"],
"fps": 1,
"timeout_sec": 3600
}Media & Content
Upload and manage non-video content (images, audio, documents), and list all content — videos and media together — in one call.
POST/api/v1/projects/:projectId/media/uploadUpload media (multipart: images / audio / docs)
Upload media (multipart: images / audio / docs)
Multipart form field file. Detected type drives downstream handling.
POST/api/v1/projects/:projectId/media/upload-from-urlUpload media from a URL
Upload media from a URL
{ "url": "https://example.com/image.jpg", "name": "Front shelf" }GET/api/v1/projects/:projectId/contentUnified list of all content (videos + media)
Unified list of all content (videos + media)
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/content using your project key or user token.
GET/api/v1/projects/:projectId/mediaList media items
List media items
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/media using your project key or user token.
GET/api/v1/projects/:projectId/media/:mediaIdGet a media item
Get a media item
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/media/:mediaId using your project key or user token.
PUT/api/v1/projects/:projectId/media/:mediaIdBetaUpdate media metadata
Update media metadata
Beta: this operation may change before it is final.
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/media/:mediaId using your project key or user token.
DELETE/api/v1/projects/:projectId/media/:mediaIdDelete media
Delete media
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/media/:mediaId using your project key or user token.
POST/api/v1/projects/:projectId/media/:mediaId/processAnalyze and moderate a media item
Analyze and moderate a media item
Queues an image or text item for analysis and moderation. Returns 202. Needs write access.
{ "mediaId": "MEDIA_ID", "queued": true }GET/api/v1/projects/:projectId/media/:mediaId/statusGet processing status
Get processing status
status moves from uploaded to processing, then analyzed, completed or error.
{
"mediaId": "MEDIA_ID",
"mediaType": "image",
"role": "content",
"status": "completed",
"analyzed": true,
"moderated": true,
"decision": "allow",
"error": null
}GET/api/v1/projects/:projectId/media/:mediaId/moderationGet moderation result
Get moderation result
decision is allow, flag or ban; a recorded human review takes priority and is shown in human_review.
{
"mediaId": "MEDIA_ID",
"mediaType": "image",
"status": "completed",
"decision": "flag",
"confidence": 82,
"reasoning": "A weapon is visible in the foreground.",
"flags": ["weapons"],
"categories_scanned": ["weapons", "violence", "nudity"],
"human_review": null,
"completedAt": "2026-10-02T09:00:12.000Z"
}PUT/api/v1/projects/:projectId/media/:mediaId/reviewSubmit review decision
Submit review decision
decision: allowed, flagged or banned. Overrides the automatic result. Needs review access.
{ "decision": "allowed", "reason": "Toy replica, not a real weapon" }Documents
Upload reference documents (PDFs, policies, manuals) that provide context for the AI agent. Documents have three states:
GET/api/v1/projects/:projectId/documentsList documents
List documents
Query params: category, status, search
POST/api/v1/projects/:projectId/documentsUpload document
Upload document
Multipart form-data. Field: file. Max 50MB. Supported: PDF, DOCX, TXT, MD, CSV, JSON, PNG, JPEG.
Additional form fields: name, category, description, status, tags (JSON array).
Default status: available. Set to included to auto-compile into agent prompt.
curl -X POST https://platform.endlessai.com/api/v1/projects/PROJECT_ID/documents \
-H "X-Api-Key: YOUR_KEY" \
-F "file=@policy.pdf" \
-F "name=Content Moderation Policy" \
-F "category=moderation_policy" \
-F "status=included"import requests
with open("policy.pdf", "rb") as f:
resp = requests.post(
f"{BASE}/projects/{PROJECT_ID}/documents",
headers={"X-Api-Key": API_KEY},
files={"file": ("policy.pdf", f, "application/pdf")},
data={
"name": "Content Moderation Policy",
"category": "moderation_policy",
"status": "included"
},
)
print(resp.json())const form = new FormData();
form.append("file", fileBlob, "policy.pdf");
form.append("name", "Content Moderation Policy");
form.append("category", "moderation_policy");
form.append("status", "included");
const res = await fetch(
`${BASE}/projects/${projectId}/documents`,
{ method: "POST", headers: { "X-Api-Key": API_KEY }, body: form }
);GET/api/v1/projects/:projectId/documents/:docIdGet document metadata
Get document metadata
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/documents/:docId using your project key or user token.
PUT/api/v1/projects/:projectId/documents/:docIdBetaUpdate document metadata / change status
Update document metadata / change status
Beta: this operation may change before it is final.
Update name, description, category, status, tags.
Status values:
available — the agent looks it up when a question needs it
included — the agent always takes it into account
archived — the agent does not use it
Changing the status to or from included updates the agent right away.
# Make document available (searchable by agent tool)
curl -X PUT https://platform.endlessai.com/api/v1/projects/PROJECT_ID/documents/DOC_ID \
-H "X-Api-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "available"}'
# Include in agent prompt (auto-compiles)
curl -X PUT https://platform.endlessai.com/api/v1/projects/PROJECT_ID/documents/DOC_ID \
-H "X-Api-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "included"}'
# Archive (hide from agent)
curl -X PUT https://platform.endlessai.com/api/v1/projects/PROJECT_ID/documents/DOC_ID \
-H "X-Api-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "archived"}'import requests
# Change document status
resp = requests.put(
f"{BASE}/projects/{PROJECT_ID}/documents/{DOC_ID}",
headers={"X-Api-Key": API_KEY, "Content-Type": "application/json"},
json={"status": "included"}, # or "available" or "archived"
)
print(resp.json())const res = await fetch(
`${BASE}/projects/${projectId}/documents/${docId}`,
{
method: "PUT",
headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ status: "included" })
}
);DELETE/api/v1/projects/:projectId/documents/:docIdDelete document and file
Delete document and file
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/documents/:docId using your project key or user token.
GET/api/v1/projects/:projectId/documents/:docId/contentDownload/serve file
Download/serve file
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/documents/:docId/content using your project key or user token.
POST/api/v1/projects/:projectId/documents/compileTrigger context compilation
Trigger context compilation
Refresh what the agent knows from all includeddocuments. Returns the updated context.
GET/api/v1/projects/:projectId/documents/compiledGet current compiled context
Get current compiled context
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/documents/compiled using your project key or user token.
GET/api/v1/projects/:projectId/documents/:docId/textGet a document's text
Get a document's text
Returns the full text by default. Query: section_id for one section of a compiled policy, or clause (a verbatim quote) for the passage around it — clause_offset and clause_length mark the match.
{
"data": {
"doc_id": "DOC_ID",
"name": "Community guidelines.pdf",
"mode": "clause_window",
"text": "...Threats of violence against a person or group are not allowed...",
"clause_found": true,
"clause_offset": 3,
"clause_length": 64,
"truncated_window": true
}
}Project Files
Plain text files for notes, plans, and agent outputs within a project.
GET/api/v1/projects/:projectId/filesList text files
List text files
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/files using your project key or user token.
GET/api/v1/projects/:projectId/files/:filenameRead file content
Read file content
Returns { "name": "notes.md", "content": "..." }
PUT/api/v1/projects/:projectId/files/:filenameCreate/update file
Create/update file
{ "content": "# My Notes\nSome content here..." }DELETE/api/v1/projects/:projectId/files/:filenameDelete file
Delete file
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/files/:filename using your project key or user token.
Arrays (Video Collections)
Group multiple videos into reusable collections for batch analysis.
GET/api/v1/projects/:projectId/arraysList arrays
List arrays
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/arrays using your project key or user token.
POST/api/v1/projects/:projectId/arraysCreate array
Create array
{
"name": "Weekend Morning",
"description": "Front camera clips for Saturday/Sunday 8-11am",
"videoIds": ["VIDEO_ID_1", "VIDEO_ID_2"]
}GET/api/v1/projects/:projectId/arrays/:arrayIdGet array details
Get array details
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/arrays/:arrayId using your project key or user token.
PATCH/api/v1/projects/:projectId/arrays/:arrayIdUpdate array
Update array
{ "name": "Updated Name", "videoIds": ["V1", "V2", "V3"] }DELETE/api/v1/projects/:projectId/arrays/:arrayIdDelete array
Delete array
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/arrays/:arrayId using your project key or user token.
POST/api/v1/projects/:projectId/arrays/:arrayId/duplicateClone array
Clone array
{ "name": "Weekend Afternoon (copy)" }GET/api/v1/projects/:projectId/arrays/:arrayId/resolveList the videos an array currently matches
List the videos an array currently matches
Stored videos that no longer exist are listed in missing. A filter-based array whose filter can't be applied matches nothing and returns failed: true with a reason.
{
"id": "ARRAY_ID",
"name": "Weekend Morning",
"kind": "static",
"videoIds": ["VIDEO_ID_1", "VIDEO_ID_2"],
"videos": [
{ "id": "VIDEO_ID_1", "name": "Front camera 08:00" },
{ "id": "VIDEO_ID_2", "name": "Front camera 09:00" }
],
"missing": [],
"failed": false,
"reason": null
}Agent / Chat
Send messages to the AI agent for analysis. The agent has access to project videos and documents, and can examine frames, compare visuals, and search project documents to answer your question.
POST/api/v1/projects/:projectId/agent/processSend message for AI processing
Send message for AI processing
Request body:
{
"message": "What companies signed the NDA?",
"streaming": true,
"useUnifiedSSE": true,
"sessionId": "unique-session-id",
"videoId": ["VIDEO_ID_1"]
}message (required) — the user query
streaming — enable streaming response via SSE
useUnifiedSSE — use unified SSE channel (recommended)
sessionId — session ID for multi-turn memory
videoId — specific video(s). If omitted, all project videos are used.
arrayId — use a video array instead of individual IDs
curl -X POST https://platform.endlessai.com/api/v1/projects/PROJECT_ID/agent/process \
-H "X-Api-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "Describe what happens in the first 10 seconds",
"sessionId": "session-001"
}'import requests
resp = requests.post(
f"{BASE}/projects/{PROJECT_ID}/agent/process",
headers={"X-Api-Key": API_KEY, "Content-Type": "application/json"},
json={
"message": "Describe what happens in the first 10 seconds",
"sessionId": "session-001"
},
timeout=300
)
print(resp.json())const res = await fetch(
`${BASE}/projects/${projectId}/agent/process`,
{
method: "POST",
headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
message: "Describe what happens in the first 10 seconds",
sessionId: "session-001"
})
}
);
const result = await res.json();Response shape (non-streaming):
{
"success": true,
"sessionId": "session-001",
"summary": {
"iterations": 1,
"processingTimeMs": 4200
},
"timestamp": "2026-02-23T10:30:00.000Z"
}summary gives you iteration count, token totals, and processing time for the run.
POST/api/v1/projects/:projectId/agent/askAsk a question and poll for the answer
Ask a question and poll for the answer
Starts a run and returns 202 with a run_id to poll. Pass video_id to ask about one video; otherwise the project's videos are used. One run at a time per credential — a second ask while one is running returns 409 with the active run_id. Returns 402 with code: "budget_stopped" when the project's token budget is paused.
curl -X POST https://platform.endlessai.com/api/v1/projects/PROJECT_ID/agent/ask \
-H "X-Api-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"question": "When did the first truck arrive?", "video_id": "VIDEO_ID"}'
# Then poll the run
curl https://platform.endlessai.com/api/v1/projects/PROJECT_ID/agent/runs/RUN_ID \
-H "X-Api-Key: YOUR_KEY"import time, requests
headers = {"X-Api-Key": API_KEY}
run = requests.post(
f"{BASE}/projects/{PROJECT_ID}/agent/ask",
headers=headers,
json={"question": "When did the first truck arrive?", "video_id": VIDEO_ID},
).json()
while True:
result = requests.get(
f"{BASE}/projects/{PROJECT_ID}/agent/runs/{run['run_id']}",
headers=headers,
).json()
if result["status"] != "running":
break
time.sleep(5)
print(result.get("answer") or result.get("error"))const headers = { "X-Api-Key": API_KEY, "Content-Type": "application/json" };
const { run_id } = await (await fetch(
`${BASE}/projects/${projectId}/agent/ask`,
{
method: "POST",
headers,
body: JSON.stringify({ question: "When did the first truck arrive?", video_id: videoId }),
}
)).json();
let result;
do {
await new Promise((r) => setTimeout(r, 5000));
result = await (await fetch(
`${BASE}/projects/${projectId}/agent/runs/${run_id}`,
{ headers }
)).json();
} while (result.status === "running");
console.log(result.answer ?? result.error);// Response (202 Accepted)
{
"run_id": "RUN_ID",
"status": "running",
"poll": "/api/v1/projects/PROJECT_ID/agent/runs/RUN_ID"
}GET/api/v1/projects/:projectId/agent/runs/:runIdGet an ask run
Get an ask run
status is running, complete, error or timeout. A run is kept for one hour. A completed run may later gain a correction field; the original answer is never changed.
{
"run_id": "RUN_ID",
"project_id": "PROJECT_ID",
"status": "complete",
"question": "When did the first truck arrive?",
"answer": "The first truck backs into bay 2 at 00:04:12.",
"error": null,
"started_at": "2026-10-01T09:30:00.000Z",
"completed_at": "2026-10-01T09:30:41.000Z"
}POST/api/v1/qna/agentAsk a question about a video, collection or video list
Ask a question about a video, collection or video list
Project key only; answers within the request. Send question and exactly one of videoId, arrayId or videoIds. Reuse the returned sessionId to ask follow-up questions with the same memory.
// Request
{
"question": "Did any forklift enter the pedestrian lane?",
"arrayId": "ARRAY_ID",
"sessionId": "session-001"
}
// Response
{
"success": true,
"sessionId": "session-001",
"content": "Yes — twice: at 00:12 and at 03:47.",
"summary": { "iterations": 4, "processingTimeMs": 18342 },
"timestamp": "2026-10-01T09:30:00.000Z"
}Direct Chat (Simple Q&A)
Ask simple factual questions about video content. Get concise answers grounded in your video. Use this for simple questions; use the Agent endpoint for complex analysis requiring tools.
POST/api/v1/projects/:projectId/chatBetaSend chat message
Send chat message
Beta: this operation may change before it is final.
Request body:
{
"message": "What is happening in this video?",
"videoId": "VIDEO_ID",
"stream": true
}message — single message (or use messages array for multi-turn)
messages — full conversation: [{ "role": "user", "content": "..." }]
videoId — target video. If omitted, uses first completed video in project.
stream — SSE streaming (default: true). Set false for JSON response.
# Streaming response
curl -N -X POST https://platform.endlessai.com/api/v1/projects/PROJECT_ID/chat \
-H "X-Api-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"message": "What happens in this video?"}'
# Non-streaming
curl -X POST https://platform.endlessai.com/api/v1/projects/PROJECT_ID/chat \
-H "X-Api-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"message": "What happens?", "stream": false}'import requests
# Non-streaming
resp = requests.post(
f"{BASE}/projects/{PROJECT_ID}/chat",
headers={"X-Api-Key": API_KEY, "Content-Type": "application/json"},
json={"message": "What happens in this video?", "stream": False},
timeout=60,
)
data = resp.json()
print(data["choices"][0]["message"]["content"])
# Multi-turn conversation
messages = [
{"role": "user", "content": "What is the person doing?"},
{"role": "assistant", "content": "The person is cooking."},
{"role": "user", "content": "What are they cooking?"},
]
resp = requests.post(
f"{BASE}/projects/{PROJECT_ID}/chat",
headers={"X-Api-Key": API_KEY, "Content-Type": "application/json"},
json={"messages": messages, "stream": False},
)
print(resp.json()["choices"][0]["message"]["content"])// Non-streaming
const res = await fetch(`${BASE}/projects/${projectId}/chat`, {
method: "POST",
headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ message: "What happens?", stream: false })
});
const data = await res.json();
console.log(data.choices[0].message.content);Non-streaming response:
{
"choices": [{
"message": {
"content": "In this video, a person is preparing food in a kitchen..."
}
}]
}Streaming response (SSE):
data: {"choices":[{"message":{"content":"In this video..."}}]}
data: [DONE]Chat Sessions
Persistent chat sessions for multi-turn conversations with the AI agent. Sessions preserve message history across page loads.
GET/api/v1/projects/:projectId/chat-sessionsList chat sessions
List chat sessions
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/chat-sessions using your project key or user token.
POST/api/v1/projects/:projectId/chat-sessionsCreate chat session
Create chat session
{ "title": "Analysis discussion" }GET/api/v1/projects/:projectId/chat-sessions/:sessionIdGet session with messages
Get session with messages
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/chat-sessions/:sessionId using your project key or user token.
PUT/api/v1/projects/:projectId/chat-sessions/:sessionIdUpdate session
Update session
{ "title": "Updated title" }DELETE/api/v1/projects/:projectId/chat-sessions/:sessionIdDelete session
Delete session
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/chat-sessions/:sessionId using your project key or user token.
Content Moderation
Check uploaded content against your moderation policy. Returns structured decisions: allow, flag, or ban.
POST/api/v1/projects/:projectId/moderateBetaUpload & moderate content
Upload & moderate content
Beta: this operation may change before it is final.
Accepts multipart file upload or a JSON body with url. Content is queued for a content scan immediately.
File uploads: Content-Type: multipart/form-data, field name file, max 500 MB.
URL submissions: { "url": "https://...", "name": "optional-name" }
// Response (202 Accepted)
{
"contentId": "uuid-of-video-record",
"status": "processing",
"message": "Content queued for moderation analysis"
}# Upload file for moderation
curl -X POST "https://platform.endlessai.com/api/v1/projects/PROJECT_ID/moderate" \
-H "X-Api-Key: YOUR_KEY" \
-F "file=@video.mp4"
# Submit URL for moderation
curl -X POST "https://platform.endlessai.com/api/v1/projects/PROJECT_ID/moderate" \
-H "X-Api-Key: YOUR_KEY" -H "Content-Type: application/json" \
-d '{"url": "https://example.com/video.mp4"}'# File upload
with open("video.mp4", "rb") as f:
resp = requests.post(
f"{BASE}/projects/{PROJECT_ID}/moderate",
headers={"X-Api-Key": API_KEY},
files={"file": ("video.mp4", f, "video/mp4")},
)
content_id = resp.json()["contentId"]import { openAsBlob } from "node:fs";
const form = new FormData();
form.append("file", await openAsBlob("video.mp4", { type: "video/mp4" }));
const res = await fetch(`${BASE}/projects/${PROJECT_ID}/moderate`, {
method: "POST",
headers: { "X-Api-Key": API_KEY },
body: form,
});
const { contentId } = await res.json();GET/api/v1/projects/:projectId/videos/:videoId/moderationGet moderation result
Get moderation result
Returns the structured moderation result combining the content scan and agent report (if available). Agent decisions take priority over the content scan.
{
"videoId": "VIDEO_ID",
"status": "completed",
"runId": "run-uuid",
"completedAt": "2026-02-23T10:30:00Z",
"sceneCount": 12,
"decision": "flag",
"confidence": 85,
"reasoning": "Potentially violent content detected in frames 45-60",
"flags": [
{
"category": "violence",
"severity": "medium",
"confidence": 0.85,
"timestamp": 14.5,
"frame_index": 45,
"description": null
}
],
"categories_scanned": ["violence", "nudity", "hate_speech"],
"frames_reviewed": 120,
"escalation": {
"reviewed": true,
"verdict": "flag",
"confidence": 85,
"frames_reviewed": 12,
"flagged_categories": ["violence"],
"matched_rules": ["policy-rule-14"],
"cost_at": 1240
},
"agent_report": { "...if pipeline ran..." }
}escalation reflects the deeper review pass when one ran: verdict, confidence, matched policy rules, and usage. Divide cost_at by 1,000 to display platform tokens.
GET/api/v1/projects/:projectId/moderation/statsGet moderation statistics
Get moderation statistics
Aggregate counts across all content in the project.
{
"data": {
"total": 150,
"processing": 3,
"completed": 140,
"allowed": 120,
"flagged": 15,
"banned": 5,
"pending": 5,
"error": 2
}
}PUT/api/v1/projects/:projectId/videos/:videoId/reviewBetaSubmit review decision
Submit review decision
Beta: this operation may change before it is final.
{
"decision": "flag",
"reason": "Suspicious activity at 2:15"
}Policy Compiler
Compile moderation policy documents into structured rules for automated enforcement. Upload policy PDFs via Documents, then compile them into a machine-readable policy snapshot.
POST/api/v1/projects/:projectId/policy/compileCompile policy from uploaded documents
Compile policy from uploaded documents
Triggers background compilation. Returns immediately — poll /policy/snapshot for progress.
GET/api/v1/projects/:projectId/policy/snapshotGet compiled policy snapshot
Get compiled policy snapshot
Returns the compiled policy (categories, rules, action taxonomy) and compile status. Poll every 3s while compile_status is non-null.
PUT/api/v1/projects/:projectId/policy/snapshotUpdate policy snapshot (promote/edit)
Update policy snapshot (promote/edit)
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/policy/snapshot using your project key or user token.
DELETE/api/v1/projects/:projectId/policy/allBetaDelete all policies and policy documents
Delete all policies and policy documents
Beta: this operation may change before it is final.
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/policy/all using your project key or user token.
GET/api/v1/projects/:projectId/policy/open-questionsGet open questions from compilation
Get open questions from compilation
Questions the compiler couldn't resolve from the policy documents. Answering them improves accuracy on recompile.
PUT/api/v1/projects/:projectId/policy/open-questions/:questionId/resolveResolve an open question
Resolve an open question
{ "answer": "Profanity should be flagged, not banned" }POST/api/v1/projects/:projectId/policy/apply-answersApply your answers to the compiled policy
Apply your answers to the compiled policy
Updates the compiled policy with the open questions you have resolved, without a full recompile. Runs in the background — poll /policy/snapshot for the result. Needs write access.
// Response
{ "data": null, "compile_status": "applying", "message": "Applying answers to the policy" }Compiled Policies
The structured rule-sets produced by the Policy Compiler. Moderation evaluates content against the active compiled policy version.
GET/api/v1/projects/:projectId/compiled-policiesList compiled policy versions
List compiled policy versions
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/compiled-policies using your project key or user token.
GET/api/v1/projects/:projectId/compiled-policies/:policyIdGet one version (rules + open questions)
Get one version (rules + open questions)
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/compiled-policies/:policyId using your project key or user token.
PATCH/api/v1/projects/:projectId/compiled-policies/:policyIdBetaEdit a compiled policy version
Edit a compiled policy version
Beta: this operation may change before it is final.
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/compiled-policies/:policyId using your project key or user token.
PUT/api/v1/projects/:projectId/compiled-policies/activeSet the active policy version
Set the active policy version
{ "policyId": "pol_a1b2c3" }DELETE/api/v1/projects/:projectId/compiled-policies/:policyIdDelete a version
Delete a version
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/compiled-policies/:policyId using your project key or user token.
Processing Pipeline
The processing pipeline runs an AI agent on uploaded content. It can be triggered automatically after the content scan or video processing, or manually via API. The agent produces a structured report with decision, confidence, and detailed findings.
POST/api/v1/projects/:projectId/pipeline/trigger/:videoIdTrigger pipeline for video
Trigger pipeline for video
Manually triggers the agent pipeline for a specific video. Returns immediately with a queue confirmation.
// Response
{
"queued": true,
"videoId": "VIDEO_ID",
"projectId": "PROJECT_ID",
"message": "Processing queued"
}GET/api/v1/projects/:projectId/videos/:videoId/reportGet agent report
Get agent report
Returns the full agent analysis report for a video. Returns 404 if no report exists yet.
{
"data": {
"run_id": "run-uuid",
"started_at": "2026-02-23T10:30:00Z",
"completed_at": "2026-02-23T10:30:45Z",
"duration_ms": 45000,
"decision": "flag",
"confidence": 85,
"summary": "Content contains potentially violent scenes requiring review",
"findings": [
{
"type": "policy_violation",
"category": "violence",
"severity": "medium",
"timestamp": 14.5,
"description": "Physical altercation between two individuals"
}
],
"agent": {
"iterations": 5,
"tokens_used": { "input": 8500, "output": 1200 },
"cost_at": 980
}
}
}GET/api/v1/projects/:projectId/pipeline/configGet pipeline configuration
Get pipeline configuration
{
"data": {
"enabled": true,
"auto_trigger": "manual",
"prompt": "Analyze this content for policy violations...",
"timeout_ms": 600000
}
}PUT/api/v1/projects/:projectId/pipeline/configBetaUpdate pipeline configuration
Update pipeline configuration
Beta: this operation may change before it is final.
Merges provided fields into existing config. Omitted fields are preserved.
{
"enabled": true,
"auto_trigger": "manual",
"prompt": "Your custom analysis instructions..."
}auto_trigger: use "manual" to run analysis on request, or "after_processing" to run it after a video is processed.
Jobs
Standing questions the platform answers for you. A job runs on each new video when it finishes processing, on a daily schedule over a window of videos, or by hand, and records a result you can read, print or deliver to your notification channels.
GET/api/v1/projects/:projectId/jobsList jobs
List jobs
Each job carries run_count and platform_tokens_total, the tokens its runs have spent. Add ?include=archived to include archived jobs.
POST/api/v1/projects/:projectId/jobsCreate a job
Create a job
name and prompt are required. Needs a role that can change jobs. Returns 201 with the job.
trigger: auto_ops_complete (each video, once it finishes processing — the default), schedule or manual.
run_scope: per_video (default) or project — one run over a window of videos. Project jobs run on schedule or manual, and a scheduled job needs schedule: { at, timezone?, days? }. The timezone defaults to the project's site timezone.
output_contract: free (default), grade, findings or report (needs report_type_id — see Report types).
audience: recipient (default, written for someone outside the team) or team.
delivery: { channel_ids, full_report? } — the notification channels that receive each result.
scope: { sources?, include_uploads?, include_live? } — which videos the job applies to.
curl -X POST https://platform.endlessai.com/api/v1/projects/PROJECT_ID/jobs \
-H "X-Api-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Opening checklist",
"prompt": "Was the shop open by 09:00 with the shutters fully up?",
"trigger": "auto_ops_complete",
"output_contract": "grade",
"delivery": { "channel_ids": ["CHANNEL_ID"] }
}'import requests
resp = requests.post(
f"{BASE}/projects/{PROJECT_ID}/jobs",
headers={"X-Api-Key": API_KEY, "Content-Type": "application/json"},
json={
"name": "Opening checklist",
"prompt": "Was the shop open by 09:00 with the shutters fully up?",
"trigger": "auto_ops_complete",
"output_contract": "grade",
"delivery": {"channel_ids": ["CHANNEL_ID"]},
},
)
job = resp.json()["data"]
print(job["id"])const res = await fetch(
`${BASE}/projects/${projectId}/jobs`,
{
method: "POST",
headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
name: "Opening checklist",
prompt: "Was the shop open by 09:00 with the shutters fully up?",
trigger: "auto_ops_complete",
output_contract: "grade",
delivery: { channel_ids: ["CHANNEL_ID"] }
})
}
);
const { data: job } = await res.json();GET/api/v1/projects/:projectId/jobs/:jobIdGet a job
Get a job
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/jobs/:jobId using your project key or user token.
PATCH/api/v1/projects/:projectId/jobs/:jobIdUpdate a job
Update a job
Send only the fields to change. Set enabled to false to pause a job. run_scope cannot be changed after creation.
DELETE/api/v1/projects/:projectId/jobs/:jobIdDelete a job
Delete a job
A job that already has results is archived instead, and its results are kept. The response says which: { "deleted": true } or { "archived": true, "job": { ... } }.
POST/api/v1/projects/:projectId/jobs/:jobId/runRun a job now
Run a job now
Needs the right to run jobs; a run spends the project's tokens. For a per-video job send video_id; for a project job send an optional from / to window (ISO 8601). Returns 202 with the result_id to poll. Add ?wait=1 to wait up to 30 seconds for the finished result.
curl -X POST https://platform.endlessai.com/api/v1/projects/PROJECT_ID/jobs/JOB_ID/run \
-H "X-Api-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "video_id": "VIDEO_ID" }'import time
import requests
headers = {"X-Api-Key": API_KEY}
run = requests.post(
f"{BASE}/projects/{PROJECT_ID}/jobs/{JOB_ID}/run",
headers=headers,
json={"video_id": VIDEO_ID},
).json()["data"]
result_id = run["result_id"]
while True:
result = requests.get(
f"{BASE}/projects/{PROJECT_ID}/job-results/{result_id}",
headers=headers,
).json()["data"]
if result["status"] != "running":
break
time.sleep(10)
print(result["status"], result["verdict"])const res = await fetch(
`${BASE}/projects/${projectId}/jobs/${jobId}/run`,
{
method: "POST",
headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ video_id: videoId })
}
);
const { data: run } = await res.json();
console.log(run.result_id);{
"data": {
"accepted": true,
"job_id": "JOB_ID",
"video_id": "VIDEO_ID",
"result_id": "RESULT_ID",
"result": { "id": "RESULT_ID", "status": "running" }
},
"meta": {}
}GET/api/v1/projects/:projectId/job-resultsList job results
List job results
Newest first. Query params: job_id, video_id, status, limit (default 50, max 200). status is one of running, completed, not_applicable, failed, budget_stopped or skipped_scope.
GET/api/v1/projects/:projectId/job-results/:resultIdGet a job result
Get a job result
{
"data": {
"id": "RESULT_ID",
"job_id": "JOB_ID",
"job_name": "Opening checklist",
"video_id": "VIDEO_ID",
"status": "completed",
"verdict": "Pass — shutters up at 08:52",
"body": "The shop opened at 08:52. Both shutters were fully raised...",
"created_at": "2026-10-01T06:05:12.000Z"
},
"meta": {}
}GET/api/v1/projects/:projectId/job-results/:resultId/viewView a job result as a printable page
View a job result as a printable page
Returns an HTML page with the result and its evidence frames, ready to print or save as PDF.
Schedules & Compliance
Daily schedules in the site's local time — such as a digest of what happened at the site — and compliance checks: time-of-day obligations from your playbook ("shop open by 09:00") that are judged against the footage and recorded as pass, fail or inconclusive.
GET/api/v1/projects/:projectId/schedulesList schedules
List schedules
Query param: kind.
POST/api/v1/projects/:projectId/schedulesCreate a schedule
Create a schedule
spec is { "type": "daily", "at": "HH:MM", "days"?: [0-6] } (0 = Sunday). timezone defaults to the project's site timezone. A digest schedule takes payload.template: site or compliance, and is delivered to the project's notification channels. catch_up: run_once_late or skip after a missed time. Returns 201.
{
"kind": "digest",
"spec": { "type": "daily", "at": "18:30", "days": [0, 1, 2, 3, 4] },
"timezone": "Asia/Jerusalem",
"payload": { "template": "site" }
}PATCH/api/v1/projects/:projectId/schedules/:scheduleIdUpdate a schedule
Update a schedule
Change spec, timezone, payload, enabled or catch_up.
DELETE/api/v1/projects/:projectId/schedules/:scheduleIdDelete a schedule
Delete a schedule
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/schedules/:scheduleId using your project key or user token.
POST/api/v1/projects/:projectId/schedules/:scheduleId/runRun a schedule now
Run a schedule now
An extra run; the schedule's next regular time is unchanged.
POST/api/v1/projects/:projectId/compliance/compileBuild compliance checks from the playbook
Build compliance checks from the playbook
Reads the project's compiled playbook and creates one scheduled check per time-of-day obligation, replacing the previous set. Rules it cannot pin to a time become open questions on the playbook. Optional timezone; otherwise the site timezone is used. Takes seconds and returns the plan.
GET/api/v1/projects/:projectId/compliance/planGet the compliance plan
Get the compliance plan
{
"data": {
"status": "compiled",
"compiled_at": "2026-10-01T07:00:00.000Z",
"timezone": "Asia/Jerusalem",
"check_count": 3,
"check_ids": ["CHECK_ID_1", "CHECK_ID_2", "CHECK_ID_3"],
"open_question_ids": [],
"compliance_applied": "full"
},
"meta": {}
}GET/api/v1/projects/:projectId/compliance/checksList compliance checks
List compliance checks
Query param: status (active, paused, archived).
PATCH/api/v1/projects/:projectId/compliance/checks/:checkIdPause or resume a compliance check
Pause or resume a compliance check
{ "status": "paused" }GET/api/v1/projects/:projectId/compliance/verdictsList compliance verdicts
List compliance verdicts
Query params: check_id, limit.
{
"data": [
{
"id": "VERDICT_ID",
"check_id": "CHECK_ID",
"window_start": "2026-10-01T05:30:00.000Z",
"window_end": "2026-10-01T06:30:00.000Z",
"verdict": "fail",
"reason": "The shutters were still down at 09:20.",
"alert_id": "ALERT_ID",
"created_at": "2026-10-01T06:31:04.000Z"
}
],
"meta": { "total": 1, "returned": 1, "hasMore": false }
}Batch Runs & Ground Truth
Run the project's processing pipeline over many videos at once, track the batch, and record the correct answers so you can measure how accurate the results are.
POST/api/v1/projects/:projectId/pipeline/trigger-batchRun the pipeline over many videos
Run the pipeline over many videos
One run per video, processed as bulk work so it does not slow down interactive requests. Pick videos with videoIds, or with a selector (source_id, from, to, tag; up to 5,000 videos). Videos that already have a run are skipped unless force is true. Returns 202 with a batch_id. Runs are charged in tokens at the batch rate. Returns 429 with code: "bulk_ceiling" when the project already has too much bulk work waiting. Requires write access.
curl -X POST https://platform.endlessai.com/api/v1/projects/PROJECT_ID/pipeline/trigger-batch \
-H "X-Api-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"selector": { "source_id": "src_SOURCE_ID", "from": "2026-10-01T00:00:00Z", "to": "2026-10-02T00:00:00Z" },
"deadline": "2026-10-02T06:00:00Z"
}'import requests
resp = requests.post(
f"{BASE}/projects/{PROJECT_ID}/pipeline/trigger-batch",
headers={"X-Api-Key": API_KEY},
json={
"selector": {
"source_id": "src_SOURCE_ID",
"from": "2026-10-01T00:00:00Z",
"to": "2026-10-02T00:00:00Z",
},
"deadline": "2026-10-02T06:00:00Z",
},
)
batch_id = resp.json()["batch_id"]const res = await fetch(
`${BASE}/projects/${projectId}/pipeline/trigger-batch`,
{
method: "POST",
headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
selector: {
source_id: "src_SOURCE_ID",
from: "2026-10-01T00:00:00Z",
to: "2026-10-02T00:00:00Z",
},
deadline: "2026-10-02T06:00:00Z",
}),
}
);
const { batch_id } = await res.json();// Response (202)
{
"batch_id": "batch_BATCH_ID",
"enqueued": 214,
"requested": 214,
"progress_url": "/api/v1/projects/PROJECT_ID/batches/batch_BATCH_ID"
}GET/api/v1/projects/:projectId/batches/:batchIdGet batch progress
Get batch progress
overdue is true when the batch's deadline has passed and runs are still outstanding.
{
"id": "batch_BATCH_ID",
"item_count": 214,
"deadline": "2026-10-02T06:00:00Z",
"created_at": "2026-10-01T22:00:00Z",
"counts": { "queued": 40, "running": 4, "completed": 168, "failed": 2 },
"done": 170,
"total": 214,
"overdue": false
}POST/api/v1/projects/:projectId/runs/:runId/ground-truthRecord the correct label for a run
Record the correct label for a run
Use the run_id from the video's agent report. One label per run and origin (external_system by default, or human_review); posting again replaces it. Requires write access.
{ "label": "class_2", "origin": "human_review", "annotator": "reviewer@example.com" }POST/api/v1/projects/:projectId/ground-truthRecord correct labels in bulk
Record correct labels in bulk
Up to 1,000 items keyed by your own external_id (the id stored on the video when it was ingested). Each label is attached to the latest finished run for that id; each result says ok or gives an error such as no run found. Requires write access.
{
"items": [
{ "external_id": "txn-88213", "label": "class_2" },
{ "external_id": "txn-88214", "label": "class_3" }
]
}GET/api/v1/projects/:projectId/analytics/labelsGet label accuracy
Get label accuracy
Compares the labels runs produced with the correct labels you recorded. Filter with from, to and source_id. accuracy is null when nothing matches.
{
"total": 170,
"correct": 161,
"accuracy": 0.947,
"per_label": {
"class_2": { "total": 120, "correct": 116, "accuracy": 0.967 },
"class_3": { "total": 50, "correct": 45, "accuracy": 0.9 }
},
"confusions": [{ "truth": "class_3", "predicted": "class_2", "n": 5 }]
}Alerts
Monitor and manage alerts generated during video analysis.
GET/api/v1/projects/:projectId/alertsList alerts
List alerts
Query params: limit, severity (critical/warning/info), status (active/acknowledged/resolved/dismissed), timeWindow, videoId
GET/api/v1/projects/:projectId/alerts/statsGet alert statistics
Get alert statistics
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/alerts/stats using your project key or user token.
POST/api/v1/projects/:projectId/alertsCreate manual alert
Create manual alert
{
"title": "Suspicious activity detected",
"description": "Person in restricted area at 14:32",
"severity": "warning",
"type": "security",
"videoId": "VIDEO_ID"
}POST/api/v1/projects/:projectId/alerts/:alertId/acknowledgeMark as acknowledged
Mark as acknowledged
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/alerts/:alertId/acknowledge using your project key or user token.
POST/api/v1/projects/:projectId/alerts/:alertId/resolveMark as resolved
Mark as resolved
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/alerts/:alertId/resolve using your project key or user token.
POST/api/v1/projects/:projectId/alerts/:alertId/dismissDismiss alert
Dismiss alert
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/alerts/:alertId/dismiss using your project key or user token.
GET/api/v1/projects/:projectId/alerts/streamSSE stream for real-time alerts
SSE stream for real-time alerts
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/alerts/stream using your project key or user token.
Review Queue
Human-in-the-loop review for flagged content. Reviewers claim items, inspect evidence, and submit decisions.
GET/api/v1/projects/:projectId/alerts/review-queueGet review queue items
Get review queue items
Returns flagged content awaiting human review. Query params: limit, status
POST/api/v1/projects/:projectId/alerts/:alertId/claimClaim item for review
Claim item for review
Locks the item to the current reviewer. Other reviewers see it as claimed.
POST/api/v1/projects/:projectId/alerts/:alertId/releaseRelease claimed item
Release claimed item
Returns the item to the queue for another reviewer.
POST/api/v1/projects/:projectId/alerts/:alertId/reviewSubmit review decision
Submit review decision
{
"decision": "flag",
"reason": "Contains borderline content"
}GET/api/v1/projects/:projectId/review-queue/configGet review queue configuration
Get review queue configuration
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/review-queue/config using your project key or user token.
PUT/api/v1/projects/:projectId/review-queue/configUpdate review queue configuration
Update review queue configuration
{
"auto_assign": true,
"review_timeout_minutes": 30,
"require_reason": true
}Reports
Written reports with evidence frames: an incident report for one alert, or a period report over a time range or a video. Reports are generated in the background and can be printed or shared by link.
GET/api/v1/projects/:projectId/reportsList reports
List reports
Newest first. Query params: alert_id, limit (default 50, max 200).
POST/api/v1/projects/:projectId/reportsGenerate a report
Generate a report
type is incident (default, needs alert_id) or period (needs from / to or video_id). Returns 202 with the report in status queued; poll it until ready or failed. Generating a report spends the project's tokens; 402 means the project's budget is stopped.
curl -X POST https://platform.endlessai.com/api/v1/projects/PROJECT_ID/reports \
-H "X-Api-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "type": "incident", "alert_id": "ALERT_ID" }'import time
import requests
headers = {"X-Api-Key": API_KEY}
report = requests.post(
f"{BASE}/projects/{PROJECT_ID}/reports",
headers=headers,
json={"type": "incident", "alert_id": ALERT_ID},
).json()
while report["status"] in ("queued", "generating"):
time.sleep(5)
report = requests.get(
f"{BASE}/projects/{PROJECT_ID}/reports/{report['id']}",
headers=headers,
).json()
print(report["status"], report.get("title"))const res = await fetch(
`${BASE}/projects/${projectId}/reports`,
{
method: "POST",
headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ type: "incident", alert_id: alertId })
}
);
const report = await res.json(); // { id, status: "queued", ... }GET/api/v1/projects/:projectId/reports/:reportIdGet a report
Get a report
{
"id": "REPORT_ID",
"type": "incident",
"status": "ready",
"alert_id": "ALERT_ID",
"video_id": "VIDEO_ID",
"title": "Unattended bag near the main entrance",
"content": { "...": "the report's sections" },
"created_at": "2026-10-01T09:14:03.000Z"
}GET/api/v1/projects/:projectId/reports/:reportId/viewView a report as a printable page
View a report as a printable page
Returns an HTML page. Answers 409 while the report is not ready yet.
POST/api/v1/projects/:projectId/reports/:reportId/shareCreate a share link for a report
Create a share link for a report
Anyone with the link can open the report without signing in. Optional ttl_days (1–365, default 30; null = never expires). The token is shown only in this response.
{
"id": "SHARE_ID",
"token": "SHARE_TOKEN",
"url": "https://platform.endlessai.com/share/SHARE_TOKEN",
"expires_at": "2026-10-31T09:20:00.000Z",
"resource_type": "report",
"resource_id": "REPORT_ID"
}DELETE/api/v1/projects/:projectId/reports/:reportIdDelete a report
Delete a report
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/reports/:reportId using your project key or user token.
Report Types
Templates that fix the sections of a job's report. A job with output_contract: "report" fills the report type it names, so every result has the same shape.
GET/api/v1/projects/:projectId/report-typesList report types
List report types
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/report-types using your project key or user token.
POST/api/v1/projects/:projectId/report-typesCreate or update a report type
Create or update a report type
Saved by name: posting an existing name updates it. Each section has a title, a format of text or table (a table needs columns), and optional guidance and required. Returns 201.
{
"name": "Daily opening report",
"description": "One report per store per morning",
"structure": {
"intro": "Summarise how the store opened.",
"verdict_hint": "On time / Late, with the opening time",
"sections": [
{ "title": "Summary", "format": "text", "required": true },
{
"title": "Checklist",
"format": "table",
"columns": [
{ "key": "item", "label": "Item" },
{ "key": "status", "label": "Status" },
{ "key": "time", "label": "Time" }
]
}
]
}
}GET/api/v1/projects/:projectId/report-types/:reportTypeIdGet a report type
Get a report type
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/report-types/:reportTypeId using your project key or user token.
DELETE/api/v1/projects/:projectId/report-types/:reportTypeIdDelete a report type
Delete a report type
Returns 204. Jobs that used it keep running, but their results no longer follow its sections.
Exports
Turn any Markdown document — a chat answer, a job result, a summary — into a PDF, an evidence pack, or a message on one of your notification channels. Every export is recorded in the project's audit log.
POST/api/v1/projects/:projectId/exports/pdfRender a document as PDF
Render a document as PDF
Body: markdown (required, up to 1 MB), title, footer, kind (for example job_result, summary, chat_answer) and resource_id. Returns the PDF file. Answers 503 if PDF rendering is unavailable.
POST/api/v1/projects/:projectId/exports/evidence-packDownload an evidence pack
Download an evidence pack
Returns a ZIP with the document (and its PDF when available), the frames and clips you list, and a manifest.json with each file's SHA-256 checksum. Up to 40 frames and 20 clips of at most 60 seconds each. Needs the right to download evidence packs; one pack at a time per caller.
{
"title": "Incident at gate 3",
"markdown": "## What happened\nA vehicle entered at 14:32...",
"kind": "alert",
"resource_id": "ALERT_ID",
"frames": [{ "videoId": "VIDEO_ID", "start": 872.5, "label": "Vehicle enters" }],
"clips": [{ "videoId": "VIDEO_ID", "start": 865, "end": 895 }]
}POST/api/v1/projects/:projectId/exports/sendSend a document to a notification channel
Send a document to a notification channel
Body: channel_id (one of the project's notification channels), markdown, title, up to 4 frames, and attach: pdf to attach the PDF where the channel supports it. Needs the right to send to channels. Returns { "sent": true, "attached": "pdf" }.
Exports are rate limited per caller; a 429 with rate_limited means wait a few minutes.
Webhooks
Register HTTP endpoints to receive real-time event notifications. Webhook payloads are signed with HMAC-SHA256 using a per-webhook secret.
GET/api/v1/projects/:projectId/webhooksList webhooks
List webhooks
Returns all registered webhooks for the project. Secrets are masked in the response.
GET/api/v1/projects/:projectId/webhooks/deliveriesGet webhook delivery history
Get webhook delivery history
Returns recent delivery attempts with status codes and response times.
{
"data": [
{
"id": "wh_abc123",
"name": "Moderation Alerts",
"url": "https://your-server.com/webhook",
"events": ["content.flagged", "content.banned", "content.approved"],
"secret": "whsec_...a1b2",
"status": "active",
"createdAt": "2026-02-20T...",
"stats": {
"totalCalls": 42,
"lastTriggered": "2026-02-23T...",
"successRate": 100,
"lastStatus": 200
}
}
],
"meta": { "total": 1 }
}POST/api/v1/projects/:projectId/webhooksCreate webhook
Create webhook
The secret is auto-generated and returned only on creation. Store it securely.
{
"name": "Moderation Alerts",
"url": "https://your-server.com/webhook",
"events": ["content.flagged", "content.banned"]
}Available events: content.flagged, content.approved, content.banned, test
PUT/api/v1/projects/:projectId/webhooks/:webhookIdUpdate webhook
Update webhook
{
"name": "Updated Name",
"url": "https://new-endpoint.com/hook",
"events": ["content.flagged"],
"status": "paused"
}DELETE/api/v1/projects/:projectId/webhooks/:webhookIdDelete webhook
Delete webhook
Returns 204 No Content on success.
POST/api/v1/projects/:projectId/webhooks/:webhookId/testSend test delivery
Send test delivery
Sends a test payload to the webhook URL and returns the delivery result.
// Response
{
"success": true,
"statusCode": 200,
"message": "Test webhook delivered successfully"
}Webhook payload format:
{
"event": "content.flagged",
"timestamp": "2026-02-23T10:30:00Z",
"projectId": "PROJECT_ID",
"webhookId": "wh_abc123",
"data": {
"contentId": "VIDEO_ID",
"decision": "flag",
"confidence": 85,
"flags": [
{ "category": "violence", "severity": "medium", "confidence": 0.85 }
]
}
}Signature verification:
import hmac, hashlib
def verify_webhook(payload_bytes, signature_header, secret):
"""Verify webhook signature from X-Webhook-Signature header."""
expected = hmac.new(
secret.encode(), payload_bytes, hashlib.sha256
).hexdigest()
received = signature_header.replace("sha256=", "")
return hmac.compare_digest(expected, received)
# In your webhook handler:
# signature = request.headers["X-Webhook-Signature"]
# verify_webhook(request.body, signature, "whsec_your_secret")const crypto = require("crypto");
function verifyWebhook(body, signatureHeader, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
const received = signatureHeader.replace("sha256=", "");
return crypto.timingSafeEqual(
Buffer.from(expected), Buffer.from(received)
);
}# Headers sent with each delivery:
# X-Webhook-Signature: sha256=<hmac-hex>
# X-Webhook-Id: <webhook-id>
# X-Webhook-Event: <event-type>
# Content-Type: application/jsonGET/api/v1/projects/:projectId/webhooks/eventsList the events you can subscribe to
List the events you can subscribe to
Use these names in a webhook's events list. meta.groups groups them by area.
{
"data": ["content.flagged", "content.approved", "content.banned", "content.processed", "run.completed", "run.failed", "scan.completed", "scan.failed", ...],
"meta": {
"total": 19,
"groups": {
"content": ["content.flagged", "content.approved", "content.banned", "content.processed"],
"run": ["run.completed", "run.failed"],
"scan": ["scan.completed", "scan.failed"],
...
}
}
}Notification Channels
Send alerts and job results to where your team already works — email, a webhook, WhatsApp, Telegram or Slack. Reads never return a channel secret in full; secret fields come back masked (for example ••••a1b2). Creating, changing or removing a channel needs a write role on the project.
GET/api/v1/notifications/capabilitiesList channel types available to your account
List channel types available to your account
The same catalogue as the project-level types endpoint, without needing a project — useful before your first project exists. Only types with available: true can deliver today; hide entries marked deprecated.
{
"data": [
{ "type": "email", "label": "Email", "available": true, "deprecated": false, "secret_fields": [] },
{ "type": "webhook", "label": "Webhook", "available": true, "deprecated": false, "secret_fields": ["secret"] },
{ "type": "whatsapp", "label": "WhatsApp", "available": true, "deprecated": false, "secret_fields": [] },
{ "type": "telegram_official", "label": "Telegram (EndlessAI bot)", "available": true, "deprecated": false, "secret_fields": [] },
{ "type": "slack_app", "label": "Slack App", "available": true, "deprecated": false, "secret_fields": ["bot_token", "signing_secret"] }
],
"capabilities": { "slack_official": true, "telegram_official": true }
}The list is abbreviated here.
GET/api/v1/projects/:projectId/notifications/typesList channel types for a project
List channel types for a project
Same shape as /notifications/capabilities, scoped to a project you can access.
GET/api/v1/projects/:projectId/notifications/channelsList channels
List channels
Returns every channel that delivers for this project, including personal channels shared across your projects. Secrets are masked.
{
"data": [
{
"id": "CHANNEL_ID",
"project_id": "PROJECT_ID",
"type": "email",
"name": "Night shift",
"config": { "to": ["security@company.com"] },
"min_severity": "medium",
"events": ["alert.created", "alert.confirmed"],
"broadcast_mode": "all",
"enabled": true,
"scope": "project",
"created_at": "2026-10-01T09:12:00.000Z"
}
]
}POST/api/v1/projects/:projectId/notifications/channelsCreate a channel
Create a channel
Request body:
{
"type": "email",
"name": "Night shift",
"config": { "to": ["security@company.com"] },
"min_severity": "medium",
"events": ["alert.created", "alert.confirmed"],
"broadcast_mode": "all",
"connect_existing_alerts": true
}type (required) — a type from the types list. Email takes config.to (one or more addresses); webhook takes config.url (https) and an optional config.secret for signing — one is generated if you leave it out. WhatsApp, Telegram and Slack are connected from the app, where you link the chat itself.
min_severity — low (default), medium, high or critical
events — which events to send, for example alert.created, alert.confirmed, budget.threshold, digest.scheduled, job.result
broadcast_mode — all (every eligible alert, default) or routed (only alerts a rule sends to this channel by id)
connect_existing_alerts — also add the channel to every alert rule that already lists its channels
curl -X POST https://platform.endlessai.com/api/v1/projects/PROJECT_ID/notifications/channels \
-H "X-Api-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "email",
"name": "Night shift",
"config": { "to": ["security@company.com"] },
"min_severity": "medium"
}'import requests
resp = requests.post(
f"{BASE}/projects/{PROJECT_ID}/notifications/channels",
headers={"X-Api-Key": API_KEY, "Content-Type": "application/json"},
json={
"type": "email",
"name": "Night shift",
"config": {"to": ["security@company.com"]},
"min_severity": "medium"
},
)
channel = resp.json()["data"]
print(channel["id"])const res = await fetch(
`${BASE}/projects/${PROJECT_ID}/notifications/channels`,
{
method: "POST",
headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
type: "email",
name: "Night shift",
config: { to: ["security@company.com"] },
min_severity: "medium"
})
}
);
const { data: channel } = await res.json();Response (201):
{
"data": { "id": "CHANNEL_ID", "type": "email", "name": "Night shift", "enabled": true },
"connected_alert_rules": 2,
"attached_jobs": 0
}Adding a destination that is already connected returns 400 — edit or re-enable the existing channel instead.
PATCH/api/v1/projects/:projectId/notifications/channels/:channelIdUpdate a channel
Update a channel
Send only the fields to change: name, config, min_severity, events, broadcast_mode, enabled. A masked secret sent back unchanged keeps the stored value. A personal channel can be changed only by the person who connected it.
{ "min_severity": "high", "enabled": true }DELETE/api/v1/projects/:projectId/notifications/channels/:channelIdDelete a channel
Delete a channel
Also unlinks any chat bound to the channel.
{ "id": "CHANNEL_ID", "deleted": true }GET/api/v1/projects/:projectId/notifications/channels/:channelId/bindingGet the linked chat
Get the linked chat
For a chat channel (WhatsApp, Telegram, Slack): whether a chat is linked, and which one.
{
"data": {
"configured": true,
"connected": true,
"platform": "telegram_official",
"chat_title": "Warehouse team",
"bound_at": "2026-10-02T08:30:00.000Z",
"enabled": true
}
}DELETE/api/v1/projects/:projectId/notifications/channels/:channelId/bindingUnlink the chat
Unlink the chat
Unlinks the chat but keeps the channel and its settings, ready to link again.
PUT/api/v1/projects/:projectId/notifications/channels/:channelId/project-settingsOverride a shared channel for this project
Override a shared channel for this project
A personal channel reaches several projects; its own settings are the defaults. Use this to change how it behaves for this project only. Set a field to null to go back to the default.
{
"min_severity": "high",
"events": ["alert.confirmed"],
"scan_summaries": "short",
"muted": false
}scan_summaries: full, short or off. muted: true stops this project's alerts reaching the channel.
DELETE/api/v1/projects/:projectId/notifications/channels/:channelId/project-settingsRemove this project's override
Remove this project's override
{ "data": { "cleared": true } }GET/api/v1/projects/:projectId/notifications/channels/:channelId/project-overridesCount projects with their own override
Count projects with their own override
{ "data": { "count": 2 } }POST/api/v1/projects/:projectId/notifications/channels/:channelId/apply-defaultsApply the channel defaults everywhere
Apply the channel defaults everywhere
Removes every project's override so the defaults apply.
{ "data": { "removed": 2 } }GET/api/v1/projects/:projectId/notifications/deliveriesList recent deliveries
List recent deliveries
Newest first. Query param: limit (1-100, default 20). The examples show selected response fields.
{
"data": [
{
"id": "DELIVERY_ID",
"channel_id": "CHANNEL_ID",
"alert_id": "ALERT_ID",
"event": "alert.created",
"status": "sent",
"attempts": 1,
"sent_at": "2026-10-03T22:14:05.000Z",
"created_at": "2026-10-03T22:14:02.000Z"
}
]
}Connected Accounts
The phone numbers and chat accounts you have connected to your own account, and your personal channels. This is account data, not project data: use a user token. Project keys are refused with 403.
GET/api/v1/identitiesList your connected accounts
List your connected accounts
For a WhatsApp number or Telegram account, projects lists each project you can manage and whether its alerts reach you there.
{
"data": [
{
"id": "IDENTITY_ID",
"kind": "phone",
"display": "+1 415 ••• ••42",
"first_seen": "2026-09-14T10:02:00.000Z",
"last_seen": "2026-10-10T18:40:00.000Z",
"dm_capable": true,
"projects": [
{ "project_id": "PROJECT_ID", "name": "Warehouse", "alerts": true, "channel_id": "CHANNEL_ID" },
{ "project_id": "PROJECT_ID_2", "name": "Front desk", "alerts": false }
]
}
]
}curl -s "https://platform.endlessai.com/api/v1/identities" \
-H "Authorization: Bearer YOUR_USER_TOKEN"import requests
resp = requests.get(
f"{BASE}/identities",
headers={"Authorization": f"Bearer {USER_TOKEN}"},
)
for identity in resp.json()["data"]:
print(identity["kind"], identity["display"])const res = await fetch(`${BASE}/identities`, {
headers: { Authorization: `Bearer ${USER_TOKEN}` }
});
const { data } = await res.json();PUT/api/v1/identities/:id/projects/:projectId/alertsTurn a project's alerts on or off for you
Turn a project's alerts on or off for you
For a WhatsApp number or Telegram account. Needs a write role on the project. Turning alerts off keeps the channel so you can turn it back on later.
{ "enabled": true }{ "data": { "project_id": "PROJECT_ID", "alerts": true, "channel_id": "CHANNEL_ID" } }DELETE/api/v1/identities/:idDisconnect an account
Disconnect an account
Stops every alert to that number or chat, in every project, then disconnects it. Calling it again returns the same result.
{ "data": { "id": "IDENTITY_ID", "released": true, "disabled_channels": 2 } }GET/api/v1/notifications/account-channelsList your personal channels
List your personal channels
Your personal channels, plus the channels of every project you manage, in one call. Channel objects have the same shape as the project channel list.
{
"data": [
{
"id": "CHANNEL_ID",
"project_id": "PROJECT_ID",
"type": "telegram_official",
"name": "Telegram — Dana",
"min_severity": "low",
"enabled": true,
"scope": "account"
}
],
"project_ids": ["PROJECT_ID", "PROJECT_ID_2"]
}Live Streams
Manage live streams (RTSP cameras or HLS URLs) for real-time video analysis.
GET/api/v1/projects/:projectId/streamsList streams
List streams
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/streams using your project key or user token.
POST/api/v1/projects/:projectId/streamsCreate live stream (RTSP or HLS URL)
Create live stream (RTSP or HLS URL)
{ "rtspUrl": "rtsp://camera.local/stream1", "name": "Front Door" }GET/api/v1/projects/:projectId/streams/:streamId/statusGet stream status
Get stream status
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/streams/:streamId/status using your project key or user token.
DELETE/api/v1/projects/:projectId/streams/:streamIdStop and delete stream
Stop and delete stream
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/streams/:streamId using your project key or user token.
Health (Unauthenticated)
Service health endpoints. No authentication required.
GET/api/v1/healthBetaFull service status
Full service status
Beta: this operation may change before it is final.
Status fields from the response:
{ "status": "ok", "timestamp": "2026-10-10T12:00:00Z" }GET/api/v1/health/readyCheck readiness
Check readiness
Send the request to https://platform.endlessai.com/api/v1/health/ready using your project key or user token.
GET/api/v1/health/liveCheck availability
Check availability
Send the request to https://platform.endlessai.com/api/v1/health/live using your project key or user token.
API Keys
Manage project-scoped API keys. Requires session auth (not API key auth).
GET/api/v1/api-keys?projectId=...List keys for project
List keys for project
Send the request to https://platform.endlessai.com/api/v1/api-keys?projectId=... using your project key or user token.
POST/api/v1/api-keysCreate new key
Create new key
The full key value is only returned on creation.
{ "name": "Production Key", "projectId": "PROJECT_ID" }PATCH/api/v1/api-keys/:idUpdate key status
Update key status
{ "status": "disabled" }Valid statuses: active, disabled, revoked
DELETE/api/v1/api-keys/:idDelete API key
Delete API key
Send the request to https://platform.endlessai.com/api/v1/api-keys/:id using your project key or user token.
Usage & Billing
Track platform token usage and operation counts. Divide costAt by 1,000 to display the billed amount in tokens. The examples show selected response fields.
GET/api/v1/projects/:projectId/usageGet usage summary
Get usage summary
Returns aggregated usage with a breakdown by operation type.
Optional query params: from (ISO date), to (ISO date) for time range filtering.
{
"data": {
"totals": {
"costAt": 48200,
"count": 47
},
"byOperation": [
{ "operation": "agent", "count": 12, "costAt": 29000 },
{ "operation": "chat", "count": 8, "costAt": 6000 },
{ "operation": "wizard", "count": 2, "costAt": 1200 }
],
"byResource": [...]
}
}GET/api/v1/projects/:projectId/usage/eventsList usage events (paginated)
List usage events (paginated)
Query params: limit (1-200, default 50), offset, from, to, operation, resource_id
{
"data": [
{
"id": "evt_abc123",
"operation": "agent",
"costAt": 1500,
"resourceId": "VIDEO_ID",
"timestamp": "2026-02-23T10:30:00Z"
}
],
"meta": { "returned": 50, "offset": 0, "limit": 50 }
}# Get usage summary
curl -s "https://platform.endlessai.com/api/v1/projects/PROJECT_ID/usage" \
-H "X-Api-Key: YOUR_KEY" | jq '.data.totals'
# Get usage for a time range
curl -s "https://platform.endlessai.com/api/v1/projects/PROJECT_ID/usage?from=2026-02-01&to=2026-02-28" \
-H "X-Api-Key: YOUR_KEY"
# List recent events
curl -s "https://platform.endlessai.com/api/v1/projects/PROJECT_ID/usage/events?limit=10" \
-H "X-Api-Key: YOUR_KEY"import requests
headers = {"X-Api-Key": API_KEY}
# Get summary
summary = requests.get(
f"{BASE}/projects/{PROJECT_ID}/usage",
headers=headers,
).json()["data"]
print(f"Tokens: {summary['totals']['costAt'] / 1000}")
for op in summary["byOperation"]:
print(f" {op['operation']}: {op['count']} calls, {op['costAt'] / 1000} tokens")const res = await fetch(`${BASE}/projects/${PROJECT_ID}/usage`, {
headers: { "X-Api-Key": API_KEY }
});
const { data } = await res.json();
console.log("Tokens:", data.totals.costAt / 1000);Budgets & Spending Caps
Set monthly spending caps for your account and for each project. A cap pauses work — it never bills past it. Amounts ending in _atomic_at are in thousandths of a token: divide by 1,000 to display tokens. The examples show selected response fields.
GET/api/v1/projects/:projectId/budgetGet a project's budget status
Get a project's budget status
Caps, spend this month and the current mode, taking both the account cap and the project cap into account — the stricter one wins.
normal — spending is within the cap
base — less than base_mode_pct (default 10%) of the cap is left. Alerts keep working; queued scans and other background work wait until the cap is raised or the month rolls over.
stopped — the cap is reached. No new billable work starts; nothing is deleted, and raising the cap resumes work straight away.
Optional query param: fresh=true to skip the 15-second cache.
{
"data": {
"project_id": "PROJECT_ID",
"period": "2026-10",
"mode": "normal",
"limiting_scope": null,
"base_mode_pct": 10,
"account": {
"cap_atomic_at": 200000000,
"spend_atomic_at": 48200000,
"remaining_atomic_at": 151800000,
"used_pct": 24.1,
"mode": "normal"
},
"project": {
"cap_atomic_at": 50000000,
"spend_atomic_at": 12500000,
"remaining_atomic_at": 37500000,
"used_pct": 25,
"mode": "normal"
}
}
}project is null when the project has no cap of its own; limiting_scope names the cap (account or project) that set the mode.
PUT/api/v1/projects/:projectId/budgetSet a project's monthly cap
Set a project's monthly cap
Needs a write role. Send only what you want to change; monthly_cap_atomic_at: null removes the project cap. Returns the updated status.
{
"monthly_cap_atomic_at": 50000000,
"base_mode_enabled": true,
"base_mode_pct": 10
}# Cap this project at 50,000 tokens a month
curl -X PUT https://platform.endlessai.com/api/v1/projects/PROJECT_ID/budget \
-H "X-Api-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "monthly_cap_atomic_at": 50000000 }'
# Check where the project stands
curl -s "https://platform.endlessai.com/api/v1/projects/PROJECT_ID/budget" \
-H "X-Api-Key: YOUR_KEY" | jq '.data.mode'import requests
headers = {"X-Api-Key": API_KEY}
requests.put(
f"{BASE}/projects/{PROJECT_ID}/budget",
headers=headers,
json={"monthly_cap_atomic_at": 50_000_000}, # 50,000 tokens
)
status = requests.get(
f"{BASE}/projects/{PROJECT_ID}/budget", headers=headers
).json()["data"]
project = status["project"]
print(status["mode"], project["spend_atomic_at"] / 1000, "tokens used")await fetch(`${BASE}/projects/${PROJECT_ID}/budget`, {
method: "PUT",
headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ monthly_cap_atomic_at: 50000000 }) // 50,000 tokens
});
const res = await fetch(`${BASE}/projects/${PROJECT_ID}/budget`, {
headers: { "X-Api-Key": API_KEY }
});
const { data } = await res.json();
console.log(data.mode);GET/api/v1/budget/accountGet the account budget
Get the account budget
Org admin, with a user token. The account cap, spend this month, and auto-reload settings for your active organization.
{
"data": {
"account_id": "ORG_ID",
"period": "2026-10",
"monthly_cap_atomic_at": 200000000,
"spend_this_period_atomic_at": 48200000,
"base_mode_enabled": true,
"base_mode_pct": 10,
"auto_reload": {
"enabled": true,
"threshold_atomic_at": 10000000,
"amount_atomic_at": 25000000,
"ceiling_atomic_at": 100000000,
"max_per_period": 3,
"used_this_period": 1,
"used_amount_atomic_at": 25000000,
"has_payment_method": true
},
"payment_method": { "saved": true, "label": "visa •••• 4242", "known": true }
}
}spend_this_period_atomic_at is null when spend could not be read — not zero. payment_method.known: false means the saved card could not be checked right now.
PUT/api/v1/budget/accountUpdate the account budget
Update the account budget
Org admin, with a user token. Send only what you want to change; monthly_cap_atomic_at: null removes the account cap.
{
"monthly_cap_atomic_at": 200000000,
"base_mode_pct": 10,
"auto_reload": {
"enabled": true,
"threshold_atomic_at": 10000000,
"amount_atomic_at": 25000000,
"ceiling_atomic_at": 100000000
}
}Auto-reload tops up your balance from your saved card when it drops below threshold_atomic_at. The reload amount must be at least the threshold, and the monthly ceiling at least one reload amount — otherwise 400.
GET/api/v1/budget/account/reloadsList auto-reload history
List auto-reload history
Org admin. Newest first, every attempt including failed and skipped ones. Query param: limit (default 12).
{
"data": {
"reloads": [
{
"id": "RELOAD_ID",
"period": "2026-10",
"amount_atomic_at": 25000000,
"status": "succeeded",
"error": null,
"created_at": "2026-10-06T03:10:00.000Z"
}
]
}
}Generated Assets
Images, reports, and other files created during your analysis.
GET/api/v1/projects/:projectId/generated-assetsList generated files
List generated files
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/generated-assets using your project key or user token.
DELETE/api/v1/projects/:projectId/generated-assets/:assetIdDelete asset
Delete asset
Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/generated-assets/:assetId using your project key or user token.
Team Management
Manage organization members and channel-level permissions. Members inherit org role unless overridden per channel.
GET/api/v1/team/membersList team members
List team members
Send the request to https://platform.endlessai.com/api/v1/team/members using your project key or user token.
POST/api/v1/team/membersInvite member
Invite member
{ "email": "reviewer@company.com", "role": "reviewer" }Roles: admin, operator, reviewer, viewer
PATCH/api/v1/team/members/:userIdUpdate member role
Update member role
{ "role": "operator" }DELETE/api/v1/team/members/:userIdRemove member
Remove member
Send the request to https://platform.endlessai.com/api/v1/team/members/:userId using your project key or user token.
GET/api/v1/team/my-permissionsGet current user's permissions
Get current user's permissions
Returns org role, channel overrides, and owned projects.
GET/api/v1/team/channels/:channelId/permissionsGet channel-level permissions
Get channel-level permissions
Send the request to https://platform.endlessai.com/api/v1/team/channels/:channelId/permissions using your project key or user token.
POST/api/v1/team/channels/:channelId/permissionsSet channel permission for user
Set channel permission for user
{ "user_id": "USER_ID", "role": "reviewer" }DELETE/api/v1/team/channels/:channelId/permissions/:userIdRemove channel permission override
Remove channel permission override
Send the request to https://platform.endlessai.com/api/v1/team/channels/:channelId/permissions/:userId using your project key or user token.
GET/api/v1/team/orgsList your organizations
List your organizations
Every organization you own or belong to. meta.activeOrgId is the one your requests act in.
{
"data": [
{
"id": "ORG_ID",
"name": "Acme Logistics",
"role": "admin",
"isOwner": true,
"isMember": true,
"projectCount": 4,
"isActive": true,
"createdAt": "2026-06-02T12:00:00.000Z",
"joinPolicy": "approval"
}
],
"meta": { "activeOrgId": "ORG_ID" }
}POST/api/v1/team/orgsCreate an organization
Create an organization
You become its admin, and it becomes your active organization.
{ "name": "Acme Logistics" }PATCH/api/v1/team/orgs/:idRename an organization
Rename an organization
Owner or org admin. Up to 120 characters.
{ "name": "Acme Logistics EU" }PUT/api/v1/team/active-orgSwitch your active organization
Switch your active organization
Saved to your account, so it applies everywhere you sign in. You can switch only into an organization you own or belong to; otherwise 403 ORG_UNREACHABLE.
{ "orgId": "ORG_ID" }PATCH/api/v1/team/orgs/:id/join-policySet how people from your Slack workspace join
Set how people from your Slack workspace join
For an organization linked to a Slack workspace. Owner or org admin.
{ "joinPolicy": "approval" }open — they join straight away; approval — an admin approves each request; closed — invite only.
GET/api/v1/team/join-requestsList join requests
List join requests
Org admin. Pending requests by default; add ?status=all to include decided ones.
{
"data": [
{
"id": "REQUEST_ID",
"user_id": "USER_ID",
"email": "dana@company.com",
"display_name": "Dana",
"status": "pending",
"requested_at": "2026-10-09T07:45:00.000Z"
}
]
}PATCH/api/v1/team/join-requests/:requestIdApprove or decline a join request
Approve or decline a join request
Org admin. role applies on approval (default viewer). A declined person may ask again.
{ "decision": "approved", "role": "reviewer" }POST/api/v1/team/members/:userId/reset-channel-accessClear a member's channel overrides
Clear a member's channel overrides
Org admin. Removes every channel-level role the member has in this organization, so their org role applies everywhere.
{ "success": true, "removed": 3 }Auth Tokens (Personal Access)
Create and manage personal access tokens for API access. Requires session auth (JWT), not API key.
POST/api/v1/auth/tokensCreate personal access token
Create personal access token
{ "name": "CI/CD Token", "expiresAt": "2026-12-31T00:00:00Z" }GET/api/v1/auth/tokensList my tokens
List my tokens
Send the request to https://platform.endlessai.com/api/v1/auth/tokens using your project key or user token.
DELETE/api/v1/auth/tokens/:idRevoke token
Revoke token
Send the request to https://platform.endlessai.com/api/v1/auth/tokens/:id using your project key or user token.
POST/api/v1/auth/token-loginValidate token and return user info
Validate token and return user info
Send the request to https://platform.endlessai.com/api/v1/auth/token-login using your project key or user token.
Flow: Upload & Process Video
Upload video
POST /projects/:projectId/videos/upload (multipart)
Poll status until completed
GET /projects/:projectId/videos/:videoId/status every 3-5s
Trigger processing (if not auto-started)
Start an Overview scan with POST /projects/:projectId/videos/:videoId/smart-scan, or use .../process to apply the project's processing settings.
Ready for analysis
Use agent or Q&A endpoints once status is completed
import requests, time
API_KEY = "svk_..."
BASE = "https://platform.endlessai.com/api/v1"
PROJECT_ID = "your-project-id"
# Step 1: Upload
with open("video.mp4", "rb") as f:
resp = requests.post(
f"{BASE}/projects/{PROJECT_ID}/videos/upload",
headers={"X-Api-Key": API_KEY},
files={"video": ("video.mp4", f, "video/mp4")},
)
video = resp.json()
video_id = video["id"]
print(f"Uploaded: {video_id}")
# Step 2: Poll until complete
while True:
status = requests.get(
f"{BASE}/projects/{PROJECT_ID}/videos/{video_id}/status",
headers={"X-Api-Key": API_KEY},
).json()
print(f"Status: {status['status']}")
if status["status"] == "completed":
break
if status["status"] == "error":
raise RuntimeError("Processing failed")
time.sleep(5)
# Step 3: Ask a question
result = requests.post(
f"{BASE}/projects/{PROJECT_ID}/agent/process",
headers={"X-Api-Key": API_KEY, "Content-Type": "application/json"},
json={"message": "Describe what happens in this video", "sessionId": "s1"},
timeout=300,
).json()
print(result)import { openAsBlob } from "node:fs";
const API_KEY = "svk_...";
const BASE = "https://platform.endlessai.com/api/v1";
const PROJECT_ID = "your-project-id";
// Step 1: Upload
const form = new FormData();
form.append("video", await openAsBlob("video.mp4", { type: "video/mp4" }));
const uploadRes = await fetch(`${BASE}/projects/${PROJECT_ID}/videos/upload`, {
method: "POST", headers: { "X-Api-Key": API_KEY }, body: form
});
const video = await uploadRes.json();
// Step 2: Poll
let status;
do {
await new Promise(r => setTimeout(r, 5000));
const statusRes = await fetch(
`${BASE}/projects/${PROJECT_ID}/videos/${video.id}/status`,
{ headers: { "X-Api-Key": API_KEY } }
);
status = await statusRes.json();
} while (status.status !== "completed");
// Step 3: Analyze
const result = await fetch(`${BASE}/projects/${PROJECT_ID}/agent/process`, {
method: "POST",
headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ message: "Describe what happens", sessionId: "s1" })
}).then(r => r.json());# Step 1: Upload
VIDEO_ID=$(curl -s -X POST "https://platform.endlessai.com/api/v1/projects/PROJECT_ID/videos/upload" \
-H "X-Api-Key: YOUR_KEY" -F "video=@video.mp4" | jq -r '.id')
# Step 2: Poll
while true; do
STATUS=$(curl -s -H "X-Api-Key: YOUR_KEY" \
"https://platform.endlessai.com/api/v1/projects/PROJECT_ID/videos/$VIDEO_ID/status" | jq -r '.status')
echo "Status: $STATUS"
[ "$STATUS" = "completed" ] && break
sleep 5
done
# Step 3: Analyze
curl -X POST "https://platform.endlessai.com/api/v1/projects/PROJECT_ID/agent/process" \
-H "X-Api-Key: YOUR_KEY" -H "Content-Type: application/json" \
-d '{"message": "Describe what happens", "sessionId": "s1"}'Flow: Document Context for Agent
Upload reference document
POST /projects/:projectId/documents with status: "available"
Mark as "included" to inject into agent prompt
PUT /projects/:projectId/documents/:docId with { "status": "included" } — triggers auto-compilation
Agent now has document context
Included docs are always taken into account. Available docs are searchable on demand.
Document states and agent access:
| Status | Always Used | Looked Up When Needed | Use Case |
|---|---|---|---|
| available | No | Yes | Large reference docs, manuals — agent searches on demand |
| included | Yes | Yes | Policies, instructions — always visible to agent |
| archived | No | No | Disabled, not visible to agent at all |
Flow: Content Moderation
Upload content for moderation
POST /projects/:projectId/moderate (multipart file or URL)
Content scan runs automatically
Checks the content against your moderation policy and reports potential violations.
Agent pipeline runs (if configured)
Configure automatic analysis to produce a detailed report after moderation.
Poll for results
GET /projects/:projectId/videos/:videoId/moderation — returns structured decision (allow/flag/ban)
Receive webhook (optional)
Configure webhooks to get content.flagged, content.banned, or content.approved events via HTTP POST
import requests, time
API_KEY = "svk_..."
BASE = "https://platform.endlessai.com/api/v1"
PROJECT_ID = "your-project-id"
# Step 1: Upload for moderation
with open("video.mp4", "rb") as f:
resp = requests.post(
f"{BASE}/projects/{PROJECT_ID}/moderate",
headers={"X-Api-Key": API_KEY},
files={"file": ("video.mp4", f, "video/mp4")},
)
content_id = resp.json()["contentId"]
# Step 2: Poll for moderation result
while True:
result = requests.get(
f"{BASE}/projects/{PROJECT_ID}/videos/{content_id}/moderation",
headers={"X-Api-Key": API_KEY},
).json()
if result["status"] == "completed":
break
time.sleep(3)
print(f"Decision: {result['decision']}")
print(f"Confidence: {result['confidence']}%")
for flag in result.get("flags", []):
print(f" - {flag['category']}: {flag['severity']} ({flag['confidence']})")
# Step 3: Check agent report (if pipeline is enabled)
report = requests.get(
f"{BASE}/projects/{PROJECT_ID}/videos/{content_id}/report",
headers={"X-Api-Key": API_KEY},
)
if report.status_code == 200:
agent = report.json()["data"]
print(f"Agent: {agent['decision']} ({agent['confidence']}%)")# Upload for moderation
CONTENT_ID=$(curl -s -X POST "$BASE/projects/PROJECT_ID/moderate" \
-H "X-Api-Key: YOUR_KEY" -F "file=@video.mp4" | jq -r '.contentId')
# Poll result
curl -s "$BASE/projects/PROJECT_ID/videos/$CONTENT_ID/moderation" \
-H "X-Api-Key: YOUR_KEY" | jq '{decision, confidence, flags}'
# Get stats
curl -s "$BASE/projects/PROJECT_ID/moderation/stats" \
-H "X-Api-Key: YOUR_KEY" | jq '.data'import { openAsBlob } from "node:fs";
const API_KEY = "svk_...";
const BASE = "https://platform.endlessai.com/api/v1";
const PROJECT_ID = "your-project-id";
// Upload for moderation
const form = new FormData();
form.append("file", await openAsBlob("video.mp4", { type: "video/mp4" }));
const uploadRes = await fetch(`${BASE}/projects/${PROJECT_ID}/moderate`, {
method: "POST", headers: { "X-Api-Key": API_KEY }, body: form
});
const { contentId } = await uploadRes.json();
// Poll for result
let result;
do {
await new Promise(r => setTimeout(r, 3000));
const res = await fetch(
`${BASE}/projects/${PROJECT_ID}/videos/${contentId}/moderation`,
{ headers: { "X-Api-Key": API_KEY } }
);
result = await res.json();
} while (result.status !== "completed");
console.log(result.decision, result.confidence);