Skip to content

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.1
Content-Type: application/json
User-Agent: TeamPredict-Webhooks/1.0
X-TeamPredict-Event: poaching.opportunity
X-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, time

def 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.