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 at least one webhook delivery (scan.complete). In rare cases, a follow-up scan.verdict_updated event may also be sent if post-processing analysis revises the verdict. 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, "signature": null }, "nsfw": { "clean": true, "categories": [], "confidence": 0.01 }, "text": { "clean": true, "toxic": { "labels": [], "maxScore": 0.0 }, "pii": { "entities": [], "count": 0 }, "sentiment": { "dominant": "NEUTRAL", "scores": {} } }, "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. See Event types below. |
scan_id | string | The unique scan identifier. |
verdict | string | Overall result: clean, infected, nsfw, toxic, mixed, or failed. |
virus | object | Malware detection results. |
virus.clean | boolean | true if no virus was detected. |
virus.signature | string | null | Detected threat name, or null if clean. |
nsfw | object | Content analysis (images) 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. |
text | object | Content analysis (text) results. Present when text analysis was applied based on file size. |
text.clean | boolean | true if no text issues were detected. |
text.toxic | object | Toxicity detection with labels array and maxScore. |
text.pii | object | PII detection with entities list and count. |
text.sentiment | object | Sentiment analysis with dominant label and scores. |
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, "signature": null }, "nsfw": { "clean": true, "categories": [], "confidence": 0.01 }, "text": { "clean": true, "toxic": { "labels": [], "maxScore": 0.0 }, "pii": { "entities": [], "count": 0 }, "sentiment": { "dominant": "NEUTRAL", "scores": {} } }, "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, "signature": "Win.Trojan.Agent-123456" }, "nsfw": { "clean": true, "categories": [], "confidence": 0.02 }, "text": { "clean": true, "toxic": { "labels": [], "maxScore": 0.0 }, "pii": { "entities": [], "count": 0 }, "sentiment": { "dominant": "NEUTRAL", "scores": {} } }, "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, "signature": null }, "nsfw": { "clean": false, "categories": ["explicit_nudity"], "confidence": 0.98 }, "text": { "clean": true, "toxic": { "labels": [], "maxScore": 0.0 }, "pii": { "entities": [], "count": 0 }, "sentiment": { "dominant": "NEUTRAL", "scores": {} } }, "file_hash": "sha256:f1e2d3c4b5a6f1e2d3c4b5a6f1e2d3c4b5a6f1e2d3c4b5a6f1e2d3c4b5a6f1e2", "metadata": { "source": "profile-picture" }, "completed_at": "2026-03-23T12:02:45Z"}Toxic text detected
Section titled “Toxic text detected”{ "event": "scan.complete", "scan_id": "scn_01HX9C3D4E5F6G7H8I9J0K1L2M", "verdict": "toxic", "virus": { "clean": true, "signature": null }, "nsfw": { "clean": true, "categories": [], "confidence": 0.01 }, "text": { "clean": false, "toxic": { "labels": ["HATE_SPEECH"], "maxScore": 0.92 }, "pii": { "entities": [], "count": 0 }, "sentiment": { "dominant": "NEGATIVE", "scores": {} } }, "file_hash": "sha256:d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3", "metadata": { "source": "comment-upload" }, "completed_at": "2026-03-23T12:03:15Z"}Event types
Section titled “Event types”| Event | Description |
|---|---|
scan.complete | Scan has finished processing. Contains the full verdict and all results. |
scan.verdict_updated | A previously completed scan’s verdict was revised by post-processing analysis. The payload contains the updated verdict and results. Handle this by replacing the original result. |
Verdict updated example
Section titled “Verdict updated example”In rare cases, post-processing analysis may revise a scan’s verdict after the initial scan.complete event. When this happens, a scan.verdict_updated webhook is sent with the corrected result:
{ "event": "scan.verdict_updated", "scan_id": "scn_01HX7Z9K3M2N4P5Q6R7S8T9U0V", "verdict": "infected", "previous_verdict": "clean", "virus": { "clean": false, "signature": "Win.Trojan.Agent-789012" }, "nsfw": { "clean": true, "categories": [], "confidence": 0.01 }, "text": { "clean": true, "toxic": { "labels": [], "maxScore": 0.0 }, "pii": { "entities": [], "count": 0 }, "sentiment": { "dominant": "NEUTRAL", "scores": {} } }, "file_hash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "metadata": { "user_id": "usr_123" }, "completed_at": "2026-03-23T12:05:00Z"}Your webhook handler should treat scan.verdict_updated the same as scan.complete, replacing any previously stored result for the given scan_id.
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 5 times (6 total delivery attempts, including the initial attempt).
- 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