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

~/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.

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

Request parameters
FieldTypeMeaning
urlstringA 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_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.
sourceobject
Where the media came from: url, canonical_url, platform, title, uploader, duration_s, truncated and analyzed_duration_s. A key the platform did not report is absent, except truncated, 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_s equals duration_s and truncated is false. In that case frames[].timestamp_s counts through the stitched slivers (0 to 19), not the original video. truncated is true only when just the front of the media was fetched, and then analyzed_duration_s is the length that was scored. A link to a video longer than 5 minutes, a playlist or a live stream is refused with 422; 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 in error, retryable beside it and the resolver's own resolverCode; a link that is not http or https answers 400 INVALID_URL. A fetched file over 100 MB answers 413. A deployment that cannot fetch links answers 501; 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_used for 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.