Scam AI raised $2.6M to combat AI-powered scams→
scam.ai

~/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 -X POST \
  "https://api.scam.ai/v1/detections" \
  -H "x-api-key: <YOUR_API_KEY>" \
  -F "file=@/path/to/clip.mp4"

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 -X POST \
  "https://api.scam.ai/v1/detections" \
  -H "x-api-key: <YOUR_API_KEY>" \
  -F "url=https://example.com/clip.mp4"

Parameters

Request parameters
FieldTypeMeaning
filebinaryThe media file. MP4, MOV or WEBM, at most 100 MB.
urlstringA public video page or a direct link to a video file. Send file or url, not both; if both arrive the file is used.
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 -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 or audio), filename, mime_type and bytes. The last three can be null when they were not known. mime_type is what the upload declared, so a client that sends no content type sees application/octet-stream here; type comes from the bytes themselves.
modelstring
The Eva version that answered this run, "Eva V1.5" or "Eva V1.6". Which version answers is set per account.
verdictstring
LIKELY_REAL, ALERT or LIKELY_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_used is 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_s and score (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 timeout (the detector did not answer in time for that frame) or not_attempted (an earlier frame failed in a way that made the rest pointless).
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[].score to 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 cause says why: timeout means the detector did not answer in time for that frame, not_attempted means an earlier frame failed in a way that made the rest pointless. A null is never a 0. Treat it as a gap in the evidence, not as a clean frame. Unscored frames are not charged.

Start building

Create an API key in the dashboard and send your first file. To go through an integration or an on-prem deployment with our team, request a demo.