virustotal-api — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited virustotal-api (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
VirusTotal (VT) is a threat intelligence platform that aggregates 70+ antivirus engines, 10+ dynamic analysis sandboxes, and numerous other security tools. Its REST API (v3) is the primary programmatic interface. The API is inspired by the JSON:API specification and returns JSON for all requests and responses, including errors.
Base URL: https://www.virustotal.com/api/v3
Authentication: Every request must include the header x-apikey: <YOUR_API_KEY>. Your key is found at https://www.virustotal.com/gui/my-apikey after signing into VirusTotal Community. Never pass the key as a query parameter — always use the header.
# Example: Get a file report by SHA-256
curl --request GET \
--url https://www.virustotal.com/api/v3/files/{sha256} \
--header 'x-apikey: YOUR_API_KEY' \
--header 'accept: application/json'# Python equivalent
import requests
url = "https://www.virustotal.com/api/v3/files/{sha256}"
headers = {"accept": "application/json", "x-apikey": "YOUR_API_KEY"}
response = requests.get(url, headers=headers)
print(response.json())Almost every decision about what you can build with the VT API depends on which tier you have. The two tiers are Public API (free) and Premium API (paid, also called "Private API" or "VT Enterprise"). Throughout this skill and the VT docs, a 🔒 icon marks enterprise-only endpoints or relationships.
| Aspect | Detail |
|---|---|
| Cost | Free — sign up at virustotal.com |
| Rate limit | 4 requests per minute |
| Daily quota | 500 requests per day |
| Rate limit scope | Enforced per (IP address, API key) tuple |
| Commercial use | Prohibited |
| File download | Not available |
| Search | Basic search only (exact hash, URL, domain, IP) |
| Hunting | IoC Stream only (no Livehunt, no Retrohunt) |
| Feeds | Not available |
| Private scanning | Not available |
| Relationships | Subset — many relationship types are 🔒 enterprise-only |
| File attributes | Subset — fields like exiftool, malware_config are 🔒 |
Restrictions to emphasize:
| Aspect | Detail |
|---|---|
| Cost | Paid — contact VT sales for pricing based on usage |
| Rate limit | Governed by your licensed service step (no fixed public cap) |
| Daily quota | Governed by your license |
| Commercial use | Allowed per agreement |
| File download | Yes — download samples, PCAPs, memory dumps, EVTX |
| Search | VT Intelligence advanced corpus search with full modifiers |
| Hunting | Livehunt (real-time YARA) + Retrohunt (historical YARA) |
| Feeds | Full intelligence feeds (file, URL, domain, IP, sandbox) |
| Private scanning | Yes — scan files/URLs without contributing to public corpus |
| Relationships | Full set including embedded_domains, itw_urls, similar_files |
| File attributes | Full set including exiftool, malware_config, office_info |
Additional enterprise capabilities:
Successful requests return HTTP 200 with this structure:
{
"data": <response_data>
}Where <response_data> is typically an object like:
{
"type": "file",
"id": "<sha256>",
"links": { "self": "https://www.virustotal.com/api/v3/files/<sha256>" },
"attributes": { ... }
}For collections (lists), the response includes pagination:
{
"data": [ ... ],
"meta": { "cursor": "..." },
"links": { "self": "...", "next": "..." }
}Errors return the appropriate HTTP status code (4xx for client errors, 5xx for server errors):
{
"error": {
"code": "NotFoundError",
"message": "Resource \"xyz\" not found"
}
}Common error codes:
AuthenticationRequiredError — missing or invalid x-apikey headerWrongCredentialsError — API key is not validForbiddenError — insufficient privileges (e.g., free key hitting enterprise endpoint)NotFoundError — resource does not exist in VT datasetQuotaExceededError — rate limit or daily quota exceededTooManyRequestsError — too many requests in a short periodTransientError — temporary server issue, retry with backoffFor the full endpoint reference with request/response details, read:
references/endpoints-free.md — All endpoints available to free-tier usersreferences/endpoints-enterprise.md — Enterprise-only endpoints (🔒)references/objects-and-relationships.md — API object schemas and relationship typesThese are the bread-and-butter endpoints most integrations use:
Files
POST /files — Upload a file for scanning (max 32MB; use upload URL for larger)GET /files/upload_url — Get a URL for uploading files >32MB (up to 650MB)GET /files/{id} — Get a file report by hash (MD5, SHA-1, or SHA-256)POST /files/{id}/analyse — Request a rescan of a known fileGET /files/{id}/comments — Get comments on a filePOST /files/{id}/comments — Add a comment to a fileGET /files/{id}/votes — Get votes on a filePOST /files/{id}/votes — Cast a vote on a fileGET /files/{id}/relationships/{relationship} — Get related objectsURLs
POST /urls — Submit a URL for scanningGET /urls/{id} — Get a URL report (id = base64url of the URL without padding)POST /urls/{id}/analyse — Request a rescanDomains
GET /domains/{domain} — Get a domain reportPOST /domains/{domain}/rescan — Request a rescanIP Addresses
GET /ip_addresses/{ip} — Get an IP address reportPOST /ip_addresses/{ip}/rescan — Request a rescanComments (global)
GET /comments — Get latest comments across VTGET /comments/{id} — Get a specific commentDELETE /comments/{id} — Delete your own commentAnalyses & Submissions
GET /analyses/{id} — Get status/results of a scan analysisGET /submissions/{id} — Get a submission objectSearch (basic)
GET /search?query={query} — Basic search for files, URLs, domains, IPs, commentsThese require a premium API key. Attempting them with a free key returns ForbiddenError.
VT Intelligence (Advanced Search)
GET /intelligence/search?query={query} — Full corpus search with modifiersGET /intelligence/search/snippets/{id} — Content search snippetsFile Downloads
GET /files/{id}/download_url — Get a time-limited download URLGET /files/{id}/download — Download the actual file bytesVT Hunting
VT Feeds
Private Scanning
Collections (advanced)
Zipping Files
Administration
VT Monitor
VT Graph
import requests
API_KEY = "your_api_key_here"
FILE_HASH = "44d88612fea8a8f36de82e1278abb02f" # EICAR test file MD5
url = f"https://www.virustotal.com/api/v3/files/{FILE_HASH}"
headers = {"x-apikey": API_KEY}
response = requests.get(url, headers=headers)
if response.status_code == 200:
data = response.json()
stats = data["data"]["attributes"]["last_analysis_stats"]
print(f"Malicious: {stats['malicious']}, Undetected: {stats['undetected']}")
elif response.status_code == 404:
print("File not found in VT database")
else:
print(f"Error: {response.status_code} - {response.json()}")import requests, base64
API_KEY = "your_api_key_here"
TARGET_URL = "https://example.com"
# Step 1: Submit the URL
headers = {"x-apikey": API_KEY, "content-type": "application/x-www-form-urlencoded"}
response = requests.post(
"https://www.virustotal.com/api/v3/urls",
headers=headers,
data=f"url={TARGET_URL}"
)
analysis_id = response.json()["data"]["id"]
# Step 2: Poll for results
import time
time.sleep(30) # Wait for analysis to complete
headers = {"x-apikey": API_KEY}
result = requests.get(
f"https://www.virustotal.com/api/v3/analyses/{analysis_id}",
headers=headers
)
print(result.json()["data"]["attributes"]["stats"])
# Alternative: look up the URL directly
url_id = base64.urlsafe_b64encode(TARGET_URL.encode()).decode().rstrip("=")
report = requests.get(
f"https://www.virustotal.com/api/v3/urls/{url_id}",
headers=headers
)import requests
API_KEY = "your_api_key_here"
FILE_PATH = "/path/to/suspicious_file.exe"
# For files <= 32MB, use the direct upload endpoint
with open(FILE_PATH, "rb") as f:
response = requests.post(
"https://www.virustotal.com/api/v3/files",
headers={"x-apikey": API_KEY},
files={"file": (FILE_PATH, f)}
)
print(response.json()["data"]["id"]) # Analysis ID to poll later
# For files > 32MB, first get an upload URL
upload_url_resp = requests.get(
"https://www.virustotal.com/api/v3/files/upload_url",
headers={"x-apikey": API_KEY}
)
upload_url = upload_url_resp.json()["data"]
# Then POST the file to that URL insteadimport requests
API_KEY = "your_premium_api_key"
# Search for recently submitted PE files detected by 10+ engines
query = "type:peexe positives:10+ fs:2025-05-01+"
response = requests.get(
"https://www.virustotal.com/api/v3/intelligence/search",
headers={"x-apikey": API_KEY},
params={"query": query, "limit": 20}
)
for item in response.json()["data"]:
sha256 = item["id"]
detections = item["attributes"]["last_analysis_stats"]["malicious"]
print(f"{sha256}: {detections} detections")
# Paginate with cursor
next_link = response.json().get("links", {}).get("next")
if next_link:
next_page = requests.get(next_link, headers={"x-apikey": API_KEY})import requests
API_KEY = "your_premium_api_key"
SHA256 = "abc123..."
# Option A: Get download URL (time-limited)
resp = requests.get(
f"https://www.virustotal.com/api/v3/files/{SHA256}/download_url",
headers={"x-apikey": API_KEY}
)
download_url = resp.json()["data"]
# Option B: Direct download
resp = requests.get(
f"https://www.virustotal.com/api/v3/files/{SHA256}/download",
headers={"x-apikey": API_KEY}
)
with open(f"{SHA256}.bin", "wb") as f:
f.write(resp.content)import requests
API_KEY = "your_api_key"
SHA256 = "abc123..."
# Get summary of all behavior reports
resp = requests.get(
f"https://www.virustotal.com/api/v3/files/{SHA256}/behaviour_summary",
headers={"x-apikey": API_KEY}
)
summary = resp.json()["data"]
# Get individual sandbox reports
resp = requests.get(
f"https://www.virustotal.com/api/v3/files/{SHA256}/behaviours",
headers={"x-apikey": API_KEY}
)
for report in resp.json()["data"]:
sandbox = report["attributes"].get("sandbox_name", "unknown")
print(f"Sandbox: {sandbox}")For free-tier users (4 req/min, 500 req/day):
15-60 minutes depending on your use case.
time.sleep(15) betweenrequests to stay safely under 4/min.
or URL may already be in the database.
/analyses/{id}rather than repeatedly hitting the file/URL endpoint.
For enterprise users:
GET /users/{id}/api_usage for currentconsumption.
descriptors_only=true on search endpoints when you only need hashes.Objects — Everything in VT is an object with a type, id, attributes, and links. Object types include: file, url, domain, ip_address, comment, analysis, etc.
Relationships — Objects link to each other through relationships. For example, a file has relationships like contacted_domains, dropped_files, behaviours. Many relationship types are enterprise-only (🔒).
Collections — Ordered lists of objects returned by endpoints that produce multiple results. Collections support cursor-based pagination via the cursor parameter.
Descriptors — Lightweight references to objects (just type + id) used for efficient listing without fetching full attributes.
Read references/endpoints-free.md when you need:
Read references/endpoints-enterprise.md when you need:
Read references/objects-and-relationships.md when you need:
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.