UserTrack
Developers / Webhooks

Growth events, pushed to you.

A signed HTTP POST the moment your SaaS hits a milestone, moves on a board, spikes, gets verified, or a data source fails and recovers. Same events as the dashboard and the feed — nothing synthesized from small metric changes.

Overview

How it works

Subscribe
Add up to 10 https endpoints in /app/developer/webhooks, pick events, optionally scope to one project.
Receive
Each event is a JSON POST with the project reference and event data, signed with HMAC-SHA256 and a timestamp.
Acknowledge
Respond 2xx within 10 s. Anything else is retried 4 more times over ~14.5 hours.
Setup

Four steps

01
Create an endpoint
Open /app/developer/webhooks, add your https URL, choose events and (optionally) one project. The signing secret (whsec_…) is shown once.
02
Verify the signature
Compute the HMAC over the raw request body and compare in constant time. Samples in Node and Python below.
03
Send a test
Click “Send test” on the endpoint; a webhook.test event goes through the exact same pipeline, signature and retry log.
04
Respond 2xx fast
Queue the work and return immediately. Use UserTrack-Event-Id to make your handler idempotent.
Events

Event catalog

milestone.reached
User / activated / converted thresholds, Top 10 / Top 100 entries, records and streaks.
"milestone": { "id": "m1…", "key": "users:10000", "kind": "users", "metric": "totalUsers", "value": 10000, "title": "10,000 users", "achievedAt": "…" }
rank.changed
The 30-day leaderboard position moved (at most once per UTC day).
"rank": { "board": "leaderboard", "window": "30d", "from": 14, "to": 9, "best": 9 }
trending.rank_changed
The 7-day trending position moved (at most once per UTC day).
"rank": { "board": "trending", "window": "7d", "from": 22, "to": 6, "score": 81.4 }
growth.spike
A day ≥ 3× the trailing 14-day average with at least 20 new users.
"spike": { "day": "2026-09-01", "newUsers": 412, "average": 96, "multiple": 4.3, "metric": "newUsers" }
integration.failed
A users source entered the unhealthy state (repeated failed syncs).
"integration": { "id": "i1…", "role": "users", "provider": "postgres", "label": "Postgres", "error": "connection refused", "consecutiveFailures": 6, "lastSuccessAt": "…" }
integration.recovered
The source synced again after an unhealthy episode.
"integration": { "id": "i1…", "role": "users", "provider": "postgres", "label": "Postgres", "unhealthySince": "…", "downForMs": 86400000 }
project.verified
First verified users sync of a project.
"verification": { "level": "verified", "provider": "clerk", "verifiedAt": "…" }
webhook.test
Sent on demand from the dashboard or MCP; carries test: true.
"message": "Test event from UserTrack…", "sample": { "type": "milestone.reached", "milestone": { "kind": "users", "metric": "totalUsers", "value": 10000, "title": "10,000 users" } }

Rank events fire at most once per UTC day per board. Milestones and spikes are deduplicated by key, so a re-sync never re-sends them.

Payload

Versioned JSON

Envelope
  • id — event id, stable across endpoints and retries.
  • type — one of the types above.
  • apiVersion2026-09-01. Only additive changes within a version.
  • createdAt — ISO 8601, when the event happened.
  • testtrue only on webhook.test.
  • data.project — id, slug, name, url, totalUsers. Always present.
Example · milestone.reached
{
  "id": "evt_milestone_reached_j57…_users_10000",
  "type": "milestone.reached",
  "apiVersion": "2026-09-01",
  "createdAt": "2026-09-02T08:00:00.000Z",
  "data": {
    "project": { "id": "j57…", "slug": "acme", "name": "Acme", "url": "https://usertrack.dev/s/acme", "totalUsers": 10000 },
    "milestone": { "id": "m1…", "key": "users:10000", "kind": "users", "metric": "totalUsers", "value": 10000, "title": "10,000 users", "achievedAt": "2026-09-02T08:00:00.000Z" }
  }
}
Signature verification

Trust, but verify

UserTrack-Signature
v1=<hex>
HMAC-SHA256 of `${timestamp}.${rawBody}` with your endpoint secret.
UserTrack-Timestamp
1756800000
Unix seconds when the request was signed. Reject if older than 5 minutes.
UserTrack-Event
milestone.reached
Event type; also in the body as type.
UserTrack-Delivery
dlv_…
Unique per delivery (one endpoint, one event). Also sent as Idempotency-Key.
UserTrack-Event-Id
evt_…
Deterministic per source event — identical across endpoints and retries.
Algorithm
  1. 01Read the raw request body as bytes — do not parse and re-serialize.
  2. 02Build the signed string: `${UserTrack-Timestamp}.${rawBody}`.
  3. 03Compute hex(HMAC-SHA256(secret, signedString)) and prefix with v1=.
  4. 04Compare with UserTrack-Signature using a constant-time comparison.
  5. 05Reject if |now − timestamp| > 300 s (5 minutes) to defeat replays.
Node / TypeScript
import { createHmac, timingSafeEqual } from "node:crypto";

// Express: app.post("/hooks", express.raw({ type: "application/json" }), handler)
export function verifyUserTrack(rawBody: Buffer | string, headers: Record<string, string | undefined>) {
  const secret = process.env.USERTRACK_WEBHOOK_SECRET;
  const timestamp = headers["usertrack-timestamp"] ?? "";
  const signature = headers["usertrack-signature"] ?? "";
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // 5-minute tolerance
  const expected = "v1=" + createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected), b = Buffer.from(signature);
  return a.length === b.length && timingSafeEqual(a, b);
}
Python
import hmac, hashlib, time

def verify_usertrack(raw_body: bytes, headers: dict, secret: str) -> bool:
    timestamp = headers.get("UserTrack-Timestamp", "")
    signature = headers.get("UserTrack-Signature", "")
    if abs(time.time() - float(timestamp or 0)) > 300:
        return False
    expected = "v1=" + hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)
Retries

Schedule

Attempt
Delay after previous failure
Cumulative
1
immediately
0
2
5 min
5 min
3
30 min
35 min
4
2 h
2 h 35 min
5
12 h
14 h 35 min
Rules
  • Retried: 5xx, 408, 425, 429, network errors and timeouts (10 s).
  • Final: any other 4xx. Fix the endpoint and use “Retry” in the delivery log.
  • After 5 attempts the delivery is exhausted; it can still be retried manually.
  • 25 consecutive failures disable the endpoint. Re-enable it from the dashboard; the failure counter resets.
  • Retries reuse the same delivery id and event id; the timestamp and signature are fresh on every attempt.
Test events

Exercise the whole pipeline

“Send test” in the dashboard (or usertrack_test_webhook over MCP) enqueues a webhook.test event regardless of the endpoint’s subscriptions. It is signed, retried and logged like any other delivery, carries test: true and a sample milestone.reached body under data.sample.

{ "id": "evt_test_…", "type": "webhook.test", "apiVersion": "2026-09-01", "createdAt": "…", "test": true,
  "data": { "project": { … }, "message": "Test event from UserTrack…", "endpointId": "…", "sample": { "type": "milestone.reached", "milestone": { … } } } }
Idempotency

Two ids, two jobs

UserTrack-Delivery · dlv_…

One per (endpoint, event). Identical on every retry of that delivery and sent again as Idempotency-Key. Use it to drop duplicate retries when your handler succeeded but the response got lost.

UserTrack-Event-Id · evt_… (= body.id)

Deterministic per source event, so two endpoints receiving the same milestone see the same id. Dedupe on it if several endpoints feed the same system.

Security

Defaults you cannot switch off

  • https only. http URLs and URLs with credentials are rejected at creation.
  • Private, loopback, link-local and cloud-metadata ranges are blocked, for literal IPs and for hostnames — DNS is resolved right before each delivery and refused if it points inside.
  • Secrets are generated server-side, shown once, and rotatable at any time (“Rotate secret”). Old signatures stop validating immediately.
  • Endpoints belong to your account; project-scoped endpoints only ever receive that project’s events. Demo projects never emit webhooks.
  • Payloads contain aggregate counts only — no emails, names or per-user rows.
  • Creating, rotating and deleting endpoints is recorded in your audit trail on /app/developer.
Limits

Numbers

Endpoints
10 per account
Timeout
10 s per attempt
Attempts
5 (immediately, 5 min, 30 min, 2 h, 12 h)
Auto-disable
25 consecutive failures
Signature tolerance
300 s
Delivery log
25 most recent per endpoint (up to 100 via API)
MCP tools
usertrack_get_webhookswebhooks:read
List endpoints with status and last delivery.
usertrack_create_webhookwebhooks:write
Create an endpoint; returns the secret once.
usertrack_update_webhookwebhooks:write
Change URL, events, scope, or enable / disable.
usertrack_test_webhookwebhooks:write
Queue a webhook.test delivery.
usertrack_get_webhook_deliverieswebhooks:read
Recent deliveries with attempts, HTTP status and errors.
Scopes webhooks:read / webhooks:write on your MCP token. See /developers#mcp.
Get started
Add your first endpoint
Free. Send a test event in under a minute.
Open /app/developer/webhooks