Delivery
We POST this to your endpoint, signed with its secret. Check the signature before trusting the body.
Headers
X-TagSentry-Signaturestringin headerrequiredt=<unix seconds>,v1=<hex HMAC-SHA256 of "t.body">, keyed with the endpoint's secret.X-TagSentry-Eventstringin headerrequiredThe event type; equal to the body's
type.Always
scan.completedX-TagSentry-Deliverystringin headerrequiredThe delivery id; equal to the body's
id. Stable across retries.
Body
idstringrequiredThe delivery id. Stable across retries and equal to
X-TagSentry-Delivery: dedupe on it.typestringrequiredAlways
scan.completedversionintegerrequiredMoves only when a receiver could notice. A new optional field does not move it.
createdAtstringrequiredISO-8601 timestamp, UTC.
accountIdstringrequiredsiteobjectrequiredsite.idstringrequiredsite.domainstringrequiredsite.regionstringrequireddataobjectrequireddata.scanRunIdstringrequiredThe scan run's id: the same id GET /sites/{siteId}/scans/latest returns as
scanIdfor that tier.data.tierstringrequiredONE SCAN REQUEST CAN SEND TWO OF THESE, one per tier, with different ids.
tier1is the scan POST /scans starts (itsscanId); when it completes, a deepertier2crawl follows and sends its own. Wait fortier1to know the inventory is established.One of
tier1,tier2data.statusstringrequiredcompleted,partialorfailed.data.finishedAtstringrequiredISO-8601 timestamp, UTC.
data.pagesScannedintegerrequireddata.tagsFoundintegerrequireddata.unmanagedCountintegerrequireddata.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