Docs Sign inGet started
Integrations

Webhooks

Get a signed HTTPS POST when a run finishes, when something real changes, or when a shipped fix gets a verdict.

Webhooks are on paid plans (Starter, Growth and Agency)

A webhook endpoint is a URL on your server. Onym sends it a JSON event each time something happens in your workspace, so you can post to Slack, open a ticket or refresh a report without polling the API.

Events

EventSent when
run.completedA scheduled run finished (headline numbers and change count)
change.detectedA run produced real changes (beyond noise)
opportunity.measuredA shipped opportunity reached a verdict (lift, drop or no change)

Events fire after scheduled runs, for every domain in the workspace. A one-off grade doesn't send them.

1. Create an endpoint

In the dashboard, open Settings → Webhooks, paste an https:// URL and pick the events you want. You need the admin role. Onym shows the endpoint's signing secret once; it starts with whsec_. Store it with your other secrets.

The URL must be reachable from the internet. Onym refuses localhost, private IP ranges and hosts that don't resolve to a public address, checks again before every delivery, and doesn't follow redirects.

2. Handle the event

Every event has the same envelope:

change.detected
{
  "id": "evt_8f3a1c0b9d2e4f6a7b81",
  "event": "change.detected",
  "created_at": "2026-10-14T06:41:09Z",
  "data": {
    "domain": {
      "id": "47adc410-7772-48cb-b07d-898978697153",
      "host": "acme.example",
      "brand": "Acme",
      "dashboard": "https://getonym.com/app/d/47adc410-7772-48cb-b07d-898978697153"
    },
    "run_date": "2026-10-14",
    "events": [
      {
        "kind": "competitor",
        "tone": "bad",
        "title": "Northwind gained on Perplexity",
        "detail": "41% → 58% of answers (n=74)",
        "link": "/competitors"
      }
    ]
  }
}

Return any 2xx status within 10 seconds, then do the slow work afterwards, for example on a queue.

A minimal handler
from flask import Flask, abort, request

app = Flask(__name__)
SECRET = "whsec_..."  # from Settings → Webhooks

@app.post("/hooks/onym")
def onym():
    if not verify(request.get_data(), request.headers.get("X-Onym-Signature", ""), SECRET):
        abort(400)
    event = request.get_json()
    if event["event"] == "change.detected":
        for change in event["data"]["events"]:
            print(change["tone"], change["title"])
    return "", 204

3. Verify the signature

Each request has these headers:

Header Value
X-Onym-Event The event type, like run.completed
X-Onym-Signature t=<unix seconds>,v1=<hex HMAC>
User-Agent Onym-Webhooks/1.0

To verify, compute an HMAC-SHA256 of the timestamp, a period and the raw request body, keyed with your secret, and compare it with v1 in constant time. Reject timestamps more than five minutes old so a captured request can't be replayed.

Verify X-Onym-Signature
import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    try:
        parts = dict(p.split("=", 1) for p in header.split(","))
        ts, sig = int(parts["t"]), parts["v1"]
    except (KeyError, ValueError):
        return False
    if abs(time.time() - ts) > tolerance:
        return False  # too old: possibly a replay
    expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)

Verify the raw bytes. If your framework parses the JSON and serializes it again before you verify, the signature won't match. Read the body before any JSON middleware touches it.

4. Send a test

Click Send test next to the endpoint in Settings. Onym posts a run.completed event whose data contains "test": true, and shows the status code your server returned.

Payloads

run.completed

JSON
{
  "domain": { "id": "…", "host": "acme.example", "brand": "Acme", "dashboard": "…" },
  "run_date": "2026-10-14",
  "compared_with": "2026-10-13",
  "visibility": { "k": 61, "n": 133, "rate": 0.459, "low": 0.376, "high": 0.543 },
  "changes": 2
}

compared_with is null after a domain's first run.

change.detected

Sent only when a run produced at least one change. events lists them; see Changes for every kind.

opportunity.measured

JSON
{
  "domain": { "id": "…", "host": "acme.example", "brand": "Acme", "dashboard": "…" },
  "opportunity": {
    "id": "…",
    "title": "Publish a comparison for “Enterprise SSO”",
    "kind": "owned",
    "shipped_at": "2026-09-02T00:00:00+00:00",
    "shipped_url": "https://acme.example/blog/sso-compared"
  },
  "verdict": "up",
  "before": { "k": 9, "n": 84, "rate": 0.107, "low": 0.057, "high": 0.191 },
  "after": { "k": 31, "n": 88, "rate": 0.352, "low": 0.262, "high": 0.455 },
  "change": { "delta": 0.245, "significant": true, "direction": "up", "pairs": 21 }
}

Each verdict (up, down or none) is sent once per opportunity. See Opportunities.

Retries and failures

Attempt When
1 As soon as the run finishes
2 2 seconds after a failure
3 8 seconds after that

A status of 300 or more, a timeout after 10 seconds, or a connection error counts as a failure. If all three attempts fail, the event counts as one failed delivery. After 20 failed deliveries in a row, Onym turns the endpoint off and marks it disabled in Settings. Fix the endpoint, then click Re-enable; that resets the count.

The event id stays the same across attempts. Events can arrive out of order and, rarely, twice, so record the ids you've handled and skip repeats.

Debugging

What you see Likely cause Fix
Signature never matches The body was parsed before verifying Verify against the raw bytes
Signature fails sometimes Your server's clock is more than 5 minutes off Sync it with NTP
Last status is 301 or 302 The URL redirects Use the final URL, including any trailing slash
Last status starts with error: DNS, TLS or connection failure Check the certificate and that the host is public
Endpoint shows as disabled 20 failed deliveries in a row Fix it, then re-enable
Nothing arrives Your plan changed to Free, or no scheduled run has finished Check Plan & billing and the Changes page

See also