Back to home

API DOCS

Everything you need to integrate Bouncer into your application.

Base URLhttps://api.usebouncer.dev

Quickstart

Scan a file in 3 steps: get an API key from the dashboard, upload a file, and poll for the result.

Upload a filebash
curl -X POST https://api.usebouncer.dev/v1/scan \
  -H "X-API-Key: bnc_live_your_key_here" \
  -F "file=@document.pdf"
Response: 202 Acceptedjson
{
  "status": "success",
  "data": {
    "scan_id": "scan_01H8MZ...",
    "status": "pending",
    "file": { "name": "document.pdf", "size": 245632 },
    "created_at": "2026-03-22T12:00:00Z"
  }
}

Authentication

All scan endpoints require an API key passed via the X-API-Key header. Create keys from the dashboard.

curl https://api.usebouncer.dev/v1/scan/usage \
  -H "X-API-Key: bnc_live_your_key_here"

API keys use the bnc_live_ prefix. Keys are hashed (SHA256) before storage. The full key is only shown once at creation.

Scan a File

POST/v1/scan

Upload a file as multipart/form-data. The scan runs asynchronously. You get back a scan_id immediately.

Request

Uploadbash
curl -X POST https://api.usebouncer.dev/v1/scan \
  -H "X-API-Key: bnc_live_your_key_here" \
  -F "file=@suspicious_file.exe"

Response: 202

{
  "status": "success",
  "data": {
    "scan_id": "scan_01H8MZ...",
    "status": "pending",
    "file": { "name": "suspicious_file.exe", "size": 1048576 },
    "poll_url": "/v1/scan/scan_01H8MZ...",
    "created_at": "2026-03-22T12:00:00Z"
  }
}

File size limits

TierMax file sizeEndpoint
Any tier~4.5 MBPOST /v1/scan
Free25 MBPOST /v2/scan
Pro100 MBPOST /v2/scan

Uploading more than about 4.5 MB requires POST /v2/scan. The endpoint above sends the file through our API, and that request is capped at roughly 4.5 MB regardless of your tier. Anything larger is rejected before it reaches Bouncer, with a bare 413 Request Too Long rather than our usual error body. v2 hands you a direct upload target instead, which removes that limit.

Large files (v2)

POST/v2/scan

Use v2 when a file is larger than about 4.5 MB. Instead of sending the file to us, you ask for an upload target and send the bytes straight to storage. Scanning starts by itself once the upload finishes, so there is no third call.

On Node, the SDK picks between v1 and v2 for you, so you can skip this section entirely. The steps below are for calling the API directly.

1. Ask for an upload target

Create the scanbash
curl -X POST https://api.usebouncer.dev/v2/scan \
  -H "X-API-Key: bnc_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"file_name": "report.pdf", "mime_type": "application/pdf"}'
Response: 201 Createdjson
{
  "status": "success",
  "data": {
    "scan_id": "scan_01H8MZ...",
    "status": "pending",
    "upload": {
      "url": "https://bouncer-dev-uploads-....s3.amazonaws.com/",
      "fields": { "key": "uploads/scan_01H8MZ.../report.pdf", "...": "..." }
    },
    "max_file_size": 26214400,
    "poll_url": "/v2/scan/scan_01H8MZ...",
    "created_at": "2026-08-10T12:00:00Z"
  }
}

No size is sent, and none is trusted. Your tier limit is enforced by the upload target itself.

2. Upload the file

Send every key/value in upload.fields as form fields, then the file last. A successful upload returns 204 with an empty body.

Upload direct to storagebash
curl -X POST "$UPLOAD_URL" \
  -F key="uploads/scan_01H8MZ.../report.pdf" \
  -F policy="..." -F x-amz-signature="..." \
  -F file=@report.pdf

3. Read the result

Poll GET /v2/scan/:scan_id or register a webhook. The read endpoints are the same as v1, so a v2 client never has to switch back.

Worth knowing

  • The upload target expires after 15 minutes. Request it just before uploading.
  • Requesting it uses one scan from your monthly quota, whether or not the file is uploaded, and counts against the month you requested it in. A scan that is never uploaded is removed after at least 24 hours; polling it then returns 404 scan_not_found.
  • Treat upload.fields as a credential. Do not log it, cache it, or pass it on.
  • A file over your tier limit is refused by storage with an EntityTooLarge XML error, not our JSON error body.
  • Re-uploading to the same target will not re-scan or change a verdict already reported.
  • v2 is for server-side callers. Browser uploads are not supported yet.
  • POST /v1/scan is unchanged and remains the simpler option under 4.5 MB, since it is a single request.

Poll Results

GET/v1/scan/:scan_id

Poll until status iscompleted orfailed. Recommended polling interval: 2 seconds.

Response: completed scan

{
  "status": "success",
  "data": {
    "scan_id": "scan_01H8MZ...",
    "status": "completed",
    "verdict": "malicious",
    "file": {
      "name": "suspicious_file.exe",
      "size": 1048576,
      "mime_type": "application/x-msdownload",
      "sha256": "a1b2c3d4..."
    },
    "threats": [
      {
        "engine": "signature",
        "name": "Win.Trojan.Agent-123456",
        "severity": "high"
      },
      {
        "engine": "pattern",
        "rule": "suspicious_pe_packer",
        "severity": "medium"
      }
    ],
    "engines": {
      "signature": { "result": "malicious", "details": "Win.Trojan.Agent-123456" },
      "pattern": { "result": "suspicious", "matched_rules": ["suspicious_pe_packer"] }
    },
    "scanned_at": "2026-03-22T12:00:05Z",
    "duration_ms": 4800
  }
}

Scan statuses

StatusDescription
pendingWaiting for scan to start
scanningScan in progress
completedScan finished, check verdict
failedScan engine error

Verdicts

VerdictDescription
cleanNo threats detected
suspiciousPattern matches but not confirmed malware
maliciousConfirmed malware detected

Scan History

GET/v1/scan

List all scans for the authenticated API key. Supports cursor-based pagination.

Query parameters

ParamTypeDescription
limitnumberMax results (default 20, max 100)
cursorstringPagination cursor from previous response

Response

{
  "status": "success",
  "data": [
    { "scan_id": "scan_01...", "status": "completed", "verdict": "clean", ... }
  ],
  "meta": {
    "total": 42,
    "limit": 20,
    "next_cursor": "eyJhcGlfa2V5X2hhc2gi..."
  }
}

Usage

GET/v1/scan/usage

Get current usage for your account. Usage is tracked per account, not per API key.

{
  "status": "success",
  "data": {
    "period": "2026-03",
    "scans_used": 142,
    "scans_limit": 500,
    "tier": "free",
    "resets_at": "2026-04-01T00:00:00Z"
  }
}

Errors

All errors follow the same format:

{
  "status": "error",
  "error": {
    "code": "invalid_api_key",
    "message": "Missing X-API-Key header"
  }
}

Error codes

HTTPCodeDescription
400invalid_fileFile missing or corrupt
401invalid_api_keyMissing, invalid, or revoked key
404scan_not_foundScan ID doesn't exist
413file_too_largeExceeds tier file size limit
429rate_limit_exceededMonthly scan quota reached
500internal_errorServer error

Rate Limits

Usage is tracked at the account level and resets on the first of each month (UTC).

TierScans / monthMax file size (via v2)
Free50025 MB
Pro (coming soon)10,000100 MB

When you hit the limit, requests return 429 with code rate_limit_exceeded. Check your current usage via GET /v1/scan/usage.

Node SDK

Optional. Everything above works from any language with an HTTP client. If you are on Node, this package wraps the same API and picks the upload path for you.

Installbash
npm install @usebouncer/sdk
Scan a filetypescript
import { Bouncer } from "@usebouncer/sdk";

const bouncer = new Bouncer("bnc_live_your_key_here");

const result = await bouncer.scan("./report.pdf");

result.verdict;   // "clean" | "suspicious" | "malicious"
result.threats;   // [{ engine, rule, severity }]
result.file;      // { name, size, mime_type, sha256 }

That single call uploads the file, waits for the scan to finish, and returns the result. Files over about 4.5 MB take the two-step route described above; the SDK chooses without being told.

Errors are typed

So you can tell “fix your key” from “you are out of scans” without matching on message strings.

Handling failurestypescript
import { BouncerAuthError, BouncerQuotaError } from "@usebouncer/sdk";

try {
  await bouncer.scan("./report.pdf");
} catch (err) {
  if (err instanceof BouncerAuthError) { /* key is wrong or revoked */ }
  if (err instanceof BouncerQuotaError) { /* out of scans this period */ }
}

Uploading without waiting

Split the calltypescript
const { scan_id } = await bouncer.createScan("./big.iso");
// store scan_id, come back later, possibly in another process
const result = await bouncer.wait(scan_id);

Requires Node 20 or newer. Full reference on npm.

Questions? Reach out at hello@manishj.com