Skip to content

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.

  • Method: POST
  • Content-Type: application/json
  • Timing: Delivered immediately after scan processing finishes (typically 10-20 seconds after submission)
  • Target: The webhook_url specified in your POST /v1/scan request

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.

{
"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"
}
FieldTypeDescription
eventstringEvent type. Currently always "scan.complete".
scan_idstringThe unique scan identifier.
verdictstringOverall result: clean, infected, nsfw, mixed, or failed.
virusobjectVirus scan results.
virus.cleanbooleantrue if no virus was detected.
virus.enginestringEngine used. Currently "clamav".
virus.signaturestring | nullDetected threat name, or null if clean.
nsfwobjectNSFW scan results.
nsfw.cleanbooleantrue if no NSFW content was detected.
nsfw.categoriesstring[]Detected categories (empty array if clean).
nsfw.confidencenumberConfidence score from 0 to 1.
file_hashstringCryptographic hash of the file, prefixed with sha256:.
metadataobjectThe metadata you provided when submitting the scan. Empty object if none was provided.
completed_atstringISO 8601 timestamp of when the scan finished.
{
"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"
}
{
"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"
}
{
"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"
}

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.

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);
});

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...
}

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"
}
}

Verify that the scan_id in the webhook payload matches a scan you actually submitted. This prevents processing payloads from unknown sources.