~/docs cat detect-video.md
Detect a video
Checks a clip frame by frame for deepfakes and face swaps, and answers with one verdict for the whole clip plus a timeline with a score for every sampled frame.
Option 1: send the file
curl
curl -X POST "https://api.scam.ai/v1/detections" \ -H "x-api-key: <YOUR_API_KEY>" \ -F "file=@/path/to/clip.mp4"
Option 2: send a link
A public video page, or a direct link to a video file. A link adds a source block to the response; see Detect from a link.
curl
curl -X POST "https://api.scam.ai/v1/detections" \ -H "x-api-key: <YOUR_API_KEY>" \ -F "url=https://example.com/clip.mp4"
Parameters
| Field | Type | Meaning |
|---|---|---|
file | binary | The media file. MP4, MOV, WEBM, MKV or AVI, at most 100 MB. |
url | string | A public video page or a direct link to a video file. Send file or url, not both; if both arrive the file is used. |
save | "true" | "false" | Keep a history row and the file. Default "true". With "false" nothing is stored and the run cannot be retrieved later. |
thumbnails | "true" | "false" | Include a small base64 JPEG of each frame the platform returned a still for, as frames[].thumbnail_b64. Default "false". For display only; it makes the response much larger. |
To ask for the frame stills as well:
curl
curl -X POST "https://api.scam.ai/v1/detections" \ -H "x-api-key: <YOUR_API_KEY>" \ -F "file=@/path/to/clip.mp4" \ -F "thumbnails=true"
The response
- idstring
- The detection id. Pass it to GET /v1/detections/{id} to read the run back.
- statusstring
- Always
"completed". The answer arrives in the same request. - created_atstring
- When the run finished. RFC 3339, millisecond precision, always Z.
- mediaobject
- What was checked:
type(image, video, audio or document),filename,mime_typeandbytes. The last three can be null when they were not known.mime_typeis what the upload declared, so a client that sends no content type seesapplication/octet-streamhere;typecomes from the bytes themselves. - modelstring
- The model that answered,
"Eva V1.6". - verdictstring
LIKELY_REAL,ALERTorLIKELY_AI. Branch on this.- scorenumber | null
- 0 to 1, the probability the media is AI-made or manipulated. null when nothing could be scored.
- summarystring
- One sentence a person can read, matching the verdict.
- credits_usednumber
- What this run cost, an integer. 0 on a sandbox key, or when the same file was sent byte for byte from this account within 30 days.
- zero_charge_reasonstring
- Only when
credits_usedis 0:"sandbox"or"duplicate". - threshold_usednumber
- The line the per-frame scores were judged against on this run. Draw it under frames[].score. Absent when there is nothing to draw.
- framesarray
- One entry per sampled frame, the whole clip:
index,timestamp_sandscore(0 to 1 with four decimals, or null when that frame was not scored). Absent when no frame series was produced. - frames[].causestring
- Only beside a null score: why that frame has no score, for example
timeoutorno_face. - frames[].thumbnail_b64string
- A small base64 JPEG of that frame, for display only. Present only when the request sent
thumbnails=true.
response · 200
{
"id": "0b7d4a92-5e1c-4f38-9a6d-2c8e1f0b3d47",
"status": "completed",
"created_at": "2026-09-15T10:18:02.517Z",
"media": {
"type": "video",
"filename": "clip.mp4",
"mime_type": "video/mp4",
"bytes": 4123904
},
"model": "Eva V1.6",
"verdict": "ALERT",
"score": 0.56,
"summary": "We detected some AI-related signals, but the result is not conclusive.",
"credits_used": 4,
"threshold_used": 0.5,
"frames": [
{ "index": 0, "timestamp_s": 0.0, "score": 0.5731 },
{ "index": 1, "timestamp_s": 1.0, "score": 0.5402 },
{ "index": 2, "timestamp_s": 2.0, "score": null, "cause": "timeout" },
{ "index": 3, "timestamp_s": 3.0, "score": 0.5688 },
{ "index": 4, "timestamp_s": 4.0, "score": 0.5519 }
]
}Frame 2 carries no score and says why. thumbnail_b64 appears on a frame only when the request sent thumbnails=true and the platform returned a still for it.
Questions
- Why 20 frames?
- A clip is sampled about one frame a second for its first 20 seconds; a longer clip gets 20 frames spread across its length. Every scored frame is one credit and a frame with a null score is not charged, so a video never costs more than 20 credits, whatever its length.
- What is threshold_used?
- The line the per-frame scores were judged against on this run. Draw it under
frames[].scoreto show which moments crossed it. It describes this response, not a setting you can change; the verdict for the clip is decided by the platform. - Why is a frame's score null?
- Nothing could score that moment, and
causesays why:timeoutmeans the detector did not answer in time for that frame,no_facemeans no face was found in it. A null is never a 0. Treat it as a gap in the evidence, not as a clean frame. Unscored frames are not charged.