Docs
From upload to verdict.
Every scan starts from a presigned URL. Set up your account once, then request scans by hand in the dashboard or from your backend with an API key.
01 - Set up
01
Add a source URL
Under Integration, add the origin your presigned URLs come from, e.g. your bucket's https host. Scans of any other host are rejected.
02
Verify it
Publish the token shown at /.well-known/zerosieve-verify-<token> on that host, then click Verify.
03
Add a webhook (optional)
Register and verify the https endpoint that should receive verdicts, then generate a signing secret. Nothing is delivered without one but regular manual scans will still work fine.
02 - Request scans
Two ways in, same pipeline.
Manual - dashboard
Paste a URL
Open the dashboard, paste a presigned URL and submit. The scan appears in the list and its status updates live as the worker reports back.
Good for one-off checks and for trying your setup before wiring up code.
Automated - API
Submit from your backend
Create a key under Integration → API keys and send it as a bearer token. Each request takes up to 100 files and returns one scan.
curl -X POST https://api.zerosieve.com/api/v1/scans \
-H "Authorization: Bearer zerosieve_sk_..." \
-H "Content-Type: application/json" \
-d '{"files":[{"url":"https://your-bucket.example.com/file.pdf?X-Amz-..."}]}'A 201 means it's queued. Check data.scan.id is present and store it - it's how you match the verdict later.
{
"status": true,
"data": {
"scan": {
"id": "cm3k9x2f80001qz8h4v7d2abc",
"status": "queued",
"requestedAt": "2026-11-05T12:00:00.000Z",
"completedAt": null,
"items": [
{ "id": "cm3k9x2f80002qz8h1m5e7xyz", "url": "https://your-bucket.example.com/file.pdf?X-Amz-...", "name": "file.pdf", "status": "pending" }
]
}
}
}Then wait for the webhook - the verdict is pushed to your endpoint as soon as the scan finishes. Every delivery carries an Aegis-Signature header: the hex HMAC-SHA256 of the raw body, keyed with your signing secret. A minimal Express receiver that rejects anything that doesn't match:
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
const app = express();
// express.raw keeps the exact bytes the signature was computed over
app.post("/webhooks/zerosieve", express.raw({ type: "application/json" }), (req, res) => {
const expected = Buffer.from(createHmac("sha256", process.env.WEBHOOK_SECRET).update(req.body).digest("hex"));
const provided = Buffer.from(String(req.headers["aegis-signature"] ?? ""));
if (expected.length != provided.length || !timingSafeEqual(expected, provided)) return res.sendStatus(401);
console.log(JSON.parse(req.body));
res.sendStatus(200);
});
app.listen(3000);No webhook after a while? Fall back to GET /api/v1/scans/:id and check whether status is clean, infected or error.