~/docs cat detect-link.md
Detect from a link
Checks media from a public link: a public video page, or a direct link to a media file (an image, a video, or a recording). The server fetches it; you never download it yourself. A web page that is not media is refused with a 422. The reply is the matching envelope plus a source block.
Send the link
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 thumbnails as a boolean:
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 | A public video page, or a direct link to an image, video or audio file. http or https only. Send url on its own; if a file is attached as well, 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. |
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 or audio),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 Eva version that answered this run,
"Eva V1.5"or"Eva V1.6". Which version answers is set per account. - 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
timeout(the detector did not answer in time for that frame) ornot_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. - 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 image, video or audio files. A web page that is not media, a private, deleted or login-walled video, a playlist, a live stream or a video longer than 5 minutes answers
422 LINK_NOT_RESOLVABLE, with the code as a string inerror,retryablebeside it and the resolver's ownresolverCode; a link that is not http or https answers400 INVALID_URL. A fetched file 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.
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.