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 family | Fires when | Severity |
|---|---|---|
dau | Daily active users trend down (or up) versus the prior two weeks | warning / info |
retention | D1 or D7 retention drops versus the previous cohort | critical |
revenue | ARPDAU drifts down, or spikes | warning / info |
A webhook fires only for the families you subscribed to, and only when the
signal's severity is at or above your floor (info → warning → critical).
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:
- 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.
- Choose the signal families (DAU, Revenue, Retention) and a minimum severity.
- 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.)
- 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-…"
}idis the delivery id — it matches theX-PlayGenus-Deliveryheader and is unique per POST, so you can de-duplicate retries on your side.signal.typeis the family you subscribed to;signal.metricis the exact underlying metric (e.g.retention_d7), andsignal.directionisupordown.dashboard_urldeep-links to the Why Engine view for that game.- A test event is identical except
eventis"alert.test".
Headers & verifying the signature
Every delivery carries three headers:
| Header | Value |
|---|---|
X-PlayGenus-Event | alert.fired (or alert.test) |
X-PlayGenus-Delivery | The delivery id — a UUID unique to this POST |
X-PlayGenus-Signature | t=<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.
REST API & event format
The PlayGenus ingestion API — endpoints, authentication, the batch envelope, and the event JSON format shared by every SDK.
Import your historical data
Import historical game analytics from GameAnalytics, PlayFab, Firebase, Unity Analytics, or any CSV into PlayGenus, so churn and LTV models train on your real history.