Snag Watcher Docs

API reference

Create a watcher

POST /v1/watchers

Creates a watcher and schedules its first run. Returns the watcher object and, when notify has a webhook, webhook_secret.

Body

FieldType
targetobjectRequired. What to watch. Matching products
conditionsobjectRequired. { "all": [...] } or { "any": [...] }, 1 to 10. Conditions
whereobjectRequired. sites (1 to 10) and aggregate
scheduleobjectRequired. every and until, optionally start_at and burst. Schedules
notifyobjectWhere results go. Webhooks
locationobjectcountry, zip, radius, currency
fetchobjectproxy and accept_site_terms
thenobjectA follow-up watcher. Follow-up watchers

target

FieldType
querystringRequired. A URL, an identifier or words, by match
matchstringRequired. url, sku, exact, line, category, listing
attributesobjectVariant filters, such as { "size": 10, "color": "Black" }. A list means any of them. Some sites also read settings here (from, to, time_zone, party_size, selector, contains); those tell the site what to fetch and never filter. Sites
excludestring[]Phrases that drop a product, as whole words

Condition

FieldType
fieldstringRequired. price, price_drop, availability, quantity, version, release_status, release_date, date, opens_at, seller, or an attribute
opstringRequired. = != < <= > >= in not_in exists changed
valueanyA number, "$19.99", "10%", a date, a status, or a list for in
compare_tostringfirst_snapshot or last_snapshot
overobjectkey (sizes, dates, items), require (all, any, min_n), n, same (site, seller)

where

FieldType
sitesstring[]Required. Hosts, repos, pages or URLs, and feed:, statuspage:, instatus: or change: names. Sites
aggregatestringeach (default), best, first, dedupe

schedule

FieldType
everystringRequired. "15m", "1h", "1d"
untilstringRequired. A date, "+30d", "after_first_match" or "never"
start_atstringFirst run, ISO 8601. UTC without an offset
burstobjectnear (release_date, opens_at), window, every

notify

FieldType
channelsstring[]Required. ["webhook"] with an API key
webhookobjecturl (https) and events: matched, changed, field_found, blocked, clamped
repeatstringonce, on_change (default), every_match
cooldownstringShortest gap between sends. Default "6h"

location and fetch

FieldType
location.zipstringUS ZIP. With radius, keeps places within that distance
location.radiusstring"25mi" or "40km"
location.countrystringUS
location.currencystringUSD
fetch.proxystringauto (default) or off
fetch.accept_site_termsbooleanRequired for sites whose robots.txt restricts the data. Sites

Request

Shell
curl https://watcher-api.manwhas.shop/v1/watchers \
  -H "Authorization: Bearer $SNAG_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: wool-runner-10" \
  -d '{
    "target": { "query": "https://www.allbirds.com/products/mens-wool-runners", "match": "url", "attributes": { "size": 10 } },
    "conditions": { "all": [{ "field": "availability", "op": "=", "value": "back_in_stock" }] },
    "where": { "sites": ["allbirds.com"] },
    "schedule": { "every": "1h", "until": "+30d" },
    "notify": { "channels": ["webhook"], "webhook": { "url": "https://you.example/snag", "events": ["matched", "blocked"] } }
  }'

Response

201 Created

JSON
{
  "watcher_id": "wch_dhjqcvaqnhrchsnv7gtc",
  "kind": "availability",
  "status": "active",
  "parent_id": null,
  "request": { "target": { "query": "https://www.allbirds.com/products/mens-wool-runners", "match": "url", "attributes": { "size": 10 } } },
  "schedule": { "every": "1h", "next_run_at": "2026-11-04T19:38:44.390Z", "until": "2026-12-04T19:38:43.902Z" },
  "clamped": [],
  "warnings": [],
  "created_at": "2026-11-04T19:38:43.902Z",
  "last_run_at": null,
  "first_match_at": null,
  "last_snapshot": null,
  "events": [],
  "webhook_secret": "whsec_Qm9vcGllcy1hcmUtbm90LWEta2V5"
}

request is shortened here; it holds the full body you sent. A broken rule returns 422 with every error at once. See Errors.

A page read by a learned recipe is learned now, before the watcher is saved, so the first run already knows how to read it. If it can't be learned, the answer is 422 with recipe_not_learned.

Suggestions

On a 422, the body may carry a suggestion: a corrected request we checked by validating and previewing it. It's never applied for you. Read it, and send suggestion.request yourself if it's what you meant.

JSON
{
  "error": { "code": "site_cant_match", "message": "Any online store can't run a listing match. It supports: url, line, category.", "field": "where.sites[0]" },
  "errors": [{ "code": "site_cant_match", "message": "Any online store can't run a listing match. It supports: url, line, category.", "field": "where.sites[0]" }],
  "suggestion": {
    "request": { "target": { "query": "Q1", "match": "line" }, "where": { "sites": ["keychron.com"] } },
    "explanation": ["keychron.com can't run a listing match; a line match on \"Q1\" finds 7 products."],
    "evidence": { "found": 26, "matching_target": 7, "would_match_now": true, "sites": [{ "site": "keychron.com", "outcome": "ok", "found": 26, "matching_target": 7, "sample": [] }] }
  }
}
Field
requestThe whole corrected body, ready to send. Shortened here
explanationWhat looked wrong and what changed, in plain sentences
evidenceWhat the preview of the suggestion found: items, how many fit the target, and a few samples per site

There's no suggestion when nothing better was found in time.