# Change events

kwery can watch products you've already queried and push a webhook the moment something
meaningful changes — a price move, a buy-box flip, an offer-count change, or a previously
not-found product coming back. You don't re-poll and you don't diff snapshots yourself:
the diffing happens on our side, and you receive the change, signed, at your endpoint.

> **Rolling out.** Change-event subscriptions are being enabled account by account —
webhook delivery is not yet switched on for self-serve accounts.
[Contact us](https://kwery.co/contact) to join the rollout; we'll configure your watch
list, event types, and thresholds with you. Everything below documents the delivery
contract your receiver should implement — it is final and will not change.


## How watching works 

Any product you query through kwery is eligible for watching — the watch list is built from
the same `source` / `country` / `key` / `value` identities you already submit. A
**subscription** ties a watch list to a delivery endpoint and says which events you want:

- your `callback_url` (the same kind of endpoint as [stream webhooks](/guides/webhooks)),
- the event types you care about (see the catalog below — or just the umbrella event),
- optional noise controls: [thresholds](#thresholds) and [`delta_only`](#delta-only).


Subscriptions are configured per account — [contact us](https://kwery.co/contact) to set up
or tune yours (self-serve management from the dashboard is on our roadmap). The underlying
lookups that feed change detection are ordinary crawls, billed as normal credits; **event
deliveries themselves are never billed**.

## Event catalog 

| Event | Fires on |
|  --- | --- |
| `product.changed` | Umbrella event — fires on any significant change (price, offer count, or buy-box). One subscription that covers everything. |
| `price.delta` | The average/representative price for the product changed. |
| `buybox.change` | The Amazon buy-box winner or buy-box price changed. |
| `stock.update` | Availability or the number of active offers changed. |
| `product.resolved` | A product that was previously not-found is now found. |
| `product.unavailable` | A product we had been finding can no longer be found. Deliberately conservative — see below. |


**About `product.unavailable`.** A lookup coming back empty is not proof a product is gone:
a marketplace outage, a temporary block, or a change to how a product is identified all
look the same from the outside. So we do not report it on the first miss. We wait until we
have missed the product several times in a row, and we do not count misses recorded while
we already know that marketplace is degraded. The trade is deliberate: you hear about a
genuine disappearance a little later, rather than being told your whole catalogue vanished
because we had a bad hour. It fires once per disappearance, not on every lookup afterwards,
and `product.resolved` follows if the product returns.

Subscribe to the umbrella event alone, or to individual event types if you only care about,
say, price moves. Notes per event:

- **`price.delta`** — `changes` carries `price` as an old → new pair. "Representative
price" is the product's aggregate (the same basis as `price_avg` in results), so one
seller repricing inside a stable field doesn't fire it unless it moves the aggregate past
your threshold.
- **`buybox.change`** — [Amazon](/sources/amazon) only. `changes` carries
`buybox_seller` and/or `buybox_price`.
- **`stock.update`** — `changes` carries `offer_count` and/or `availability`.
- **`product.resolved`** — carries no `changes` block; the product's (re)appearance *is*
the signal. Useful for delisting monitoring: you learn the moment a competitor's product
is listed again.


## Payload reference 

Every change event is a `POST` to your `callback_url` with this body (the example watches
an [Idealo](/sources/idealo) product):

```json
{
  "event": "price.delta",
  "delivered_at": "2026-07-01T09:15:32.000Z",
  "product": {
    "source": "idealo",
    "country": "de",
    "key": "gtin",
    "value": "4006381333962",
    "name": "STABILO point 88 ocker"
  },
  "changes": {
    "price": { "old": 17.40, "new": 16.80 }
  }
}
```

| Field | Meaning |
|  --- | --- |
| `event` | One of the five event types above. |
| `delivered_at` | When this delivery was signed and sent (ISO 8601, UTC). |
| `product` | The identity you originally queried: `source`, `country`, `key`, `value`, plus the product `name` for human-readable logs. |
| `changes` | The changed fields, each an **old → new pair**. Keys depend on the event (`price`, `offer_count`, `availability`, `buybox_seller`, `buybox_price`). Absent for `product.resolved`. |


By default the event also includes the **full current record** alongside `changes`, so you
don't need a second lookup to get the rest of the product's fields.

## Delta-only mode 

If you only need the diff, set `delta_only: true` on the subscription: kwery then sends
just the `product` identity and the `changes` block — smaller payloads, useful for
high-volume watch lists where you keep your own product store anyway.

## Thresholds 

To avoid firing on noise — a cent of price jitter, a seller re-listing the same offer —
each subscription can carry change thresholds (minimum absolute or percentage move before
`price.delta` fires) and a margin floor. Thresholds are configured per subscription when we
set it up; tell us what "meaningful" means for your catalog and we tune to it.

## Delivery, signing, and retries 

Change events use the same delivery machinery as [Stream API webhooks](/guides/webhooks) — same
signing, same retry behavior, same replay. In full:

**Headers on every delivery:**

| Header | Content |
|  --- | --- |
| `X-Kwery-Signature` | `v1=` + HMAC-SHA256 of `"{timestamp}.{raw_body}"`, keyed with your `webhook_secret`. |
| `X-Kwery-Timestamp` | Unix seconds when the delivery was signed. |
| `X-Kwery-Delivery-Id` | Unique ID for this delivery — use it for idempotency and when talking to support. |


**Your endpoint should:** verify the signature against the **raw body** (constant-time
compare), reject timestamps older than 5 minutes (replay protection), respond `2xx`
quickly, and process asynchronously. The full verification walkthrough with Node.js and
Python code is in [Webhooks → Verify the signature](/guides/webhooks#3-verify-the-signature) —
the steps are identical for change events.

**Retries:** an immediate first attempt, then **+30 s → +2 min → +10 min → +30 min**
(5 attempts total, ±20% jitter). `3xx`, `5xx`, `429`, and network errors are retried; any
other `4xx` is treated as permanent. Exhausted deliveries are **dead-lettered and available
for replay** — same replay endpoints as stream deliveries.

**Idempotency:** deliveries can arrive more than once (a retry can race your slow `2xx`).
De-duplicate on `X-Kwery-Delivery-Id`.

## Questions we get 

- **What does this cost?** The crawls that feed change detection are ordinary lookups at
standard [credit rates](/guides/pricing#source-weights); deliveries are free. There is no
separate "events" fee.
- **How fast after a change do I get the event?** Detection happens when the product is
re-crawled — the event fires within that crawl's processing, so latency is bounded by
your watch list's crawl cadence, not by the delivery pipeline.
- **Can one change produce two events?** Yes, by design — a price move on a watched Amazon
product can fire both `price.delta` and the umbrella `product.changed` if you subscribed
to both. Subscribe to one level, or de-duplicate on (`product`, `delivered_at`).
- **What if my endpoint is down for an hour?** Retries cover ~43 minutes; anything past
that is dead-lettered and replayable — nothing is silently lost.


## Next steps

- [Webhooks](/guides/webhooks) — signature verification code, retry schedule, and replay
endpoints.
- [Confidence score](/guides/trust) — score the results your events point you at.
- [Destinations](/guides/destinations) — where change events (and batch/stream results) can be
delivered.