API DOCS
Everything you need to integrate Bouncer into your application.
https://api.usebouncer.devQuickstart
Scan a file in 3 steps: get an API key from the dashboard, upload a file, and poll for the result.
curl -X POST https://api.usebouncer.dev/v1/scan \
-H "X-API-Key: bnc_live_your_key_here" \
-F "file=@document.pdf"{
"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
/v1/scanUpload a file as multipart/form-data. The scan runs asynchronously. You get back a scan_id immediately.
Request
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
| Tier | Max file size | Endpoint |
|---|---|---|
| Any tier | ~4.5 MB | POST /v1/scan |
| Free | 25 MB | POST /v2/scan |
| Pro | 100 MB | POST /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)
/v2/scanUse 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
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"}'{
"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.
curl -X POST "$UPLOAD_URL" \
-F key="uploads/scan_01H8MZ.../report.pdf" \
-F policy="..." -F x-amz-signature="..." \
-F file=@report.pdf3. 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.fieldsas a credential. Do not log it, cache it, or pass it on. - A file over your tier limit is refused by storage with an
EntityTooLargeXML 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/scanis unchanged and remains the simpler option under 4.5 MB, since it is a single request.
Poll Results
/v1/scan/:scan_idPoll 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
| Status | Description |
|---|---|
| pending | Waiting for scan to start |
| scanning | Scan in progress |
| completed | Scan finished, check verdict |
| failed | Scan engine error |
Verdicts
| Verdict | Description |
|---|---|
| clean | No threats detected |
| suspicious | Pattern matches but not confirmed malware |
| malicious | Confirmed malware detected |
Scan History
/v1/scanList all scans for the authenticated API key. Supports cursor-based pagination.
Query parameters
| Param | Type | Description |
|---|---|---|
| limit | number | Max results (default 20, max 100) |
| cursor | string | Pagination 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
/v1/scan/usageGet 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
| HTTP | Code | Description |
|---|---|---|
| 400 | invalid_file | File missing or corrupt |
| 401 | invalid_api_key | Missing, invalid, or revoked key |
| 404 | scan_not_found | Scan ID doesn't exist |
| 413 | file_too_large | Exceeds tier file size limit |
| 429 | rate_limit_exceeded | Monthly scan quota reached |
| 500 | internal_error | Server error |
Rate Limits
Usage is tracked at the account level and resets on the first of each month (UTC).
| Tier | Scans / month | Max file size (via v2) |
|---|---|---|
| Free | 500 | 25 MB |
| Pro (coming soon) | 10,000 | 100 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.
npm install @usebouncer/sdkimport { 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.
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
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