DEV Community

Alex Spinov
Alex Spinov

Posted on

VirusTotal Has a Free API — Scan Files and URLs Against 70+ Antivirus Engines

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

  1. Sign up at virustotal.com
  2. Go to your profile → API Key
  3. 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)")
Enter fullscreen mode Exit fullscreen mode

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')}")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

for r in results:
print(f"{r['status']} | {r['ioc']} | {r['malicious']} detections")

Enter fullscreen mode Exit fullscreen mode




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.

Top comments (0)