Skip to main content
Use VirusTotal through Corsair: one client, typed API calls. Copy your key from the VirusTotal account menu. It goes in the x-apikey header — not a bearer token — and every call is against https://www.virustotal.com/api/v3.
  • The key header is x-apikey. Without it the API answers AuthenticationRequiredError; with a wrong one, WrongCredentialsError. Both are 401, and both mean the credential needs fixing.
  • Two different 429s, and only one is retryable. TooManyRequestsError is a burst that clears, and the plugin retries it up to three times with backoff. QuotaExceededError means a minute, daily or monthly allowance is spent — retrying only burns another request against a quota that is already gone, so it has a separate QUOTA_ERROR handler with no retries.
  • NotAvailableYet is a 400, not a 404. A freshly submitted file or URL exists but has not been processed, so it answers 400 NotAvailableYet rather than 404 NotFoundError. The plugin keeps them apart: one means wait, the other means the indicator is unknown.
  • Scans answer an id, not a verdict. urls.scan and every rescan return an analysis descriptor; poll analyses.get until its status is completed. A queued analysis is a normal result, not a failure.
  • A URL is addressed by an identifier, not the raw URL. urls.get takes either the unpadded base64url form (RFC 4648 §3.2), which urlId() builds, or the SHA-256 id a URL report returns. The base64url form decodes back to the full URL, so a lookup sends VirusTotal the whole address, query string and any token in it included. Strip secrets before looking a URL up.
  • urls.scan is the one form-encoded route. It takes url= as a form field rather than a JSON body, which is why the plugin names the media type for that call.
  • Commenting and voting use a JSON:API envelope. The body is { data: { type: 'comment', attributes: { text } } } or { data: { type: 'vote', attributes: { verdict } } }, not a plain field map. A word starting with # in a comment becomes a tag.
  • A vote is harmless or malicious — there is no abstention, and no way to change a vote other than voting again.
  • Submitting an indicator publishes it. VirusTotal’s docs are explicit that anything scanned or queried through the API joins the public dataset, so urls.scan cannot be withdrawn — and the plugin cannot undo it either.
  • Paging is meta.cursor. Lists answer { data, meta: { cursor } } rather than a next link, and limit is capped at 40.
  • Reports are large and open. A file report carries over a hundred attributes and a per-engine verdict map; the plugin names the common ones and passes the rest through rather than pretending to model them all.
What you get:
  • 19 typed API operations

Setup

1

Install

2

Add the plugin

corsair.ts
With multiTenancy: true, scope calls with corsair.withTenant(id). See Quick start for KEK + Hub keys, and Multi-tenancy for account isolation.
3

Choose authentication

4

Connect a tenant

Mint a connect link and send the tenant to it. Hub hosts the page and delivers the result to your app — see Connect / OAuth.

Example API calls

Read a file report
Submit a URL for analysis
See the full list on the API page.

What’s next

API reference

Every virustotal.api.* operation with input and output types.

Connect / OAuth

createLink, Hub delivery, and tenant connect flows.

Use with agents

Expose this plugin’s operations as MCP tools.