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.
Previous

CI / GitHub Actions

Scan build artifacts from GitHub Actions or any CI runner.

Next