PlayGenus

Webhook alerts

Get an HMAC-signed HTTP POST when a critical signal fires in your game — DAU drops, retention slips, or revenue drift — filtered by the signals you care about and a minimum severity.

You don't have to open the dashboard every morning to find out something broke. Point PlayGenus at an HTTPS endpoint, pick the signals you care about, and we'll POST you a signed payload the moment one of them crosses the line.

Alerts are built on the same Why Engine that powers the dashboard — so what fires is a real detected signal, not a raw threshold you had to guess at.

Rolling out: automatic daily evaluation is being enabled gradually as games accumulate enough history to detect signals reliably. You can set up and verify an endpoint today — the Send test event button delivers a real signed payload on demand — and it will start firing on live signals as the rollout reaches your account.

What can fire

Each alert comes from a signal the Why Engine already tracks. You subscribe by signal family and set a minimum severity:

Signal familyFires whenSeverity
dauDaily active users trend down (or up) versus the prior two weekswarning / info
retentionD1 or D7 retention drops versus the previous cohortcritical
revenueARPDAU drifts down, or spikeswarning / info

A webhook fires only for the families you subscribed to, and only when the signal's severity is at or above your floor (infowarningcritical). Set the floor to warning and you'll hear about problems but not routine fluctuations.

The same signal won't fire twice for the same game within a cooldown window — a persistent dip alerts you once, not every day.

Setting it up

In the dashboard, go to Settings → Alerts:

  1. Add your endpoint URL. It must be HTTPS — we won't POST to plain HTTP, and we won't resolve to a private or internal address.
  2. Choose the signal families (DAU, Revenue, Retention) and a minimum severity.
  3. Copy the signing secret shown when you create the endpoint. It's displayed once — store it now. You'll use it to verify payloads. (You can roll it later, which invalidates the old one.)
  4. Hit Send test event to confirm your receiver accepts the payload before any real signal fires.

The payload

We POST application/json to your URL:

{
  "event": "alert.fired",
  "id": "d5f1a3c0-6b2e-4a9f-b1c2-9e7f0a4d8b31",
  "created_at": "2026-07-16T03:00:00Z",
  "game": { "id": "1a58da93-…", "name": "Touchline Manager" },
  "signal": {
    "id": "a1b2c3d4e5f6",
    "type": "dau",
    "direction": "down",
    "severity": "warning",
    "metric": "dau",
    "title": "DAU Trending Down 22.4%",
    "summary": "Daily active users declined from 5,100 to 3,960 over the last 14 days",
    "recommendation": "Review UA campaigns, check for seasonal patterns, or assess if a recent update impacted engagement.",
    "delta": -0.224,
    "detected_at": "2026-07-16T02:40:00Z"
  },
  "dashboard_url": "https://app.playgenus.com/dashboard/why?game=1a58da93-…"
}
  • id is the delivery id — it matches the X-PlayGenus-Delivery header and is unique per POST, so you can de-duplicate retries on your side.
  • signal.type is the family you subscribed to; signal.metric is the exact underlying metric (e.g. retention_d7), and signal.direction is up or down.
  • dashboard_url deep-links to the Why Engine view for that game.
  • A test event is identical except event is "alert.test".

Headers & verifying the signature

Every delivery carries three headers:

HeaderValue
X-PlayGenus-Eventalert.fired (or alert.test)
X-PlayGenus-DeliveryThe delivery id — a UUID unique to this POST
X-PlayGenus-Signaturet=<unix>,v1=<hex> — see below

The signature is HMAC-SHA256(secret, "<t>.<raw request body>"), hex-encoded. The timestamp t is included in the signed material, so you can reject stale or replayed deliveries. Verify against the raw request body before you parse it — re-serializing the JSON will change the bytes and break the signature.

const crypto = require('crypto');

function verifyPlayGenusWebhook(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((kv) => kv.split('=')),
  );

  // Reject anything older than 5 minutes to blunt replay attacks.
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 || '');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Reject any request whose signature doesn't verify — that's how you know the POST is really from us and wasn't tampered with in transit.

Delivery & retries

  • Respond with any 2xx and we consider the delivery done.
  • On a 5xx or network error we retry up to 3 times with exponential backoff. A persistent outage on your side won't lose the alert to a single blip.
  • On a 4xx (bad URL, your endpoint rejected it) we don't retry — retrying a request that's wrong won't make it right. Fix the endpoint and the next signal will deliver.

Aim to acknowledge fast (return 2xx, then do your work): treat the webhook as a notification, not a place to run a long job.

On this page