Webhooks
When a scan completes, FileSafety sends the result as an HTTP POST to the webhook_url you provided when submitting the scan. Webhooks are the recommended way to receive scan results in production.
Delivery
Section titled “Delivery”- Method:
POST - Content-Type:
application/json - Timing: Delivered immediately after scan processing finishes (typically 10-20 seconds after submission)
- Target: The
webhook_urlspecified in yourPOST /v1/scanrequest
Each scan triggers exactly one webhook delivery. The webhook URL is per-scan, not a global configuration — you can use different URLs for different scans.
Payload structure
Section titled “Payload structure”{ "event": "scan.complete", "scan_id": "scn_01HX7Z9K3M2N4P5Q6R7S8T9U0V", "verdict": "clean", "virus": { "clean": true, "engine": "clamav", "signature": null }, "nsfw": { "clean": true, "categories": [], "confidence": 0.01 }, "file_hash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "metadata": {}, "completed_at": "2026-03-23T12:00:00Z"}Payload fields
Section titled “Payload fields”| Field | Type | Description |
|---|---|---|
event | string | Event type. Currently always "scan.complete". |
scan_id | string | The unique scan identifier. |
verdict | string | Overall result: clean, infected, nsfw, mixed, or failed. |
virus | object | Virus scan results. |
virus.clean | boolean | true if no virus was detected. |
virus.engine | string | Engine used. Currently "clamav". |
virus.signature | string | null | Detected threat name, or null if clean. |
nsfw | object | NSFW scan results. |
nsfw.clean | boolean | true if no NSFW content was detected. |
nsfw.categories | string[] | Detected categories (empty array if clean). |
nsfw.confidence | number | Confidence score from 0 to 1. |
file_hash | string | Cryptographic hash of the file, prefixed with sha256:. |
metadata | object | The metadata you provided when submitting the scan. Empty object if none was provided. |
completed_at | string | ISO 8601 timestamp of when the scan finished. |
Example payloads
Section titled “Example payloads”Clean file
Section titled “Clean file”{ "event": "scan.complete", "scan_id": "scn_01HX7Z9K3M2N4P5Q6R7S8T9U0V", "verdict": "clean", "virus": { "clean": true, "engine": "clamav", "signature": null }, "nsfw": { "clean": true, "categories": [], "confidence": 0.01 }, "file_hash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "metadata": {"user_id": "usr_123"}, "completed_at": "2026-03-23T12:00:00Z"}Infected file
Section titled “Infected file”{ "event": "scan.complete", "scan_id": "scn_01HX8A1B2C3D4E5F6G7H8I9J0K", "verdict": "infected", "virus": { "clean": false, "engine": "clamav", "signature": "Win.Trojan.Agent-123456" }, "nsfw": { "clean": true, "categories": [], "confidence": 0.02 }, "file_hash": "sha256:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", "metadata": {"user_id": "usr_456"}, "completed_at": "2026-03-23T12:01:30Z"}NSFW content detected
Section titled “NSFW content detected”{ "event": "scan.complete", "scan_id": "scn_01HX9B2C3D4E5F6G7H8I9J0K1L", "verdict": "nsfw", "virus": { "clean": true, "engine": "clamav", "signature": null }, "nsfw": { "clean": false, "categories": ["explicit_nudity"], "confidence": 0.98 }, "file_hash": "sha256:f1e2d3c4b5a6f1e2d3c4b5a6f1e2d3c4b5a6f1e2d3c4b5a6f1e2d3c4b5a6f1e2", "metadata": {"source": "profile-picture"}, "completed_at": "2026-03-23T12:02:45Z"}Retry policy
Section titled “Retry policy”FileSafety expects your webhook endpoint to respond with a 2xx status code (e.g., 200 OK).
- If your endpoint returns a non-2xx status or the connection fails, the delivery is retried up to 3 times with exponential backoff (delays of approximately 10 seconds, 1 minute, and 5 minutes).
- After all retries are exhausted, the webhook is marked as failed. The scan result remains available via GET /v1/scan/{id} indefinitely.
Handling tips
Section titled “Handling tips”Respond quickly
Section titled “Respond quickly”Return a 200 response immediately, before doing any heavy processing. Process the webhook payload asynchronously:
app.post("/webhooks/filesafety", (req, res) => { res.status(200).send("ok");
// Process asynchronously processWebhook(req.body).catch(console.error);});Idempotency
Section titled “Idempotency”Your webhook endpoint may receive the same payload more than once (due to retries or network issues). Use the scan_id field to deduplicate:
async function processWebhook(payload) { const alreadyProcessed = await db.exists(`webhook:${payload.scan_id}`); if (alreadyProcessed) return;
await db.set(`webhook:${payload.scan_id}`, true); // Handle the scan result...}Use metadata for correlation
Section titled “Use metadata for correlation”Include identifiers in the metadata field when submitting scans so you can route webhook payloads back to the correct user, upload, or workflow:
{ "metadata": { "user_id": "usr_123", "upload_id": "upl_456", "source": "profile-picture" }}Validate the payload
Section titled “Validate the payload”Verify that the scan_id in the webhook payload matches a scan you actually submitted. This prevents processing payloads from unknown sources.
See also
Section titled “See also”- Webhook Integration Guide — Full Express.js implementation example
- POST /v1/scan — Submitting scans with webhook URLs