The Platform API

API documentation

Manage your videos, ask questions, and build applications with the Platform API. Explore the operations below for request details, responses, and examples.

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.

Base URL
https://platform.endlessai.com/api/v1
Auth
Project key or user token
Format
JSON (application/json)
Envelope Format

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.

1. X-Api-Key Header
X-Api-Key: svk_...

API keys are project-bound. All requests are automatically scoped to the key's project.

2. Bearer Token
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.

3. Query Parameter (SSE only)
?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/projects

Errors & Responses

All errors return JSON with an error field:

{
  "error": "Human-readable error message"
}
400Bad RequestValidation error or missing fields
401UnauthorizedMissing or invalid authentication
403ForbiddenInsufficient permissions or wrong project
404Not FoundResource does not exist
409ConflictInvalid state transition (e.g. alert already resolved)
500Server ErrorInternal error

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"}'
// 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"
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" }
        }
      ]
    }'
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
    }'
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:

available — searchable by agent tool
included — always taken into account by the agent
archived — hidden from agent
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"
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"}'
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"
  }'

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"
// 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}'

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]
Agent vs Direct Chat:Direct Chat is fast and cheap — direct Q&A against video content. Use it for simple factual questions. The Agent endpoint is for complex analysis: it can answer questions across videos and supporting documents and provide detailed findings.

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"}'
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"] }
    }'
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" }'
{
    "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"
    }'
// 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" }'
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.

Share Links

Public links to a single alert, report or job result. Anyone with the link can view that one item without signing in, until the link expires or you revoke it.

GET/api/v1/projects/:projectId/shareList share links

List share links

Query params: resource_type, resource_id. Each link shows its expiry, whether it was revoked, and how often it was opened. Tokens are never returned again after creation.

POST/api/v1/projects/:projectId/shareCreate a share link

Create a share link

resource_type is alert, report or job_result. ttl_days is 1–365 (default 30); null means the link never expires. Save the url — the token is shown only in this response.

{ "resource_type": "alert", "resource_id": "ALERT_ID", "ttl_days": 7 }
DELETE/api/v1/projects/:projectId/share/:shareIdRevoke a share link

Revoke a share link

Send the request to https://platform.endlessai.com/api/v1/projects/:projectId/share/:shareId using your project key or user token.

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")
GET/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"
    }'

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

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'
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

1

Upload video

POST /projects/:projectId/videos/upload (multipart)

2

Poll status until completed

GET /projects/:projectId/videos/:videoId/status every 3-5s

3

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.

4

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)

Flow: Document Context for Agent

1

Upload reference document

POST /projects/:projectId/documents with status: "available"

2

Mark as "included" to inject into agent prompt

PUT /projects/:projectId/documents/:docId with { "status": "included" } — triggers auto-compilation

3

Agent now has document context

Included docs are always taken into account. Available docs are searchable on demand.

Document states and agent access:

StatusAlways UsedLooked Up When NeededUse Case
availableNoYesLarge reference docs, manuals — agent searches on demand
includedYesYesPolicies, instructions — always visible to agent
archivedNoNoDisabled, not visible to agent at all

Flow: Content Moderation

1

Upload content for moderation

POST /projects/:projectId/moderate (multipart file or URL)

2

Content scan runs automatically

Checks the content against your moderation policy and reports potential violations.

3

Agent pipeline runs (if configured)

Configure automatic analysis to produce a detailed report after moderation.

4

Poll for results

GET /projects/:projectId/videos/:videoId/moderation — returns structured decision (allow/flag/ban)

5

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']}%)")