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.1Content-Type: application/jsonUser-Agent: TeamPredict-Webhooks/1.0X-TeamPredict-Event: poaching.opportunityX-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, 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.