/v1/phishing/urlscanURLScan.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.
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
| Step | Description |
|---|---|
| 1. Submit | Each URL is submitted to URLScan.io with private visibility; requests never fall back to unlisted or public scans |
| 2. Render | URLScan loads the page in a real browser, capturing screenshots, DOM, network requests, and certificates |
| 3. Analyze | Community and engine verdicts are computed based on page behavior, content, and known threat patterns |
| 4. Return | Results include verdicts, completion state, effective URLs (after redirects), page metadata, and hosting details; screenshot links are withheld |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| urls | string[] | Yes | Array 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
| Field | Type | Description |
|---|---|---|
| url | string | The originally submitted URL |
| state | complete | unknown | Whether a valid, private result was verified |
| risk | malicious | suspicious | no_known_threats | unknown | Assessment; no known threats does not establish safety |
| uuid | string | URLScan scan ID — use to view full results at urlscan.io/result/{uuid} |
| verdicts.overall.score | number | Provider score: zero means no detection, not proof of safety |
| verdicts.overall.malicious | boolean | Whether the URL is considered malicious |
| verdicts.engines | array | Individual engine verdicts (community, third-party scanners) |
| screenshotUrl | string | null | Withheld (null) to avoid exposing private scan artifacts |
| pageTitle | string | null | HTML title of the rendered page |
| pageIp | string | null | IP address hosting the scanned page |
| pageCountry | string | null | Country code of the hosting IP |
| effectiveUrl | string | null | Final URL after following all redirects |
| error | string | null | Error 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.