> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.detectant.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.detectant.com/_mcp/server.

# Scan multiple files

POST https://api.detectant.com/v1/scans/batch
Content-Type: multipart/form-data

Uploads between 1 and 20 files in repeated `files` multipart parts. Each
file uses its account plan's 50 MB or 250 MB per-file limit, validation, content-type
analysis, malware detection, persistence, usage accounting, and billing path
as `createScan`. The encoded multipart request limit is the plan's
per-file limit × 20 plus 1 MiB for multipart framing.

Results preserve submitted-file order. File-level validation or scanner
failures are returned in the corresponding result and do not prevent
other files from being scanned. The whole request is rejected only for
request-level failures such as invalid multipart data, authentication or
rate-limit failure, more than 20 files, or an oversized total request.


Reference: https://docs.detectant.com/api-reference/scan-batch

## Authentication

- `X-API-Key` header (required) — API key in the `mak.<lookup-id>.<secret>` format.

## Request

### Body (multipart/form-data)

This endpoint expects a multipart form with multiple files.

- `files` (files, required) — Ordered file contents to scan. Each part must include a filename and uses the account plan's 50 MB or 250 MB direct-upload limit.

## Response

### 200

Ordered per-file scan results, including any partial failures.

- `results` (list of ScanBatchResult, required) — One outcome per submitted file, in submission order.

## Errors

### 400 Bad Request Error

The request parameters or upload are invalid.

- `error` (APIError, required)

### 401 Unauthorized Error

The API key is missing or invalid.

- `error` (APIError, required)

### 403 Forbidden Error

A dashboard session was supplied where an API key is required.

- `error` (APIError, required)

### 413 Content Too Large Error

The total encoded multipart request exceeds the account plan's batch limit.

- `error` (APIError, required)

### 429 Too Many Requests Error

The plan's current-period scan allowance, global request rate, or concurrent scanner capacity was exceeded. Plan exhaustion uses `USAGE_LIMIT_EXCEEDED` and includes the plan, limit, and reset time in `details`.

- `error` (APIError, required)

### 500 Internal Server Error

An unexpected internal failure occurred.

- `error` (APIError, required)

## Types

### ScanBatchResult

- `index` (integer, required) — Zero-based position of the submitted file.
- `filename` (string, required) — Filename supplied by the multipart part.
- `http_status` (integer, required) — HTTP status that the equivalent single-file request would return.
- `scan` (Scan, required, nullable) — Stored scan result, including its scan ID, or null when validation failed before a scan was created.
- `error` (APIError, required, nullable) — Per-file validation or scanning error, or null when the scan completed.

### APIError

- `code` (string, required) — Stable machine-readable error code.
- `message` (string, required) — Human-readable error message.
- `details` (map from string to string, required) — Context such as the ID of a failed stored scan. Empty when no context applies.

### Scan

- `id` (string, required) — Server-generated scan identifier.
- `created_at` (datetime, required)
- `status` (enum, required)
  - Allowed values: `completed`, `failed`
- `verdict` (enum, required)
  - Allowed values: `clean`, `malicious`, `suspicious`, `unknown`
- `filename` (string, required) — Filename supplied by the multipart upload.
- `sha256` (string, required)
- `size_bytes` (long, required)
- `duration_ms` (double, required)
- `timings` (ScanTimings, required)
- `failure` (ScanFailure, required, nullable) — Failure information, or `null` when the scan completed.
- `detections` (list of string, required) — Detection names reported for the file. Empty when no threats were detected.
- `type_analysis` (TypeAnalysis, required)
- `source` (ScanSource, required)

### ScanTimings

- `file_type_ms` (double, required)
- `malware_scan_ms` (double, required)

### ScanFailure

- `code` (enum, required)
  - Allowed values: `SCANNER_UNAVAILABLE`, `CONTENT_EXPANSION_LIMIT_EXCEEDED`
- `message` (string, required)

### TypeAnalysis

- `declared` (TypeDeclaration, required)
- `identified` (TypeIdentification, required)
- `status` (enum, required)
  - Allowed values: `match`, `compatible`, `mismatch`, `malformed`, `unscannable`, `unknown`
- `structurally_valid` (boolean, required, nullable)
- `confidence` (enum, required)
  - Allowed values: `high`, `medium`, `low`
- `reason` (TypeAnalysisReason, required, nullable)

### ScanSource

- `type` (enum, required)
  - Allowed values: `api`, `s3`
- `integration_id` (string, required, nullable) — S3 integration identifier, or `null` for direct API scans.
- `integration_name` (string, required, nullable) — S3 integration name, or `null` for direct API scans.

### TypeDeclaration

- `extension` (string, optional) — Raw extension from the uploaded filename, without the leading dot.
- `mime` (string, optional) — Normalized MIME type declared by the multipart part.

### TypeIdentification

- `format` (string, optional) — Canonical format identified from file content.
- `mime` (string, optional) — Canonical MIME type identified from file content.

### TypeAnalysisReason

- `code` (enum, required)
  - Allowed values: `malformed_file`, `encrypted_archive`, `unsupported_archive`, `content_expansion_limit_exceeded`, `file_type_mismatch`
- `claimed` (string, optional)
- `detected` (string, optional)

## Examples

**Request**

```json
{
  "files": []
}
```

**Response**

```json
{
  "results": [
    {
      "index": 0,
      "filename": "invoice.pdf",
      "http_status": 201,
      "scan": {
        "id": "scan_0123456789abcdef0123456789abcdef",
        "created_at": "2026-08-22T12:00:00Z",
        "status": "completed",
        "verdict": "clean",
        "filename": "invoice.pdf",
        "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
        "size_bytes": 24576,
        "duration_ms": 148.2,
        "timings": {
          "file_type_ms": 4.1,
          "malware_scan_ms": 102.7
        },
        "failure": null,
        "detections": [],
        "type_analysis": {
          "declared": {
            "extension": "pdf",
            "mime": "application/pdf"
          },
          "identified": {
            "format": "PDF",
            "mime": "application/pdf"
          },
          "status": "match",
          "structurally_valid": true,
          "confidence": "high",
          "reason": null
        },
        "source": {
          "type": "api",
          "integration_id": null,
          "integration_name": null
        }
      },
      "error": null
    },
    {
      "index": 1,
      "filename": "archive.zip",
      "http_status": 400,
      "scan": null,
      "error": {
        "code": "MALFORMED_FILE",
        "message": "The archive is malformed and could not be scanned.",
        "details": {}
      }
    }
  ]
}
```

**SDK Code**

```typescript
import { Detectant } from "@detectant-api/sdk";

const detectant = new Detectant({
  apiKey: process.env.DETECTANT_API_KEY,
});

const batch = await detectant.scanBatch([
  { path: "./invoice.pdf" },
  { path: "./archive.zip" },
]);

```

```php
<?php

use Detectant\DetectantClient;
use Detectant\Requests\CreateScanBatchRequest;
use Detectant\Utils\File;

$detectant = new DetectantClient(
    apiKey: getenv('DETECTANT_API_KEY') ?: null,
);

$batch = $detectant->scanBatch(new CreateScanBatchRequest([
    'files' => [
        File::createFromFilepath('./invoice.pdf'),
        File::createFromFilepath('./archive.zip'),
    ],
]));

```

```python
import requests

url = "https://api.detectant.com/v1/scans/batch"

payload = "-----011000010111000001101001--\r\n"
headers = {
    "X-API-Key": "<apiKey>",
    "Content-Type": "multipart/form-data; boundary=---011000010111000001101001"
}

response = requests.post(url, data=payload, headers=headers)

print(response.json())
```

```go
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://api.detectant.com/v1/scans/batch"

	payload := strings.NewReader("-----011000010111000001101001--\r\n")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("X-API-Key", "<apiKey>")
	req.Header.Add("Content-Type", "multipart/form-data; boundary=---011000010111000001101001")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://api.detectant.com/v1/scans/batch")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<apiKey>'
request["Content-Type"] = 'multipart/form-data; boundary=---011000010111000001101001'
request.body = "-----011000010111000001101001--\r\n"

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.detectant.com/v1/scans/batch")
  .header("X-API-Key", "<apiKey>")
  .header("Content-Type", "multipart/form-data; boundary=---011000010111000001101001")
  .body("-----011000010111000001101001--\r\n")
  .asString();
```

```csharp
using RestSharp;

var client = new RestClient("https://api.detectant.com/v1/scans/batch");
var request = new RestRequest(Method.POST);
request.AddHeader("X-API-Key", "<apiKey>");
request.AddHeader("Content-Type", "multipart/form-data; boundary=---011000010111000001101001");
request.AddParameter("multipart/form-data; boundary=---011000010111000001101001", "-----011000010111000001101001--\r\n", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = [
  "X-API-Key": "<apiKey>",
  "Content-Type": "multipart/form-data; boundary=---011000010111000001101001"
]
let parameters = []

let boundary = "---011000010111000001101001"

var body = ""
var error: NSError? = nil
for param in parameters {
  let paramName = param["name"]!
  body += "--\(boundary)\r\n"
  body += "Content-Disposition:form-data; name=\"\(paramName)\""
  if let filename = param["fileName"] {
    let contentType = param["content-type"]!
    let fileContent = String(contentsOfFile: filename, encoding: String.Encoding.utf8)
    if (error != nil) {
      print(error as Any)
    }
    body += "; filename=\"\(filename)\"\r\n"
    body += "Content-Type: \(contentType)\r\n\r\n"
    body += fileContent
  } else if let paramValue = param["value"] {
    body += "\r\n\r\n\(paramValue)"
  }
}

let request = NSMutableURLRequest(url: NSURL(string: "https://api.detectant.com/v1/scans/batch")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```