AI video detection API
Detect signals of AI-generated visuals, audio and metadata in video.
Pass video URL → create detection → poll result
https://videodetector.org/api/v1Video input
Pass the URL of your video. Direct file uploads and upload IDs are not supported. Supported formats: MP4, MOV, AVI, FLV and TS; maximum 400,000,000 bytes and 1,800 seconds. Provide a publicly accessible direct file URL, not a player page, sharing page or login-protected link. The server reads video metadata to verify size and duration, then uses your original URL for detection without storing a copy of the video.
- Prepare an HTTP or HTTPS direct video URL (standard port, maximum 2,048 ASCII characters). Its hostname must resolve to public IPv4 addresses. Private addresses, redirects and custom authorization headers are not supported. Signed URLs are supported but must remain valid throughout queueing and detection.
- The origin must support Range requests with 206, Content-Range, Content-Length and a strong ETag or Last-Modified header. Keep the content unchanged until detection finishes. Videos that cannot be read or verified are rejected.
- POST /video-detections with the JSON below and a unique Idempotency-Key. The server verifies duration and reserves credits. A 202 response contains the detection id and current status.
- GET /video-detections/{id} every 15 seconds until succeeded, failed, timed_out or cancelled. GET /video-detections?ids=id1,id2 batches up to 25 IDs within one request.
Create a detection
{
"input": {
"type": "url",
"url": "https://media.example.com/video.mp4"
},
"client_reference": "order-123"
}A 429 response includes Retry-After (seconds). Creation endpoints require an Idempotency-Key (1–128 letters, numbers, dots, underscores, colons or hyphens). Retry the same request with the same key; changing its payload returns 409. Keep keys after a timeout. Expired resources cannot be recreated with an old key.
Understand the result
The response contains id, service, status, client_reference, input.duration_seconds, billing, result, error and timestamps. Result verdicts are suspected_ai, not_detected or unknown. Missing evidence stays unknown. Provider confidence describes an individual provider label; it is not the probability that the entire video is AI-generated. Detection is probabilistic and cannot prove authenticity.
billing.state is held, charged or released. held_credits is the reserved amount; charged_credits is the final charge. A queued task may be cancelled; started jobs cannot be cancelled. timed_out is terminal for the API even if the upstream provider later responds.
GET /video-detections/{id}/evidence returns up to 50 evidence entries with frame offsets, audio time ranges and detection labels. Follow next_cursor until null. The API retains detection records and results; it does not host or provide downloads of the source video at your URL.
Frame offsets and audio start/end times are expressed in seconds. Evidence preserves upstream labels and numeric confidence without inventing a combined score.
{
"id": "DETECTION_ID",
"service": "video_ai",
"status": "succeeded",
"client_reference": "order-123",
"input": {
"duration_seconds": 61
},
"billing": {
"state": "charged",
"held_credits": 0,
"charged_credits": 2
},
"result": {
"schema_version": "1",
"verdict": "suspected_ai",
"visual": {
"verdict": "suspected_ai",
"returned_frame_count": 12,
"matched_returned_frame_count": 3
},
"audio": {
"verdict": "unknown",
"reason": "provider_result_missing"
},
"metadata": {
"ai_marker_detected": null
}
},
"error": null,
"created_at": "2026-10-10T00:00:00Z",
"started_at": "2026-10-10T00:00:01Z",
"completed_at": "2026-10-10T00:01:00Z",
"expires_at": null,
"request_id": "REQUEST_ID"
}Endpoints
| GET | /account |
|---|---|
| POST · GET | /video-detections |
| GET | /video-detections/{id} |
| GET | /video-detections/{id}/evidence |
| POST | /video-detections/{id}/cancel |
Complete working examples
Python 3.10+ (standard library) and Node.js 20+ (no dependencies). Both accept a video URL, submit a detection and poll the result. They coordinate requests at 1 QPS and reuse keys on retry. Each example assumes it is the only client using the account.
Set VIDEO_API_TOKEN, then run python3 video-api.py "https://media.example.com/video.mp4" or node video-api.mjs "https://media.example.com/video.mp4". VIDEO_API_BASE defaults to https://videodetector.org/api/v1. Save the printed key and detection ID; set VIDEO_DETECTION_KEY and pass the same URL to recover a submission.
# Python 3.10+. Standard library only. Run: python video-api.py https://media.example.com/video.mp4
# Set VIDEO_API_TOKEN; optionally set VIDEO_API_BASE (default below).
import json
import os
import sys
import time
import uuid
import urllib.error
import urllib.request
BASE = os.environ.get("VIDEO_API_BASE", "https://videodetector.org/api/v1").rstrip("/")
TOKEN = os.environ["VIDEO_API_TOKEN"]
next_request = 0.0
def request(method, path, body=None, key=None):
global next_request
headers = {"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json"}
if key:
headers["Idempotency-Key"] = key
data = None if body is None else json.dumps(body).encode()
for attempt in range(6):
time.sleep(max(0, next_request - time.monotonic()))
req = urllib.request.Request(BASE + path, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=120) as response:
return json.load(response)
except urllib.error.HTTPError as error:
try:
detail = json.loads(error.read())
except (ValueError, UnicodeError):
detail = {"error": {"code": "HTTP_ERROR", "status": error.code}}
code = detail.get("error", {}).get("code")
if attempt == 5 or not (error.code in (429, 500, 502, 503, 504) or code in ("RESOURCE_BUSY",)):
raise RuntimeError(detail) from None
time.sleep(max(float(error.headers.get("Retry-After", 0)), min(2 ** attempt, 16)))
except (urllib.error.URLError, TimeoutError):
if attempt == 5:
raise
time.sleep(min(2 ** attempt, 16))
finally:
# Wait after the response; connection latency must not compress arrival gaps.
next_request = time.monotonic() + 1.05
raise RuntimeError("Request retries exhausted")
# The URL must stay accessible and its contents unchanged until detection finishes.
video_url = sys.argv[1]
if not video_url.startswith(("https://", "http://")):
raise ValueError("Pass a publicly accessible video URL")
detection_key = os.environ.get("VIDEO_DETECTION_KEY", str(uuid.uuid4()))
print("Recovery key:", detection_key)
job = request("POST", "/video-detections", {
"input": {"type": "url", "url": video_url},
"client_reference": "video-example"
}, detection_key)
print("Detection ID:", job["id"])
deadline = time.monotonic() + 1800
while job["status"] in ("queued", "processing"):
if time.monotonic() > deadline:
raise RuntimeError("Still processing. Query the printed detection ID later; do not create another task.")
time.sleep(15)
job = request("GET", "/video-detections/" + job["id"])
print(json.dumps(job, indent=2, ensure_ascii=False))
Errors
400 invalid input or URL; 401 invalid token; 402 insufficient credits; 403 API disabled; 404 not found; 409 conflicting key, unfinished-job limit or task state; 410 expired resource; 413 request too large; 422 inaccessible video, unsupported range reads, missing content validator, changed content or invalid format/duration; 429 account rate limit; 5xx temporary error. JSON errors include error.code and request_id. Back off and retry transient errors with the original key. Never recreate an unknown submission with a new key.