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.
| Level | Name | Delivers from | Good for |
|---|---|---|---|
| 1 | Critical only | 90%+ risk | Near-certain moves and departures only |
| 2 | High | 75%+ risk | The dashboard's red tier |
| 3 | Elevated (default) | 50%+ risk | Everything that can trigger an email alert |
| 4 | Moderate | 35%+ risk | Earlier, noisier signals |
| 5 | Every signal | 20%+ risk | Best 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.
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...{
"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.
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"
});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.