POST/v1/phishing/urlscan

URLScan.io Analysis

Submit URLs to URLScan.io for full-page rendering, screenshot capture, and threat analysis. Returns verdicts, page metadata, hosting details, with explicit completion states. Screenshot links are withheld.

Try this endpoint in the playground →

Credits

3

Per request

Max URLs

5

Per request

Timeout

45s

One shared deadline for the entire batch

Plans

All

Free, Starter, Professional, Enterprise

How It Works

StepDescription
1. SubmitEach URL is submitted to URLScan.io with private visibility; requests never fall back to unlisted or public scans
2. RenderURLScan loads the page in a real browser, capturing screenshots, DOM, network requests, and certificates
3. AnalyzeCommunity and engine verdicts are computed based on page behavior, content, and known threat patterns
4. ReturnResults include verdicts, completion state, effective URLs (after redirects), page metadata, and hosting details; screenshot links are withheld

Request Body

FieldTypeRequiredDescription
urlsstring[]YesArray of 1-5 URLs to scan. Must use http:// or https:// scheme.

Code Examples

cURL

curl -X POST https://api.dfir-lab.ch/v1/phishing/urlscan \
  -H "Authorization: Bearer sk-dfir-your-key-here" \
  -H "Content-Type: application/json" \
  -d '{
    "urls": [
      "https://suspicious-login.com",
      "https://another-url.net"
    ]
  }'

Python

import requests

response = requests.post(
    "https://api.dfir-lab.ch/v1/phishing/urlscan",
    headers={
        "Authorization": "Bearer sk-dfir-your-key-here",
        "Content-Type": "application/json",
    },
    json={
        "urls": [
            "https://suspicious-login.com",
            "https://another-url.net",
        ]
    },
    timeout=60,  # 45s shared scan budget plus request overhead
)

response.raise_for_status()
data = response.json()
for result in data["data"]["results"]:
    verdict = (result.get("verdicts") or {}).get("overall") or {}
    if verdict.get("malicious") is True:
        status = "POTENTIAL THREAT"
    elif result.get("state") == "complete" and result.get("risk") == "no_known_threats" and not result.get("error") and verdict.get("malicious") is False and verdict.get("score") == 0:
        status = "NO KNOWN THREATS — safety unverified"
    else:
        status = "UNKNOWN OR SUSPICIOUS — inspect evidence"
    print(f"{result['url']}: {status}")
    # Do not log URLs or private scan IDs in production telemetry.

TypeScript

const response = await fetch(
  "https://api.dfir-lab.ch/v1/phishing/urlscan",
  {
    method: "POST",
    headers: {
      Authorization: "Bearer sk-dfir-your-key-here",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      urls: [
        "https://suspicious-login.com",
        "https://another-url.net",
      ],
    }),
    signal: AbortSignal.timeout(60_000),
  }
);

if (!response.ok) throw new Error(`HTTP ${response.status}`);
const { data, meta } = await response.json();

for (const result of data.results) {
  const overall = result.verdicts?.overall;
  const status = overall?.malicious === true ? "POTENTIAL THREAT"
    : result.state === "complete" && result.risk === "no_known_threats" && !result.error && overall?.malicious === false && overall?.score === 0
      ? "NO KNOWN THREATS — safety unverified" : "UNKNOWN OR SUSPICIOUS";
  console.log(
    `${result.url}: ${status}`,
    `(score: ${overall?.score ?? "unavailable"})`
  );
  // Keep private scan IDs and URLs out of production logs.
}

console.log(`Credits used: ${meta.credits_used}`);

Example Response

{
  "data": {
    "results": [
      {
        "url": "https://suspicious-login.com",
        "uuid": "12345678-abcd-1234-abcd-123456789012",
        "state": "complete",
        "risk": "malicious",
        "verdicts": {
          "overall": { "score": 75, "malicious": true },
          "engines": [
            { "engine": "community", "result": "malicious" }
          ]
        },
        "screenshotUrl": null,
        "pageTitle": "Login - Secure Account",
        "pageIp": "203.0.113.42",
        "pageCountry": "CN",
        "effectiveUrl": "https://suspicious-login.com/phish",
        "error": null
      },
      {
        "url": "https://another-url.net",
        "uuid": "87654321-dcba-4321-dcba-210987654321",
        "state": "complete",
        "risk": "no_known_threats",
        "verdicts": {
          "overall": { "score": 0, "malicious": false },
          "engines": []
        },
        "screenshotUrl": null,
        "pageTitle": "Another URL - Home",
        "pageIp": "198.51.100.10",
        "pageCountry": "US",
        "effectiveUrl": "https://another-url.net",
        "error": null
      }
    ],
    "summary": {
      "total": 2,
      "malicious": 1,
      "unknown": 0,
      "screenshotsAvailable": 0
    }
  },
  "meta": {
    "request_id": "req_abc",
    "credits_used": 3,
    "credits_remaining": 97,
    "processing_time_ms": 15000
  }
}

Response Fields

FieldTypeDescription
urlstringThe originally submitted URL
statecomplete | unknownWhether a valid, private result was verified
riskmalicious | suspicious | no_known_threats | unknownAssessment; no known threats does not establish safety
uuidstringURLScan scan ID — use to view full results at urlscan.io/result/{uuid}
verdicts.overall.scorenumberProvider score: zero means no detection, not proof of safety
verdicts.overall.maliciousbooleanWhether the URL is considered malicious
verdicts.enginesarrayIndividual engine verdicts (community, third-party scanners)
screenshotUrlstring | nullWithheld (null) to avoid exposing private scan artifacts
pageTitlestring | nullHTML title of the rendered page
pageIpstring | nullIP address hosting the scanned page
pageCountrystring | nullCountry code of the hosting IP
effectiveUrlstring | nullFinal URL after following all redirects
errorstring | nullError message if the scan failed for this URL

The entire URLScan batch shares a 45-second scan deadline. URLs are scanned sequentially; later items may remain unknown when the shared budget expires. The budget covers validation, submission and polling, not 45 seconds for each URL. Allow request overhead in the client timeout. The aggregate URL Assessments endpoint uses a shorter browser budget of at most 30 seconds within its overall deadline.

If a scan times out before results are ready, a partial result is returned with state: unknown, an error, and a scan ID if submission succeeded. A scan ID is not evidence of completion; screenshot links remain withheld.

Scans require private visibility on URLScan.io. Submission and result visibility are verified. Private quota failures or unexpected visibility return an unknown result; there is no fallback to unlisted or public. Unlisted scans used by older versions are visible to vetted researchers and security companies. Screenshot links are withheld; do not share scan IDs for confidential URLs.

A score of 0 with malicious: false means no threats were detected at the time of scanning. It is not a guarantee of safety. Null verdicts, errors, partial results, and unfinished scans are unknown, even when a screenshot is available. An active scan may consume one-time links or notify their sender.