VirusTotal API Tutorial 2026: Automate IOC & Malware Analysis for SOC Analysts

·

VirusTotal aggregates 70+ anti-malware engines, multiple sandboxes, and a massive crowd-sourced corpus of files, URLs, domains and IPs. The web UI is fine for a one-off check, but a SOC that triages hundreds of indicators a day lives in the API. This guide takes you from a fresh API key to a production enrichment pipeline using the current v3 API.

You will install both official clients — vt-cli and vt-py — query every object type, build a Dockerised enrichment microservice, run bulk hunting with Retrohunt and Livehunt, and wire verdicts into Microsoft Sentinel and Splunk. Every command is copy-pasteable and uses non-sensitive demo indicators (the EICAR test file, example.com, 8.8.8.8) so you can follow along on the free tier.

◈ Table of Contents

01 What the VirusTotal API Is 02 Prerequisites & API Key Tiers 03 Method 1 — vt-cli 04 Method 2 — vt-py (Python) 05 Method 3 — REST & Docker 06 First Lookups 07 Scan, Poll & Pivot 08 Intelligence, Retrohunt & Livehunt 09 Automation & SIEM Integration 10 Quotas, OPSEC & Troubleshooting 11 Sources & References
🔍

01 — WHAT THE VIRUSTOTAL API IS & WHY A DEFENDER CARES

BEGINNER
01
One Request, Seventy Opinions
Foundation

VirusTotal (VT) is a Google/Chronicle-owned threat-intelligence service. When you submit a file hash, URL, domain or IP, VT returns the combined verdict of 70+ anti-malware engines plus sandbox behaviour, static properties, and a web of relationships — the other domains a file contacted, the files that dropped it, the IPs a domain resolved to. For a SOC analyst, that turns a lone, context-free indicator into an attributed story.

The modern interface is the v3 REST API. Every request is plain HTTPS with a single authentication header, x-apikey, and every response is a JSON object — a file, url, domain, ip_address or analysis. Learn the object model once and every endpoint behaves the same way. The legacy v2 API still answers but is deprecated; write all new tooling against v3.

⚡

Alert Enrichment

Auto-attach a VT verdict to every file hash, URL and IP in a SIEM alert so analysts triage with context instead of a bare indicator.

🧠

IOC Validation

Confirm whether an indicator from a threat feed is still live, widely detected, or a false positive before you block it fleet-wide.

🔗

Pivoting

Follow relationships from one dropper to its C2 domains, sibling samples and campaign infrastructure without ever leaving the terminal.

🔎

Retro & Live Hunting

Run a YARA rule across the historic corpus (Retrohunt) or against every new submission going forward (Livehunt) to catch your threat early.

📊

Bulk Triage

Score a CSV of hundreds of quarantined hashes overnight and surface only the ones worth an analyst's attention.

📡

Surface Monitoring

Watch for your own files, domains or brand appearing in the public corpus — an early signal of a leak or impersonation campaign.

VT reputation is an input, not a verdict. A clean VT result does not prove a file is safe — targeted malware is often unknown to VT. Treat "0/72 detections" as "no community evidence yet", never as "benign".

02
The v3 Object Model
Concept

Every v3 endpoint returns an object with a stable shape: a type, an id, a bag of attributes, and a set of relationships you can expand. Once you can read one object, you can read them all.

ObjectEndpoint (GET)ID is…
file/api/v3/files/{id}MD5, SHA-1 or SHA-256
url/api/v3/urls/{id}base64url(url) without padding, or SHA-256 of the URL
domain/api/v3/domains/{id}the domain name
ip_address/api/v3/ip_addresses/{id}the IPv4/IPv6 address
analysis/api/v3/analyses/{id}ID returned when you submit a file/URL

The single most useful attribute on a file, domain or ip_address object is last_analysis_stats — the counts of malicious, suspicious, undetected, harmless and timeout engines. That ratio (for example 52/72 malicious) is the number your automation keys off.

🔑

02 — PREREQUISITES & API KEY TIERS

BEGINNER
01
Get Your API Key
Setup

Create a free account at virustotal.com, then open your profile menu and choose API key. The key is a 64-character hex string tied to your account. Everything in this guide works on the free (Public) key; the premium tiers remove the rate limits and unlock Intelligence search, Retrohunt and Livehunt.

🔗
Where to find your key
virustotal.com → avatar menu → API key
OPEN →

Store the key in an environment variable so it never lands in your shell history or a committed script. On Linux/macOS:

EXPORT THE KEY (BASH / ZSH)
# add to ~/.bashrc or ~/.zshrc so it persists export VT_API_KEY="paste_your_64_char_key_here" # vt-cli reads this variable name specifically: export VTCLI_APIKEY="$VT_API_KEY"
EXPORT THE KEY (POWERSHELL)
# current session only $env:VT_API_KEY = "paste_your_64_char_key_here" # persist for the user setx VT_API_KEY "paste_your_64_char_key_here"

Never hardcode the key in source, notebooks or container images, and never commit ~/.vt.toml. A leaked VT key can be abused against your quota and, on premium, exposes your Intelligence searches. Rotate it from your profile the moment it appears in any shared location.

02
Free vs Premium — Know Your Limits
Quota

The free Public API is rate-limited and non-commercial. Design your tooling around the ceiling from day one — it is the single most common thing that breaks a VT script in production.

CapabilityPublic (Free)Premium / Enterprise
Request rate4 requests / minuteContract-defined, far higher
Daily quota500 requests / dayContract-defined
Lookups (hash/url/domain/ip)YesYes
Upload & scan filesYesYes
Intelligence searchNoYes
RetrohuntNoYes
Livehunt (YARA)NoYes
Commercial useProhibitedPermitted under contract

The free API is for research and non-commercial use only, and may not be built into a product or a workflow that does not contribute new files. Using a free key in a commercial SOC pipeline violates VT's terms — use a licensed Premium key there.

At 4 req/min a naive loop over 500 hashes takes two-plus hours and then dies at the daily cap. Batch, cache verdicts locally, and sleep 15 s between calls. The automation section builds this in.

03
Toolbox & Versions
Prereqs

Pick whichever client matches your workflow. You do not need all three — but seeing the same lookup in CLI, Python and raw REST makes the object model click.

ComponentVersionWhy
vt-clilatest release recommendedFast one-liners & shell pipelines
Python3.8+ minRuns the vt-py library
vt-pylatest (pip)Programmatic enrichment, async scanning
curl / jqany recentRaw REST testing & JSON parsing
Docker + Compose24+ / v2Containerised enrichment microservice
💾

03 — METHOD 1: THE vt-cli COMMAND LINE

BEGINNER
01
Install vt-cli on Any Platform
Install

vt-cli is VirusTotal's official Go-based command-line client. It is a single static binary with no runtime dependencies, which makes it ideal for analyst workstations and CI runners alike.

macOS — HOMEBREW
$ brew install virustotal-cli $ vt version
LINUX / WINDOWS — PRE-COMPILED BINARY
# download the archive for your OS/arch from the releases page: # https://github.com/VirusTotal/vt-cli/releases $ curl -LO https://github.com/VirusTotal/vt-cli/releases/latest/download/Linux64.zip $ unzip Linux64.zip -d vt-cli $ sudo install vt-cli/vt /usr/local/bin/vt $ vt version
BUILD FROM SOURCE — REQUIRES GO 1.14+
$ git clone https://github.com/VirusTotal/vt-cli $ cd vt-cli && make install # ensure the Go bin dir is on PATH $ export GOBIN=$(go env GOPATH)/bin && export PATH=$PATH:$GOBIN

The exact release asset name (Linux64.zip, macOS64.zip, windows64.zip) changes between versions. Always copy the link for the latest release from the GitHub releases page rather than assuming a filename.

02
Authenticate the CLI
Config

Either run the interactive initialiser — which writes the key to ~/.vt.toml — or rely on the VTCLI_APIKEY environment variable you set earlier. The --apikey flag overrides both for one-off calls.

INTERACTIVE INIT
$ vt init # paste your key when prompted; saved to ~/.vt.toml (chmod 600 it) $ chmod 600 ~/.vt.toml
VERIFY IT WORKS
# the Google public DNS IP is a safe, always-present object $ vt ip 8.8.8.8

Add vt completion to your shell with vt completion bash (or zsh/fish). Tab-completing object types and flags saves a surprising amount of friction during live triage.

03
Shape the Output
Usage

Full objects are huge. Use --include to pull only the attributes you need and --format json when you intend to pipe into jq. This also conserves nothing on quota (one lookup is one lookup) but it keeps your terminal readable.

PROJECT ONLY THE VERDICT FIELDS
$ vt file 44d88612fea8a8f36de82e1278abb02f \ --include=last_analysis_stats,meaningful_name,type_description
JSON OUT → jq FOR THE MALICIOUS COUNT
$ vt file 44d88612fea8a8f36de82e1278abb02f --format json \ | jq '.[0].attributes.last_analysis_stats'
🐍

04 — METHOD 2: THE vt-py PYTHON LIBRARY

INTERMEDIATE
01
Install vt-py in a Virtual Environment
Install

vt-py is VirusTotal's official Python client. Always install it into a project virtual environment so its dependency pins never collide with your system Python.

CREATE A VENV AND INSTALL
$ python3 -m venv .venv $ source .venv/bin/activate # Windows: .venv\Scripts\activate (.venv)$ pip install vt-py (.venv)$ python -c "import vt; print('vt-py ready')"
02
Your First Report in Python
Code

The client reads objects with get_object() using the same path you would hit over REST. Read the key off the environment — never inline it.

file_lookup.py
import os, vt client = vt.Client(os.environ["VT_API_KEY"]) try: # EICAR test file MD5 — safe, always present in the corpus f = client.get_object("/files/44d88612fea8a8f36de82e1278abb02f") print("name :", f.meaningful_name) print("type :", f.type_description) print("size :", f.size, "bytes") print("stats :", f.last_analysis_stats) finally: client.close()
RUN IT
(.venv)$ python file_lookup.py # stats: {'malicious': 60+, 'undetected': .., 'harmless': 0, ...}

Accessing an attribute that is absent raises AttributeError. Use f.get("reputation") for fields that may not exist (new or rarely-seen objects) instead of dotted access.

03
Async for Throughput
Code

On premium quota you will want concurrency. vt.Client doubles as an async context manager, exposing *_async() coroutines you can gather. (On the free tier the 4/min limit dominates, so keep it serial there.)

async_lookup.py
import os, asyncio, vt async def main(hashes): async with vt.Client(os.environ["VT_API_KEY"]) as client: async def one(h): obj = await client.get_object_async(f"/files/{h}") return h, obj.last_analysis_stats["malicious"] return await asyncio.gather(*[one(h) for h in hashes]) if __name__ == "__main__": out = asyncio.run(main(["44d88612fea8a8f36de82e1278abb02f"])) print(out)
🌐

05 — METHOD 3: RAW REST & A DOCKERISED SERVICE

INTERMEDIATE
01
Talk to v3 with curl
REST

Any HTTP client works — the only requirement is the x-apikey header. This is what you reach for when debugging, or when integrating from a language without an official SDK.

GET A FILE REPORT
$ curl --request GET \ --url https://www.virustotal.com/api/v3/files/44d88612fea8a8f36de82e1278abb02f \ --header "x-apikey: $VT_API_KEY" | jq '.data.attributes.last_analysis_stats'
LOOK UP A DOMAIN AND AN IP
$ curl -s https://www.virustotal.com/api/v3/domains/example.com \ -H "x-apikey: $VT_API_KEY" | jq '.data.attributes.last_analysis_stats' $ curl -s https://www.virustotal.com/api/v3/ip_addresses/8.8.8.8 \ -H "x-apikey: $VT_API_KEY" | jq '.data.attributes.as_owner'
LOOK UP A URL (ID IS base64url WITHOUT PADDING)
$ URLID=$(printf "http://example.com/" | base64 | tr '+/' '-_' | tr -d '=') $ curl -s https://www.virustotal.com/api/v3/urls/$URLID \ -H "x-apikey: $VT_API_KEY" | jq '.data.attributes.last_analysis_stats'
02
Wrap It in a Container
Docker

A small enrichment microservice is the cleanest way to share VT access across a team without handing the key to everyone. Here is a complete Flask + vt-py service with a docker-compose.yml that keeps the key in an env file and caps the container.

app.py
import os, vt from flask import Flask, jsonify app = Flask(__name__) APIKEY = os.environ["VT_API_KEY"] @app.get("/enrich/file/<h>") def enrich_file(h): client = vt.Client(APIKEY) try: o = client.get_object(f"/files/{h}") s = o.last_analysis_stats verdict = "malicious" if s["malicious"] >= 3 else "clean" return jsonify(hash=h, stats=s, verdict=verdict) except vt.APIError as e: return jsonify(error=str(e)), 404 finally: client.close()
requirements.txt
vt-py flask gunicorn
Dockerfile
FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app.py . USER nobody EXPOSE 8080 CMD ["gunicorn","-b","0.0.0.0:8080","-w","2","app:app"]
docker-compose.yml
services: vt-enrich: build: . image: cyberhawk/vt-enrich:1.0 container_name: vt-enrich env_file: .env # contains VT_API_KEY=... ports: - "127.0.0.1:8080:8080" # bind to loopback only read_only: true cap_drop: [ALL] security_opt: ["no-new-privileges:true"] restart: unless-stopped deploy: resources: limits: cpus: "0.50" memory: 256M
BUILD, RUN AND TEST
$ echo "VT_API_KEY=$VT_API_KEY" > .env && chmod 600 .env $ docker compose up --build -d $ curl -s http://127.0.0.1:8080/enrich/file/44d88612fea8a8f36de82e1278abb02f | jq

Bind the service to 127.0.0.1 (or put it behind an authenticated reverse proxy) and never expose it to the internet. An open enrichment endpoint lets anyone spend your VT quota and, on premium, run Intelligence queries on your dime.

You are ready to build when
  • vt version, python -c "import vt", and a curl file lookup all succeed
  • VT_API_KEY resolves from the environment, not a hardcoded string
  • ~/.vt.toml and .env are chmod 600 and git-ignored
  • the containerised endpoint returns a verdict for the EICAR hash
🔍

06 — FIRST LOOKUPS: FILES, URLS, DOMAINS & IPS

BEGINNER
01
Read a File Verdict Properly
Triage

The number everyone quotes — "52/72" — is last_analysis_stats.malicious over the total engines. But a serious triage reads more: first/last submission dates tell you whether this is a fresh sample or a years-old commodity, and reputation plus community votes flag known false positives.

AttributeWhat it tells you
last_analysis_statsmalicious / suspicious / undetected / harmless engine counts
meaningful_namethe most representative filename seen in the wild
type_descriptionfile type (PE32 executable, PDF, Office Open XML…)
first_submission_dateepoch of first time VT ever saw this file
times_submittedhow widespread/commodity the sample is
reputationcommunity score; strongly negative = widely flagged
signature_infoAuthenticode signer, if the binary is signed
CLI — THE FIELDS THAT MATTER
$ vt file 44d88612fea8a8f36de82e1278abb02f \ --include=last_analysis_stats,first_submission_date,times_submitted,reputation

One or two "malicious" engines on a widely-submitted, old, signed file is usually a false positive. Three-plus independent, reputable engines on a rarely-seen, recently-submitted binary is the pattern that warrants escalation.

02
Domains & IPs — Infrastructure Context
Triage

For network indicators, the engine verdict matters less than the context: who owns the ASN, when the domain was registered (a day-old domain is a red flag), the current resolutions, and the HTTPS certificate. VT surfaces all of it on the object.

DOMAIN CONTEXT WITH jq
$ vt domain example.com --format json | jq '.[0].attributes | {stats:.last_analysis_stats, created:.creation_date, registrar:.registrar}'
IP CONTEXT
$ vt ip 8.8.8.8 --include=as_owner,country,last_analysis_stats
03
Submit a URL for Scanning
Triage

If a URL has never been seen, submit it for analysis. VT returns an analysis ID you poll until it is complete, then read the resulting URL object. vt-py wraps the whole dance in one call.

vt-py — SCAN A URL AND WAIT
import os, vt client = vt.Client(os.environ["VT_API_KEY"]) analysis = client.scan_url("http://example.com/", wait_for_completion=True) print(analysis.stats) # {'malicious': 0, 'harmless': .., ...} client.close()
CLI EQUIVALENT
$ vt url http://example.com/
⚙️

07 — CORE WORKFLOWS: SCAN, POLL & PIVOT

INTERMEDIATE
01
Upload & Detonate a File
Scan

To analyse a file VT has never seen, upload it. vt-py streams the file and, with wait_for_completion=True, blocks until the multi-engine scan and sandbox run finish. Files over 32 MB need a special upload URL, which vt-py negotiates for you.

vt-py — UPLOAD AND READ THE VERDICT
import os, vt client = vt.Client(os.environ["VT_API_KEY"]) with open("/tmp/suspware.bin", "rb") as f: analysis = client.scan_file(f, wait_for_completion=True) print(analysis.stats) client.close()
CLI — SCAN A LOCAL FILE
$ vt scan file /tmp/suspware.bin # prints an analysis id; follow it with: $ vt analysis <analysis_id>

Uploading a file makes it available to VT and, on the premium corpus, to other researchers. Never upload documents containing secrets, PII, or anything that identifies your organisation — a leaked internal file in VT is a disclosure incident. For sensitive samples, look up the hash first; only upload if you accept it going public.

02
Poll an Analysis to Completion
Scan

When you do not want to block, submit and poll. The analysis object carries a status of queued, in-progress or completed. Respect the rate limit while polling.

REST — POLL THE ANALYSIS
$ curl -s https://www.virustotal.com/api/v3/analyses/<id> \ -H "x-apikey: $VT_API_KEY" | jq '.data.attributes.status'
03
Pivot Through Relationships
Pivot

Relationships are what make VT an investigation tool rather than a lookup table. From a single dropper you can walk to the domains it contacted, the files it wrote, and the parent that delivered it — building out the campaign without touching the endpoint again.

ObjectUseful relationshipReveals
filecontacted_domains / contacted_ipsC2 and callback infrastructure
filedropped_files / execution_parentspayload chain & delivery vector
domainresolutions / communicating_fileshosting IPs & malware using the domain
ip_addresscommunicating_files / urlseverything that talked to the host
CLI — WALK A RELATIONSHIP
# every domain a given file contacted $ vt file <sha256> relationship contacted_domains # every file seen communicating with a domain $ vt domain badsite.example relationship communicating_files
vt-py — ITERATE A RELATIONSHIP
for d in client.iterator(f"/files/{sha256}/contacted_domains", limit=20): print(d.id)

Over REST you can expand relationships inline with ?relationships=contacted_domains,dropped_files on the file endpoint — one request instead of three, which matters on the 4/min free limit.

🔮

08 — ADVANCED: INTELLIGENCE, RETROHUNT & LIVEHUNT

ADVANCED
01
VT Intelligence Search
Premium

Intelligence search (premium) lets you query the entire corpus with modifiers — file type, detection count, sandbox behaviour, import hash, submitter geography and dozens more. It is the difference between "tell me about this hash" and "show me every PE in the last week that beacons and is signed by this cert".

CLI — SEARCH FOR A FAMILY, FRESHLY SEEN
$ vt search 'type:peexe tag:signed engines:agenttesla fs:2026-09-25+'
vt-py — ITERATE SEARCH RESULTS
for f in client.iterator("/intelligence/search", params={"query": "behaviour:'powershell -enc' p:5+"}, limit=50): print(f.sha256, f.last_analysis_stats["malicious"])

Build searches from the results of an incident: take the imphash or a distinctive mutex from one confirmed sample and search the corpus for siblings. That is how you turn one detection into a family-wide block list.

02
Retrohunt — Hunt the Past
Premium

Retrohunt runs a YARA rule across VT's historic corpus (a rolling window of recent submissions). Write a rule that captures your threat's unique bytes, launch the job, and collect every matching sample VT has ever seen.

campaign.yar
rule cyberhawk_demo_loader { meta: author = "CyberHawk" desc = "demo retrohunt rule - unique marker" strings: $a = "X-Hawk-Loader/1.0" ascii $b = { 6a 40 68 00 30 00 00 } condition: uint16(0) == 0x5a4d and any of them }
LAUNCH & COLLECT
$ vt retrohunt start campaign.yar $ vt retrohunt list $ vt retrohunt matches <job_id>
03
Livehunt — Hunt the Future
Premium

Livehunt keeps a YARA ruleset running against every new submission. When something matches — a new variant of your tracked family, a document abusing your brand — VT notifies you. It is the closest thing to a standing tripwire across the global malware stream.

MANAGE LIVEHUNT RULESETS
$ vt hunting ruleset add cyberhawk-loader campaign.yar $ vt hunting ruleset list # recent matches land as notifications: $ vt hunting notification list

A sloppy YARA rule (a single common string, no header anchor) will match thousands of files and bury real hits in noise — and burn your notification quota. Anchor on the file type, require multiple distinctive strings, and test with Retrohunt before promoting a rule to Livehunt.

🤖

09 — AUTOMATION & SIEM INTEGRATION

ADVANCED
01
Bulk Hash Enrichment with Caching
Script

The real-world task: take a CSV of hashes from quarantine or a hunt, enrich each against VT, and write a verdict column — without blowing the 4/min / 500-day free quota. The pattern is: cache locally, skip anything already known, sleep between calls, and handle the 404 for hashes VT has never seen.

enrich_hashes.py
import os, csv, json, time, vt CACHE = "vt_cache.json" cache = json.load(open(CACHE)) if os.path.exists(CACHE) else {} client = vt.Client(os.environ["VT_API_KEY"]) def verdict(h): if h in cache: return cache[h] # never spend quota twice try: o = client.get_object(f"/files/{h}") r = {"malicious": o.last_analysis_stats["malicious"], "name": o.get("meaningful_name", "")} except vt.APIError as e: r = {"malicious": None, "name": "not_found" if e.code == "NotFoundError" else str(e.code)} cache[h] = r json.dump(cache, open(CACHE, "w")) time.sleep(16) # stay under 4 req/min on free tier return r with open("hashes.csv") as fi, open("enriched.csv", "w", newline="") as fo: w = csv.writer(fo); w.writerow(["sha256", "malicious", "name"]) for (h,) in csv.reader(fi): v = verdict(h.strip()); w.writerow([h, v["malicious"], v["name"]]) client.close()

Persist the cache to a small SQLite table or Redis in a real SOC. Most hashes you see are repeats; a warm cache turns a 500-call day into a handful of genuinely new lookups.

02
Surface Hashes to Enrich — KQL & SPL
SIEM

Before you call VT, you need the list of indicators worth spending quota on. These queries pull distinct, recent file hashes from your telemetry so your enrichment job only touches what is new. Run the query, feed the output into enrich_hashes.py, and write the verdict back as a watchlist or lookup.

DETECTS: distinct SHA-256 hashes of files written/executed in the last 24h (Microsoft Defender / Sentinel) — the candidate set for VT enrichment.
KQL — MICROSOFT SENTINEL / DEFENDER XDR
DeviceFileEvents | where Timestamp > ago(24h) | where isnotempty(SHA256) | where ActionType in ("FileCreated", "FileModified") | summarize hosts = dcount(DeviceId), firstSeen = min(Timestamp) by SHA256, FileName | order by hosts desc
DETECTS: the same candidate hash set from Sysmon process-creation events in Splunk.
SPL — SPLUNK (SYSMON)
index=sysmon EventCode=1 earliest=-24h | eval sha256=lower(replace(Hashes,".*SHA256=([0-9A-Fa-f]+).*","\1")) | where sha256!="" | stats dc(ComputerName) as hosts min(_time) as firstSeen by sha256 Image | sort - hosts

In Sentinel you do not have to script the write-back: the built-in VirusTotal Logic App connector enriches entities inside an automation playbook. In Splunk, the VirusTotal app on Splunkbase adds a | virustotal search command. The scripted path above is the portable, no-license-required option.

03
Wire It Into Wazuh / Scheduled Jobs
Integrate

Wazuh ships a native VirusTotal integration that checks file hashes from its File Integrity Monitoring against VT and raises an alert on a hit. For everything else, a cron-driven enrichment job keeps your indicator store warm.

WAZUH — ossec.conf INTEGRATION BLOCK
<integration> <name>virustotal</name> <api_key>YOUR_VT_API_KEY</api_key> <group>syscheck</group> <alert_format>json</alert_format> </integration>
CRON — NIGHTLY ENRICHMENT AT 02:00
$ crontab -e # m h dom mon dow command 0 2 * * * cd /opt/vt && /opt/vt/.venv/bin/python enrich_hashes.py >> /var/log/vt-enrich.log 2>&1
A healthy enrichment pipeline
  • caches every verdict and never re-queries a known hash
  • sleeps to stay under the per-minute and daily quota
  • treats NotFoundError as "unknown", not "clean"
  • writes verdicts back where analysts already look (SIEM watchlist / lookup)
  • uses a licensed Premium key if the SOC is a commercial operation
🛡️

10 — QUOTAS, OPSEC & TROUBLESHOOTING

REFERENCE
01
Common HTTP Errors & What They Mean
Debug

v3 returns a JSON error body with a machine-readable code alongside the HTTP status. Handle these explicitly — especially the difference between 429 (slow down) and a 404 (VT has never seen this indicator).

StatuscodeMeaning & fix
401AuthenticationRequiredErrorMissing/invalid x-apikey header. Check the env var is exported.
403ForbiddenErrorYour tier lacks this endpoint (e.g. Intelligence on a free key).
404NotFoundErrorObject not in corpus. This is "unknown", not "clean". Consider uploading.
429QuotaExceededErrorRate or daily quota hit. Back off; no queue exists — the request is dropped.
400BadRequestErrorMalformed ID (e.g. bad URL base64) or query syntax.
BACK OFF CLEANLY ON 429 (vt-py)
import time, vt try: o = client.get_object(f"/files/{h}") except vt.APIError as e: if e.code == "QuotaExceededError": time.sleep(60) # wait out the per-minute window and retry
02
Troubleshooting Table
Debug
SymptomLikely causeFix
vt: command not foundBinary not on PATHMove vt to /usr/local/bin or add GOBIN to PATH
401 on every callKey not exported / typoecho $VT_API_KEY; re-run vt init
Script stalls then 429No inter-call delaySleep ≥15 s; cache; batch under 500/day
URL lookup 400Wrong URL ID encodingbase64url, strip = padding, or submit to get ID
ModuleNotFoundError: vtvenv not activatedsource .venv/bin/activate before running
Verdict looks staleCached last_analysisRe-submit to force a fresh multi-engine scan
03
OPSEC & Legal Guardrails
Safety

The VT API is a force multiplier, but the two mistakes that actually hurt organisations are uploading things they should not and treating a clean result as proof of safety. Internalise these before you point automation at it.

Hash first, upload second. A hash lookup is passive and leaks nothing. An upload publishes the file. Searching VT for a known-bad hash also signals to a premium-subscriber adversary that someone is investigating it — operationally relevant during an active intrusion.

Only enrich indicators you are authorised to investigate, and use a licensed Premium key for any commercial or production SOC workflow. The free key is research-only; building it into a paid service breaches VT's terms.

📚

11 — SOURCES & REFERENCES

REFERENCE

◈ Put VirusTotal to Work

This guide is defensive tooling for authorised use — enrich only indicators you are permitted to investigate, and never upload sensitive files. Pair VT enrichment with CyberHawk's IOC Scanner to triage indicators fast, and browse more hands-on SOC tooling on the CyberHawk blog.

Want the detection and response side? Our SOP library covers IOC-match and malware triage playbooks that consume exactly this kind of enrichment.

◈ Stay Connected

Follow CyberHawk Threat Intel for threat intelligence, deployment guides and hands-on SOC tooling content.

🌐 Website ▶️ YouTube ▶️ YouTube (2) 𝕏 Twitter / X ♪ TikTok ✈️ Telegram
🔍 IOC Scanner 🛠️ Live Tools 📚 Courses 🚨 Threat Intel 📝 Blog 📋 SOPs

"They can't exploit you if you are the Exploit."