All endpoints

DocsAPIWebhooks

Webhookscan.completed

A scan finished

Once per scan run, whatever its outcome.

Delivery

We POST this to your endpoint, signed with its secret. Check the signature before trusting the body.

Headers

  • X-TagSentry-Signaturestringin headerrequired

    t=<unix seconds>,v1=<hex HMAC-SHA256 of "t.body">, keyed with the endpoint's secret.

  • X-TagSentry-Eventstringin headerrequired

    The event type; equal to the body's type.

    Always scan.completed

  • X-TagSentry-Deliverystringin headerrequired

    The delivery id; equal to the body's id. Stable across retries.

Body

  • idstringrequired

    The delivery id. Stable across retries and equal to X-TagSentry-Delivery: dedupe on it.

  • typestringrequired

    Always scan.completed

  • versionintegerrequired

    Moves only when a receiver could notice. A new optional field does not move it.

  • createdAtstringrequired

    ISO-8601 timestamp, UTC.

  • accountIdstringrequired
  • siteobjectrequired
  • site.idstringrequired
  • site.domainstringrequired
  • site.regionstringrequired
  • dataobjectrequired
  • data.scanRunIdstringrequired

    The scan run's id: the same id GET /sites/{siteId}/scans/latest returns as scanId for that tier.

  • data.tierstringrequired

    ONE SCAN REQUEST CAN SEND TWO OF THESE, one per tier, with different ids. tier1 is the scan POST /scans starts (its scanId); when it completes, a deeper tier2 crawl follows and sends its own. Wait for tier1 to know the inventory is established.

    One of tier1, tier2

  • data.statusstringrequired

    completed, partial or failed.

  • data.finishedAtstringrequired

    ISO-8601 timestamp, UTC.

  • data.pagesScannedintegerrequired
  • data.tagsFoundintegerrequired
  • data.unmanagedCountintegerrequired
  • data.newOrChangedCountintegerrequired

Example body

{  "id": "3f6c1b8e-2d4a-4c7e-9a51-0b8f2e6d7c10",  "type": "scan.completed",  "version": 0,  "createdAt": "2026-09-26T14:02:00.000Z",  "accountId": "3f6c1b8e-2d4a-4c7e-9a51-0b8f2e6d7c10",  "site": {    "id": "3f6c1b8e-2d4a-4c7e-9a51-0b8f2e6d7c10",    "domain": "silverpine.example",    "region": "…"  },  "data": {    "scanRunId": "3f6c1b8e-2d4a-4c7e-9a51-0b8f2e6d7c10",    "tier": "tier1",    "status": "…",    "finishedAt": "2026-09-26T14:02:00.000Z",    "pagesScanned": 0,    "tagsFound": 0,    "unmanagedCount": 0,    "newOrChangedCount": 0  }}

Answer with any 2xx. Anything else counts as a failed attempt.

Delivery and signing

Read status: a failed scan is an event too.

Verifying a delivery. Every POST carries X-TagSentry-Signature: t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256, keyed with the endpoint's secret (whsec_…, shown once when the endpoint is added), of the string t + "." + rawBody: the timestamp, a full stop, then the body EXACTLY as received, before any JSON parsing. Compare in constant time and refuse a t more than 300 seconds from your clock.

const crypto = require("node:crypto");
function verify(secret, header, rawBody, nowSeconds = Math.floor(Date.now() / 1000)) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!Number.isInteger(t) || Math.abs(nowSeconds - t) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  const given = Buffer.from(parts.v1 ?? "", "hex");
  return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}

X-TagSentry-Event names the event; X-TagSentry-Delivery is the delivery id, stable across retries. Answer 2xx within 10 seconds. A timeout, 408, 429 or 5xx is retried with backoff, 8 tries in all; any other answer, and a redirect, fails at once. We never follow redirects.

From the OpenAPI document, version 2026-08-26. Raw OpenAPI