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.