Get told when it changes, without polling

A monitor re-runs a call on an interval, compares it with the last result and sends a signed webhook only when a rule you set fires. Costs are estimated when you create it, and every delivery can be verified.

Four events, one header

Every delivery names its event in the Onekey-Event header and in the body's type. Only monitor.changed carries the delta, the matched rules and the data.

Webhook events
  1. 1Onekey-Event: monitor.changeda rule fired
  2. 2Onekey-Event: monitor.run_faileda run failed; the body carries the error
  3. 3Onekey-Event: monitor.pausedthe monitor paused itself; reason
  4. 4Onekey-Event: testsent on demand to check your endpoint

What a monitor is#

A monitor is a saved call that Onekey re-runs on an interval, compares with the previous result, and reports to a webhook you own when something you asked about changes. It is "tell me when it changes" without polling. Nothing here uses a language model: change detection is hashing and arithmetic over the data the endpoint served. Any of the 1,995 endpoints can be monitored, and so can three recipes: a multi-platform search, a creator card, or a public URL.

Interval15m, 1h, 6h, 12h or 24h. Nothing shorter. Each run lands within 5 % of the interval.
Cap25 active monitors per account. Paused ones do not count.
WebhookAn https URL on port 443 to a public host, signed with a per-monitor secret shown once.
CostThe endpoint's price per run, with the estimate on the monitor itself. Cache hits are free and failed runs are refunded.

Create one#

This watches the star count of a repository once an hour and asks for a webhook when it crosses 251,000 or moves by 1 % or more:

shell
curl -X POST "https://api.onekey.dev/v1/monitors" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "name": "react stars",
    "target": {"platform": "github", "endpoint": "repo", "params": {"owner": "facebook", "repo": "react"}},
    "interval": "1h",
    "rules": [
      {"on": "threshold", "field": "entity.stats.stars", "op": "gte", "value": 251000},
      {"on": "field_change", "field": "entity.stats.stars", "op": "changed_by_pct", "value": 1}
    ],
    "webhook": {"url": "https://hooks.example.com/onekey"}
  }'

Replace the example webhook URL with an HTTPS endpoint you control: the host in the example does not exist, and a URL that does not resolve to a public address is refused with WEBHOOK_URL_REJECTED (checked on the dev API, along with an invalid interval, which is a free INVALID_INTERVAL). The answer is 201 with the monitor and webhook.secret, shown once, so copy it then. It also carries the cost estimate: estimated_credits_per_run, estimated_monthly_credits (720 for a 1-credit endpoint hourly: 2,880 at 15m, 120 at 6h, 30 at 24h) and estimated_monthly_credits_max. Choose the longest interval that still meets your need.

Rules: what deserves a webhook#

Up to 10 rules per monitor, validated when you create it. A webhook is sent when at least one fires, and the event says which rule fired and for which items.

RuleFires when
any_change (default)The content hash of the served data differs from the previous run.
new_itemsAt least n item ids appeared (min_count, default 1).
field_changeA numeric field changed on any item, optionally by at least an amount (gt, lt, gte, lte) or by a percentage (changed_by_pct).
thresholdA numeric field crosses a line. Edge-triggered: one alert when crossed, not one per run above it.

The first run is a baseline: nothing to compare with, so no rule fires. Every later run carries a delta of what was added, removed and changed, with from, to, delta and percentage per numeric field; each list is capped at 100 entries and says when it is truncated. A recipe run with a failed leg keeps that platform's previous items and lists it in partial_platforms, so a platform having a bad hour does not read as everything on it being removed. Rule and delta details are on the monitors docs.

Webhooks you can trust#

Every delivery is signed. The header is Onekey-Signature: t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of "<t>." + raw request body with your secret. Verify over the raw bytes, compare in constant time, and refuse a timestamp more than 300 seconds off your clock:

python
import hmac, hashlib, time

def verify(secret: str, header: str, body: bytes, tolerance_s: int = 300) -> bool:
    try:
        parts = dict(p.strip().split("=", 1) for p in header.split(","))
        t, given = int(parts["t"]), parts["v1"]
    except (KeyError, ValueError):
        return False
    if abs(time.time() - t) > tolerance_s:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(given, expected)

Paste a real delivery into the free webhook signature verifier to check it in your browser and see why a check fails. It runs locally: your secret is never sent anywhere.

Webhook URL rules (SSRF)#

A webhook URL is refused with WEBHOOK_URL_REJECTED, at creation and on update, unless it is https on port 443 to a public host. No http, no user info, no other port, no private or numeric IP literals, and no internal names (localhost, *.local, *.internal). The host must resolve only to public addresses, so loopback, private ranges, link-local, cloud-metadata and reserved addresses are all rejected, and a host with one private answer among public ones is rejected. At delivery time the host is resolved again with the same rules and the connection goes to that exact address, so a DNS change between the check and the connect cannot redirect the request. To test locally, put a public HTTPS tunnel or a request catcher in front of your machine.

When a monitor pauses#

If a run cannot be paid for (account balance, the creating key's credit limit, or a revoked key) the monitor pauses itself, charges nothing, and sends monitor.paused with the reason. A run that fails for any other reason (the source errored, the record was not found) is refunded, reported as monitor.run_failed, and the monitor stays active. Monitors belong to the key that created them, so create them with a key you intend to keep.

Limits, honestly#