VirusTotal scans files and URLs against 70+ antivirus engines simultaneously. Their API is free for up to 500 requests per day.
One scan. Seventy verdicts. Zero cost.
The Story
A friend downloaded a tool from GitHub. Their antivirus said it was clean. But they had a nagging feeling — one engine isn't enough.
They uploaded it to VirusTotal manually. Three engines flagged it as a trojan. The "clean" binary was actually malware that their specific AV didn't detect.
Now they automate this check for every download. Here's how.
Get Your Free API Key
- Sign up at virustotal.com
- Go to your profile → API Key
- Copy your key (free tier: 500 lookups/day, 4 requests/minute)
Scan a URL
\`python
import requests
import time
API_KEY = "your-api-key-here"
headers = {"x-apikey": API_KEY}
Submit URL for scanning
url_to_scan = "https://example.com/suspicious-download"
response = requests.post(
"https://www.virustotal.com/api/v3/urls",
headers=headers,
data={"url": url_to_scan}
)
analysis_id = response.json()["data"]["id"]
print(f"Scan submitted: {analysis_id}")
Wait for results
time.sleep(15)
Get results
result = requests.get(
f"https://www.virustotal.com/api/v3/analyses/{analysis_id}",
headers=headers
).json()
stats = result["data"]["attributes"]["stats"]
print(f"Malicious: {stats['malicious']}")
print(f"Suspicious: {stats['suspicious']}")
print(f"Clean: {stats['undetected']}")
print(f"Timeout: {stats['timeout']}")
`\
Check a File Hash
Don't upload the file — just check its hash. Faster and more private:
\`python
import hashlib
def check_file(filepath):
"""Check if a file is known malware by its SHA-256 hash."""
# Calculate hash
sha256 = hashlib.sha256()
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
sha256.update(chunk)
file_hash = sha256.hexdigest()
# Query VirusTotal
response = requests.get(
f"https://www.virustotal.com/api/v3/files/{file_hash}",
headers={"x-apikey": API_KEY}
)
if response.status_code == 200:
attrs = response.json()["data"]["attributes"]
stats = attrs["last_analysis_stats"]
print(f"File: {filepath}")
print(f"SHA-256: {file_hash}")
print(f"Detection: {stats['malicious']}/{sum(stats.values())} engines")
print(f"First seen: {attrs.get('first_submission_date', 'Unknown')}")
if stats["malicious"] > 0:
print("⚠ MALWARE DETECTED!")
# Show which engines flagged it
for engine, result in attrs["last_analysis_results"].items():
if result["category"] == "malicious":
print(f" └─ {engine}: {result['result']}")
else:
print("✓ File appears clean")
elif response.status_code == 404:
print(f"File not in VirusTotal database (never scanned)")
check_file("suspicious_file.exe")
`\
Scan a Domain
Check if a domain is associated with malware:
\`python
def check_domain(domain):
"""Check domain reputation on VirusTotal."""
response = requests.get(
f"https://www.virustotal.com/api/v3/domains/{domain}",
headers={"x-apikey": API_KEY}
)
if response.status_code == 200:
attrs = response.json()["data"]["attributes"]
stats = attrs.get("last_analysis_stats", {})
print(f"Domain: {domain}")
print(f"Malicious: {stats.get('malicious', 0)}")
print(f"Suspicious: {stats.get('suspicious', 0)}")
print(f"Clean: {stats.get('undetected', 0)}")
# WHOIS data
whois = attrs.get("whois", "Not available")
print(f"Registrar: {attrs.get('registrar', 'Unknown')}")
print(f"Created: {attrs.get('creation_date', 'Unknown')}")
check_domain("example.com")
`\
Batch Scanner — Check Multiple IOCs
\`python
import time
def batch_scan(iocs, ioc_type="url"):
"""Scan multiple indicators of compromise."""
results = []
for ioc in iocs:
if ioc_type == "url":
endpoint = f"https://www.virustotal.com/api/v3/urls"
# URL ID is base64url of the URL
import base64
url_id = base64.urlsafe_b64encode(ioc.encode()).decode().strip("=")
r = requests.get(
f"https://www.virustotal.com/api/v3/urls/{url_id}",
headers={"x-apikey": API_KEY}
)
elif ioc_type == "hash":
r = requests.get(
f"https://www.virustotal.com/api/v3/files/{ioc}",
headers={"x-apikey": API_KEY}
)
elif ioc_type == "ip":
r = requests.get(
f"https://www.virustotal.com/api/v3/ip_addresses/{ioc}",
headers={"x-apikey": API_KEY}
)
if r.status_code == 200:
stats = r.json()["data"]["attributes"].get("last_analysis_stats", {})
mal = stats.get("malicious", 0)
results.append({"ioc": ioc, "malicious": mal, "status": "⚠ THREAT" if mal > 0 else "✓ Clean"})
else:
results.append({"ioc": ioc, "malicious": 0, "status": "? Not found"})
time.sleep(15) # Free tier rate limit
for r in results:
print(f"{r['status']} | {r['ioc']} | {r['malicious']} detections")
Check suspicious IPs
batch_scan(["8.8.8.8", "1.1.1.1"], ioc_type="ip")
`\
What You Can Build
- Download scanner — automatically check every file before opening
- Email security gateway — scan attachments and links
- Incident response tool — bulk-check IOCs from threat reports
- Browser extension backend — warn users about malicious URLs
- CI/CD security check — scan build artifacts before deployment
Free Tier Limits
| Feature | Free | Premium |
|---|---|---|
| Lookups/day | 500 | 30,000+ |
| Requests/minute | 4 | 720 |
| File upload | 32 MB | 650 MB |
| Hunting rules | No | Yes |
500 lookups per day is generous for personal security tools and small projects.
Building security tools? Check my GitHub for more free API tutorials and developer tools.
Need custom dev tools, scrapers, or API integrations? I build automation for dev teams. Email spinov001@gmail.com — or explore awesome-web-scraping.
More from me: 10 Dev Tools I Use Daily | 77 Scrapers on a Schedule | 150+ Free APIs
Also: Neon Free Postgres | Vercel Free API | Hetzner 4x More Server
NEW: I Ran an AI Agent for 16 Days — What Actually Works
Need data from the web without writing scrapers? Check my *Apify actors** — ready-made scrapers for HN, Reddit, LinkedIn, and 75+ more sites. Or email: spinov001@gmail.com*
Top comments (0)