Skip to content
TeamPredict

Developers / Webhooks

Webhooks guide

TeamPredict pushes a signed HTTPS POST to your endpoint the moment a warning or opportunity is detected - no polling. Admins add endpoints in the dashboard under Settings → Developers, where every endpoint also gets a test button and a delivery log.

Events

employee.warning

Someone on your own roster shows resignation-risk signals - a risky profile change, or a detected departure. The retention lens: act before the resignation letter.

poaching.opportunity

Someone at a tracked competitor shows the same signals - which there mean “may be open to a move.” The payload carries the competitor so your recruiting flow knows where they work today.

ping

A test event sent from the dashboard's “Send test” button, signed like every other delivery.

Sensitivity levels

Each event type on each endpoint has its own five-level sensitivity slider. The level sets the minimum AI risk score a change must reach to be delivered - higher level, more notifications. Departures are always delivered at every level.

LevelNameDelivers fromGood for
1Critical only90%+ riskNear-certain moves and departures only
2High75%+ riskThe dashboard's red tier
3Elevated (default)50%+ riskEverything that can trigger an email alert
4Moderate35%+ riskEarlier, noisier signals
5Every signal20%+ riskBest for warehouses and analytics

Delivery format

Deliveries are HTTPS POSTs with a JSON body. The event id is stable across retries - use it as an idempotency key. Respond with any 2xx to acknowledge; redirects are not followed, and requests time out after 10 seconds, so acknowledge first and process asynchronously.

Headers
POST /webhooks/teampredict HTTP/1.1Content-Type: application/jsonUser-Agent: TeamPredict-Webhooks/1.0X-TeamPredict-Event: poaching.opportunityX-TeamPredict-Delivery: cmdl1x2f40001...X-TeamPredict-Signature: t=1753607642,v1=5f8a2c...
Body (poaching.opportunity)
{  "id": "evt_18342_9",  "type": "poaching.opportunity",  "createdAt": "2026-07-27T09:14:02.511Z",  "sensitivityLevel": 3,  "organization": { "id": 7, "name": "Acme Robotics" },  "data": {    "competitor": { "id": 9, "name": "Rival Systems" },    "employee": {      "id": 456,      "name": "Sam Ortiz",      "title": "Senior Product Designer",      "location": "Denver, Colorado",      "linkedinUrl": "https://www.linkedin.com/in/samortiz",      "profileImageUrl": "https://...",      "url": "https://app.teampredict.ai/dashboard/organizations/9/employees/456"    },    "change": {      "id": 18342,      "changeType": "open_to_work",      "changedFields": "open_to_work; off; on",      "riskScore": 0.82,      "riskLevel": "high",      "summary": "Turned on Open to Work and updated their headline.",      "factors": ["Open to Work badge enabled", "Headline rewritten"],      "detectedAt": "2026-07-27T09:14:02.511Z"    }  }}

employee.warning events share the same shape with data.competitor set to null.

Verifying signatures

Every endpoint has a signing secret (shown at creation and retrievable by admins in the dashboard). Each delivery's X-TeamPredict-Signature header carries a Unix timestamp and an HMAC-SHA256 hex digest of `${timestamp}.${rawBody}`. Recompute it with your secret over the raw request body and compare in constant time; reject timestamps older than a few minutes.

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";function verifyTeamPredictSignature(header, rawBody, secret) {  const parts = Object.fromEntries(    header.split(",").map((p) => p.split("=", 2))  );  const t = Number(parts.t);  // Reject replays: only accept timestamps from the last 5 minutes.  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;  const expected = createHmac("sha256", secret)    .update(`${t}.${rawBody}`)    .digest("hex");  const a = Buffer.from(parts.v1 ?? "", "utf8");  const b = Buffer.from(expected, "utf8");  return a.length === b.length && timingSafeEqual(a, b);}// Express example - use the RAW body, not the parsed JSON:app.post("/webhooks/teampredict",  express.raw({ type: "application/json" }),  (req, res) => {    const ok = verifyTeamPredictSignature(      req.header("X-TeamPredict-Signature") ?? "",      req.body.toString("utf8"),      process.env.TEAMPREDICT_WEBHOOK_SECRET    );    if (!ok) return res.status(400).send("bad signature");    res.status(200).end(); // ack fast, process async    const event = JSON.parse(req.body.toString("utf8"));    // ... handle event.type: "employee.warning" | "poaching.opportunity" | "ping"  });
Python
import hashlib, hmac, timedef verify_teampredict_signature(header: str, raw_body: bytes, secret: str) -> bool:    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)    t = int(parts.get("t", "0"))    if not t or abs(time.time() - t) > 300:  # reject replays        return False    expected = hmac.new(        secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256    ).hexdigest()    return hmac.compare_digest(parts.get("v1", ""), expected)

Retries & failures

  • A delivery is attempted immediately, then retried after roughly 5 minutes, 30 minutes, 2 hours, 8 hours, and 24 hours - 6 attempts in total.
  • Any 2xx response counts as delivered. Timeouts (10s), connection errors, redirects, and non-2xx responses count as failures.
  • An endpoint that fails many deliveries in a row is disabled automatically; fix the receiver, then re-enable it in the dashboard (the failure counter resets).
  • The last 20 deliveries per endpoint - with status, HTTP code, and error - are visible in the dashboard's delivery log.
  • Endpoint URLs must be public HTTPS on the default port; localhost and private-network addresses are rejected.

Need to backfill or cross-reference after an event? The REST API serves the same employees, changes, and competitors on demand.