← All posts

Stop polling: monitors and signed webhooks

Save a listing and a condition once. Ferox checks it on a schedule and calls your server when the condition is met.

Most teams start the same way. They write a loop that fetches a product page every few minutes and compares the price with the last one. It works until it doesn't. The loop needs a server, a store for the last value and retry logic. It also spends a request on every pass, even when nothing changed.

Monitors move that loop to our side.

A monitor is a saved call plus a rule

You pick a marketplace, an endpoint and its parameters, the same ones you would pass to the API. Then you add a condition:

  • price_below fires when the price is at or under a value you set.
  • price_above fires when the price is at or over a value.
  • price_change_pct fires when the price moves by at least that percent since the last check, in either direction.
  • back_in_stock fires when an item that was out of stock comes back.
  • any_change fires when the price or stock state changes at all.

Each monitor has its own interval. The default is once a day. Our scheduler looks for due monitors every five minutes.

What a check costs

A monitor run is an ordinary metered call, so it costs what the same API call would cost. If your team is out of credits, we skip the run instead of fetching data you have not paid for. Nothing fires, and the next run tries again.

What you receive

When a condition fires, we email the addresses on the monitor and POST this payload to each webhook subscribed to monitor.triggered:

{
  "event": "monitor.triggered",
  "monitor": {"id": 42, "name": "Headphones under $200",
              "marketplace": "walmart", "endpoint": "product_details"},
  "condition": {"type": "price_below", "value": 200},
  "current":  {"price": 189.0, "in_stock": true},
  "previous": {"price": 219.0, "in_stock": true},
  "triggered_at": "2026-10-12T09:15:00"
}

You get the old and new values side by side, so your handler doesn't need to keep its own history.

Check the signature

Anyone who learns your webhook URL can post to it. Every delivery carries an X-Ferox-Signature header: an HMAC-SHA256 of the raw request body, keyed with the whsec_ secret we show once when you create the webhook. Check it before you act on the payload:

import hashlib
import hmac

def is_from_ferox(secret: str, raw_body: bytes, signature: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

Sign the raw bytes you received. If you parse the JSON and dump it again, the key order or spacing can change and the signature will not match.

Keep a fallback

We try each delivery, but a webhook can still fail if your server is down at the wrong moment. Treat webhooks as the fast path. Read the monitor's run history in the dashboard or API when you need the full record.

Want to try this on your own products?

Try for free ↗

More from the blog