Snag Watcher API https://watcher-api-staging.manwhas.shop

Guides

Webhooks

JSON
"notify": {
  "channels": ["webhook"],
  "repeat": "on_change",
  "cooldown": "6h",
  "webhook": { "url": "https://you.example/snag", "events": ["matched", "changed", "blocked"] }
}

Events

typeWhendata
matchedThe conditions hold for the first timeresults, conditions, and then if a follow-up was created
changedThe matched result changedprevious, current, what
field_foundA release date, price or opening time appearedsite, url, field, value
blockedA site blocked 3 runs in a row, or works againsite, since, runs, recovered
clampedWe slowed your schedule to a site's minimumclamps

Every event has the same envelope:

JSON
{
  "id": "evt_4m2k9q8x7c1v0b3n5z6w",
  "type": "changed",
  "created_at": "2026-11-05T08:00:12.004Z",
  "watcher_id": "wch_dhjqcvaqnhrchsnv7gtc",
  "kind": "availability",
  "data": {
    "group": "site:allbirds.com",
    "previous": [{ "site": "allbirds.com", "attributes": { "size": 10 }, "price": { "cents": 11000, "currency": "USD" }, "availability": "low_stock" }],
    "current": [{ "site": "allbirds.com", "attributes": { "size": 10 }, "price": { "cents": 8800, "currency": "USD" }, "availability": "in_stock" }],
    "what": ["price", "availability"]
  }
}

Each result has site, url, title, version, attributes, price, availability, quantity, date, release_status, release_date and seller. Anything the page didn't show is null.

Repeat and cooldown

repeatSends
onceThe first match only
on_changeThe first match, then each change. A price has to move 1% or more. The default
every_matchEvery run that matches

cooldown is the shortest gap between sends; the default is 6h. Restocks and new slots ignore it.

Verify the signature

Text
Snag-Signature: t=1793820000,v1=5f1c0e...
Snag-Event-Id: evt_4m2k9q8x7c1v0b3n5z6w

v1 is the hex HMAC-SHA256 of t, a dot and the raw request body, keyed with your webhook_secret. Reject a t more than 5 minutes old, and use Snag-Event-Id to drop repeats.

JavaScript
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifySnag(secret, rawBody, header) { const { t, v1 } = Object.fromEntries(header.split(',').map((part) => part.split('='))); if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; const expected = createHmac('sha256', secret).update(<span class="hljs-subst">${t}</span>.<span class="hljs-subst">${rawBody}</span>).digest('hex'); return v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected)); }

Verify against the raw body, before your framework parses the JSON.

Retries

Answer with a 2xx within 10 seconds. Anything else is retried after 1m, 5m, 30m, 2h and 12h, then dropped. After 20 dropped deliveries in a row, we stop sending for that watcher.

The URL must use https (or localhost with a test key) and can't resolve to a private address.