Language examples
Scan a file with cURL, JavaScript, TypeScript, Python, Go, PHP, Ruby, and Java.
All examples call the same endpoint:
POST https://api.trythreatless.com/api/v1/scans
Header: X-API-KEY
Multipart field: file
Replace YOUR_API_KEY and ./suspect.bin with your values. For a known-positive test, scan the official file from eicar.org as described in Test with EICAR — do not embed that payload in these snippets.
Success, detection, and errors
Clean:
clean
{
"filename": "suspect.bin",
"extension": "bin",
"sizeInBytes": 2048,
"scanDurationMillis": 42,
"infected": false,
"virusName": null
}
Malware detected:
infected
{
"filename": "payload.exe",
"extension": "exe",
"sizeInBytes": 4096,
"scanDurationMillis": 51,
"infected": true,
"virusName": "Win.Test.EICAR_HDB-1"
}
Oversize payload:
400
{
"error": "Scan failed",
"message": "File size exceeds limit: 10MB"
}
Missing key:
401
{
"error": "X-API-KEY MISSING"
}
Quota exhausted:
429
{
"error": "Rate limit exceeded",
"message": "Hourly scan limit reached",
"limit": 5,
"resetTime": 1710000000,
"retryAfter": 842
}
cURL
cURL
curl -sS -X POST https://api.trythreatless.com/api/v1/scans \
-H "X-API-KEY: YOUR_API_KEY" \
-F "file=@./suspect.bin"
Add -f if you want non-2xx statuses to fail a CI step. Infection still returns HTTP 200 — inspect infected in the JSON.
JavaScript / TypeScript (fetch)
TypeScript
const scanFile = async (file: Blob, apiKey: string) => {
const body = new FormData()
body.append('file', file)
const response = await fetch('https://api.trythreatless.com/api/v1/scans', {
method: 'POST',
headers: { 'X-API-KEY': apiKey },
body,
})
if (response.status === 429) {
const retryAfter = Number(response.headers.get('Retry-After') || 60)
throw new Error(`Quota exhausted. Retry in ${retryAfter}s`)
}
if (!response.ok) {
throw new Error(`Scan failed: ${response.status}`)
}
return await response.json() as {
filename: string
infected: boolean
virusName: string | null
scanDurationMillis: number
}
}
Node.js with the same fetch API:
JavaScript
import { readFile } from 'node:fs/promises'
const apiKey = process.env.THREATLESS_API_KEY
const bytes = await readFile('./suspect.bin')
const body = new FormData()
body.append('file', new Blob([bytes]), 'suspect.bin')
const response = await fetch('https://api.trythreatless.com/api/v1/scans', {
method: 'POST',
headers: { 'X-API-KEY': apiKey },
body,
})
const result = await response.json()
if (result.infected) {
console.error('Blocked', result.virusName)
process.exit(1)
}
Python
Python
import os
import sys
import requests
api_key = os.environ["THREATLESS_API_KEY"]
with open("suspect.bin", "rb") as handle:
response = requests.post(
"https://api.trythreatless.com/api/v1/scans",
headers={"X-API-KEY": api_key},
files={"file": handle},
timeout=60,
)
if response.status_code == 429:
retry_after = response.headers.get("Retry-After", "60")
sys.exit(f"Quota exhausted. Retry in {retry_after}s")
response.raise_for_status()
result = response.json()
if result.get("infected"):
sys.exit(f"Blocked: {result.get('virusName')}")
print(result)
Go
Go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
type ScanResult struct {
Filename string `json:"filename"`
Infected bool `json:"infected"`
VirusName *string `json:"virusName"`
}
func main() {
file, err := os.Open("suspect.bin")
if err != nil {
panic(err)
}
defer file.Close()
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", "suspect.bin")
if err != nil {
panic(err)
}
if _, err := io.Copy(part, file); err != nil {
panic(err)
}
contentType := writer.FormDataContentType()
writer.Close()
req, err := http.NewRequest(http.MethodPost, "https://api.trythreatless.com/api/v1/scans", body)
if err != nil {
panic(err)
}
req.Header.Set("X-API-KEY", os.Getenv("THREATLESS_API_KEY"))
req.Header.Set("Content-Type", contentType)
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
fmt.Fprintf(os.Stderr, "Quota exhausted. Retry-After=%s\n", resp.Header.Get("Retry-After"))
os.Exit(1)
}
if resp.StatusCode >= 300 {
io.Copy(os.Stderr, resp.Body)
os.Exit(1)
}
var result ScanResult
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
panic(err)
}
if result.Infected {
fmt.Fprintf(os.Stderr, "Blocked: %v\n", result.VirusName)
os.Exit(1)
}
fmt.Printf("clean: %s\n", result.Filename)
}
PHP
PHP
<?php
$apiKey = getenv('THREATLESS_API_KEY');
$path = __DIR__ . '/suspect.bin';
$handle = curl_init('https://api.trythreatless.com/api/v1/scans');
curl_setopt_array($handle, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-KEY: ' . $apiKey],
CURLOPT_POSTFIELDS => [
'file' => new CURLFile($path, mime_content_type($path) ?: 'application/octet-stream', basename($path)),
],
]);
$raw = curl_exec($handle);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);
if ($status === 429) {
fwrite(STDERR, "Quota exhausted\n");
exit(1);
}
$result = json_decode($raw, true);
if (!empty($result['infected'])) {
fwrite(STDERR, 'Blocked: ' . ($result['virusName'] ?? 'unknown') . PHP_EOL);
exit(1);
}
echo $raw, PHP_EOL;
Ruby
Ruby
require "json"
require "net/http"
require "uri"
uri = URI("https://api.trythreatless.com/api/v1/scans")
file_path = "suspect.bin"
boundary = "ThreatlessBoundary#{Time.now.to_i}"
file_data = File.binread(file_path)
body = +""
body << "--#{boundary}\r\n"
body << "Content-Disposition: form-data; name=\"file\"; filename=\"#{File.basename(file_path)}\"\r\n"
body << "Content-Type: application/octet-stream\r\n\r\n"
body << file_data
body << "\r\n--#{boundary}--\r\n"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request["X-API-KEY"] = ENV.fetch("THREATLESS_API_KEY")
request["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
request.body = body
response = http.request(request)
abort("Quota exhausted") if response.code == "429"
result = JSON.parse(response.body)
abort("Blocked: #{result["virusName"]}") if result["infected"]
puts result
Java
Java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
public class ScanExample {
public static void main(String[] args) throws Exception {
String apiKey = System.getenv("THREATLESS_API_KEY");
byte[] file = Files.readAllBytes(Path.of("suspect.bin"));
String boundary = "ThreatlessBoundary";
byte[] body = buildMultipart(boundary, "suspect.bin", file);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.trythreatless.com/api/v1/scans"))
.header("X-API-KEY", apiKey)
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(HttpRequest.BodyPublishers.ofByteArray(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 429) {
System.err.println("Quota exhausted");
System.exit(1);
}
System.out.println(response.body());
}
private static byte[] buildMultipart(String boundary, String filename, byte[] file) {
String header = "--" + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"file\"; filename=\"" + filename + "\"\r\n"
+ "Content-Type: application/octet-stream\r\n\r\n";
String footer = "\r\n--" + boundary + "--\r\n";
byte[] headerBytes = header.getBytes(java.nio.charset.StandardCharsets.UTF_8);
byte[] footerBytes = footer.getBytes(java.nio.charset.StandardCharsets.UTF_8);
byte[] body = new byte[headerBytes.length + file.length + footerBytes.length];
System.arraycopy(headerBytes, 0, body, 0, headerBytes.length);
System.arraycopy(file, 0, body, headerBytes.length, file.length);
System.arraycopy(footerBytes, 0, body, headerBytes.length + file.length, footerBytes.length);
return body;
}
}
URL scan (any language)
Send url instead of file:
cURL URL
curl -X POST https://api.trythreatless.com/api/v1/scans \
-H "X-API-KEY: YOUR_API_KEY" \
-F "url=https://example.com/file.exe"
See Scan a URL for SSRF rules.
Status codes and message strings are listed under Errors.