~/docs cat detect-link.md
Detect from a link
Checks a video from a public link: a public video page, or a direct link to a video file. The server fetches it; you never download it yourself. Links are fetched for video only; an image or a recording is sent as a file. The reply is the video envelope plus a source block.
Send the link
curl
curl -X POST "https://api.scam.ai/v1/detections" \ -H "x-api-key: <YOUR_API_KEY>" \ -F "url=https://www.youtube.com/watch?v=dQw4w9WgXcQ"
A JSON body is accepted too, with save and thumbnails as booleans:
curl · json
curl -X POST "https://api.scam.ai/v1/detections" \
-H "x-api-key: <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "thumbnails": true}'Parameters
| Field | Type | Meaning |
|---|---|---|
url | string | The link to a public video page or a video file. http or https only. Send url on its own; if a file is attached as well, 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. |
The response
The video envelope with source added.
- 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. - sourceobject
- Where the media came from:
url,canonical_url,platform,title,uploader,duration_s,truncatedandanalyzed_duration_s. A key the platform did not report is absent, excepttruncated, which is always a boolean.
response · 200
{
"id": "e4d2b8f1-3c6a-4d9e-b7f0-1a5c8e2d9b36",
"status": "completed",
"created_at": "2026-09-15T10:25:09.204Z",
"media": {
"type": "video",
"filename": "youtube-dQw4w9WgXcQ.mp4",
"mime_type": "video/mp4",
"bytes": 21398112
},
"model": "Eva V1.6",
"verdict": "LIKELY_REAL",
"score": 0.08,
"summary": "No strong signs of deepfake manipulation were found.",
"credits_used": 20,
"threshold_used": 0.5,
"frames": [
{ "index": 0, "timestamp_s": 0.0, "score": 0.0611 },
{ "index": 1, "timestamp_s": 1.0, "score": 0.0842 }
],
"source": {
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"canonical_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"platform": "youtube",
"title": "Example video",
"uploader": "Example channel",
"duration_s": 212.0,
"truncated": false,
"analyzed_duration_s": 212.0
}
}The frames list is shortened here; a real reply carries every sampled frame.
Questions
- How is a long video sampled, and what does truncated mean?
- A video of 20 seconds or less is fetched whole. A longer one is not: the server fetches 20 one-second slivers spread evenly across its whole length and scores one frame from each, so
analyzed_duration_sequalsduration_sandtruncatedis false. In that caseframes[].timestamp_scounts through the stitched slivers (0 to 19), not the original video.truncatedis true only when just the front of the media was fetched, and thenanalyzed_duration_sis the length that was scored. A link to a video longer than 5 minutes, a playlist or a live stream is refused with422; upload a clip instead. - Which links work?
- Public video pages the resolver supports, and direct links to video files. A private or login-walled page, a page that is not a video, or a link the server cannot reach answers
400or422with a message written for an end user. A video longer than 5 minutes, a playlist or a live stream answers422. A fetched video over 100 MB answers413. A deployment that cannot fetch links answers501; upload the file instead. - Is a link charged differently from an upload?
- No. A link becomes the same file the upload path sees, so it costs what that media costs:
credits_usedfor a video link is the number of frames scored, at most 20.