OnyxMetric

Webhooks

Get a signed HTTPS POST the moment a goal converts or a custom event is tracked

Webhooks push events to your own systems as they happen. Point a webhook at an HTTPS endpoint, pick the goals and custom event names you care about, and OnyxMetric sends a signed JSON POST for every matching event, typically within a few seconds of ingest.

Webhooks are available on paid plans. Each site can have up to 10 webhooks.

Creating a webhook

Open Site settings > Webhooks, add your endpoint URL, and choose at least one goal or event name. The signing secret is shown once, right after creation. Store it somewhere safe; if you lose it, rotate it to get a new one.

You can also manage webhooks through the API (/api/sites/{siteId}/webhooks) or the list_webhooks, create_webhook, and delete_webhook MCP tools. Reading webhooks needs the webhooks:read scope; changing them needs webhooks:write.

What triggers a delivery

  • Event names: a custom event whose name exactly equals one of the names you picked.
  • Goals: any event that satisfies a goal's definition, using the same matching rules as the goal reports (path patterns, event names, property filters, autocapture values).

An event that matches several triggers on the same webhook produces one delivery per trigger, so you always know which goal or event name fired. Pageviews only trigger webhooks through path goals.

Endpoint requirements

  • The URL must use https:// and may not contain a username or password.
  • The host must resolve to a public address. Private, loopback, link-local, and other reserved ranges (for example 10.0.0.0/8, 192.168.0.0/16, 127.0.0.1, 169.254.169.254, ::1) are rejected when you save the webhook and again at send time.
  • Redirects are not followed. Respond with any 2xx status within 10 seconds to acknowledge a delivery.

Payload

Every delivery is a POST with Content-Type: application/json:

{
  "id": "5f0c1c8e-6d5b-4a59-9f0e-2b8f4c0a7d13",
  "type": "event.matched",
  "created_at": "2026-10-18T15:04:05.000Z",
  "webhook_id": "0b6f3a52-2c1e-4f0a-8d4e-7a1b9c3d5e6f",
  "site_id": 42,
  "trigger": { "kind": "goal", "goal_id": 7, "name": "Signup" },
  "event": {
    "type": "custom_event",
    "event_name": "signup",
    "props": { "plan": "pro" },
    "pathname": "/welcome",
    "hostname": "example.com",
    "session_id": "a1b2c3",
    "user_id": "d4e5f6",
    "identified_user_id": "customer-123",
    "country": "US",
    "timestamp": "2026-10-18 15:04:04.512"
  }
}

trigger is either { "kind": "goal", "goal_id", "name" } or { "kind": "event", "name" }. id is stable across retries of the same delivery, so use it to deduplicate.

The Send test button (or POST /api/sites/{siteId}/webhooks/{webhookId}/test) sends a test.ping payload with id, type, created_at, webhook_id, and site_id, and returns the result immediately.

Verifying signatures

Each request carries three headers:

HeaderValue
Onyx-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>
Onyx-Webhook-IdThe webhook's ID
Onyx-Delivery-IdThe delivery's ID (same as the payload id)

The signature is an HMAC-SHA256, keyed with your full secret (including the whsec_ prefix), over the string <t>.<raw request body>. Always verify against the raw body bytes, before any JSON parsing, and reject timestamps more than 5 minutes old to block replays.

verify.js
import crypto from "node:crypto";

export function verifyOnyxSignature(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map(part => part.split("=", 2)));
  const timestamp = Number(parts.t);
  if (!Number.isFinite(timestamp) || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;

  const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(parts.v1, "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express example: keep the raw body for verification.
app.post("/hooks/onyx", express.raw({ type: "application/json" }), (req, res) => {
  const ok = verifyOnyxSignature(req.body.toString("utf8"), req.get("Onyx-Signature") ?? "", process.env.ONYX_WEBHOOK_SECRET);
  if (!ok) return res.sendStatus(400);
  const delivery = JSON.parse(req.body);
  // ...handle delivery.trigger / delivery.event, dedupe on delivery.id
  res.sendStatus(204);
});

Rotating the secret takes effect immediately for new attempts, so deploy the new secret to your endpoint right after rotating.

Retries and auto-disable

A delivery fails on a non-2xx response, a timeout, or a connection error. Failed deliveries are retried up to 5 more times, after 1 minute, 5 minutes, 30 minutes, 2 hours, and 6 hours (6 attempts in total).

After 20 consecutive failed deliveries the webhook is disabled and the reason is shown in settings. Fix the endpoint, then re-enable the webhook to reset the failure count. Any successful delivery also resets it.

The delivery log for each webhook (status, attempts, response code, error) is kept for 30 days.

On this page