Skip to content
TeamPredict
DevelopersAPI reference

REST API

Everything the dashboard does, over HTTPS

Read your people, their risk changes and your competitors - and change them too: add someone to tracking, start watching a new competitor, wire up a webhook. JSON in, JSON out, one API key.

GET readPOST createPATCH updateDELETE remove

Introduction

Every endpoint lives under https://app.teampredict.ai/api/v1. Requests and responses are JSON; dates are ISO 8601 in UTC. There is one version, v1, and new fields may be added to responses - parse leniently.

A key reaches one company plus the competitor workspaces that company tracks. Pass organizationId to work inside a competitor workspace; leave it out for your own company.

Everything on this page is also published as a machine readable OpenAPI 3.1 document at /openapi.json (and /openapi.yaml), so a client or an AI agent can generate against it without reading the prose.

First call
curl https://app.teampredict.ai/api/v1/me \  -H "Authorization: Bearer tp_your_api_key"

Authentication

Send your key as a bearer token on every request. Admins create keys in the dashboard under Settings → Developers. The full key is shown once at creation; only a hash is stored, so a lost key is revoked and replaced, never recovered.

Keys are per company. Revoking one takes effect immediately and cannot be undone.

Treat a key like a password

A write key can add people, and adding people costs money. Keep keys server-side, never in a browser or a mobile app, and give each integration its own key so one can be revoked without breaking the others.

Every request
Authorization: Bearer tp_your_api_keyContent-Type: application/json

Scopes

Each key is created either read-only or read and write. The choice is fixed at creation: to widen a key you create a new one and revoke the old, so an existing key can never quietly gain the power to spend money. Keys created before write access existed stay read-only.

Scopes

readevery key
All GET endpoints: people, risk changes, competitors, webhooks, billing.
writeopt-in
Everything above, plus POST, PATCH and DELETE. A read-only key calling one of those gets 403 INSUFFICIENT_SCOPE.
GET /me
{  "organization": { "id": 7, "name": "Northwind" },  "competitors": [    { "id": 9, "name": "Contoso" },    { "id": 14, "name": "Fabrikam" }  ],  "scopes": ["read", "write"]}

Seats and billing

Writing through the API bills exactly like clicking in the dashboard. Each tracked person is one seat at $5 per month, prorated for the part of the period they are tracked. There is no separate API bill.

Additions are charged in batches of 10 seats or $50, whichever comes first. A seat or two rides along to your next normal invoice; once a batch reaches either ceiling, the next add charges the whole batch to your card right then. So you never carry more than about $50 of unbilled additions, and a card that has stopped working shows up within one batch instead of at the end of the month.

Two ceilings because seats are not money. Ten seats is about $50 on the monthly plan but about $500 on the annual one, so the seat count alone would not bound anything on an annual plan. The dollar ceiling is what keeps the answer at about $50 on every plan.

Untracking or removing someone releases the seat, and Stripe credits the unused part. People inside your competitor workspaces are tracked profiles on the same subscription, at the same price.

What stops a runaway script

Batch settlement. A batch charges immediately once it reaches ten seats or $50 of additions, whichever lands first, so unbilled additions never exceed roughly $50 before your card is asked to cover them - on the monthly plan and the annual one alike.

A failed charge stops the bill growing. If that batch charge is finally declined, every further add is refused with 402 SEAT_CHARGE_FAILED until the card is fixed. Everyone already tracked keeps being monitored - only new seats are blocked. A single retryable decline does not trip it, and the block lifts by itself as soon as any payment on the account goes through.

Free-trial cap. Before your first paid invoice, a company can track at most 100 people, through any door. Each competitor workspace carries its own allowance.

Daily API allowance. A paying company can add up to 200 tracked people a day through the API. Roster imports and dashboard adds do not count against it.

Each answers with the ceiling you hit: 402 SEAT_CHARGE_FAILED, 402 TRIAL_LIMIT_REACHED or 429 SEAT_QUOTA_EXCEEDED. Batch calls add everything they can and report the rest in skipped rather than failing whole.

Adding without paying

Send tracked: false to put someone on the roster with no seat and no charge: they are not scanned and raise no signals until you switch tracking on with PATCH /employees/{id}. Useful for staging a roster before deciding who to watch.

Check before you add
curl https://app.teampredict.ai/api/v1/billing \  -H "Authorization: Bearer tp_your_api_key"

Errors

Every failure is JSON with the same two fields: a human-readable error, and a machine code on the ones worth branching on. Standard HTTP status codes carry the category.

StatusCodeMeaning
400INVALID_BODYA parameter is missing or the wrong type. The message says which.
401-Missing, malformed, unknown, or revoked API key.
402SUBSCRIPTION_INACTIVENo active subscription or trial, so API access is paused.
402TRIAL_LIMIT_REACHEDThe add would pass the free-trial seat cap. Start a paid plan.
402SEAT_CHARGE_FAILEDA seat charge was declined. No new people until the card is fixed.
403INSUFFICIENT_SCOPEA read-only key called a write endpoint.
404-The resource does not exist, or is outside this key's reach.
409EMPLOYEE_EXISTSThat LinkedIn profile is already on the roster.
429SEAT_QUOTA_EXCEEDEDToday's API seat-add allowance is spent. Retry-After says when.
429ROW_QUOTA_EXCEEDEDToday's allowance for adding people at all, tracked or not, is spent.
429-Over 120 requests a minute. Wait Retry-After seconds.
503SEAT_RESERVATION_BUSYToo many adds for this company at once. Retry-After says when to try again.
Shape
{  "error": "Your free trial can track up to 100 people (0 left). Start a paid plan to track more.",  "code": "TRIAL_LIMIT_REACHED"}

Rate limits

Each key may make 120 requests per minute. Past that the API answers 429 with a Retry-After header in seconds. Wait that long and retry; a backoff loop of three or four attempts is enough for any normal integration.

The seat allowance uses the same status with a much longer Retry-After, because it frees up over a rolling 24 hours rather than in a minute. Check code to tell the two apart.

Throttled
HTTP/1.1 429 Too Many RequestsRetry-After: 37Content-Type: application/json{ "error": "Too many requests. Please try again shortly." }

Pagination

List endpoints (/employees, /changes, /employees/{id}/changes) take page and pageSize (default 25, max 100) and answer with { items, total, page, pageSize, hasMore }.

Loop while hasMore is true. Results are newest first, so for a nightly job it is cheaper to pass since on /changes than to page through history you already have.

Page through a roster
let page = 1;const people = [];for (;;) {  const res = await call(`/employees?page=${page}&pageSize=100`);  people.push(...res.items);  if (!res.hasMore) break;  page += 1;}

Recipe: keep the roster in sync

The most common integration: a nightly job that mirrors your HRIS. Starters get added in one batch, leavers get untracked - which releases the seat but keeps their history, so a boomerang hire picks up where they left off.

Untrack rather than delete unless you genuinely want the record gone. Both stop the charge; only one is reversible.

Nightly sync
// Keep TeamPredict's roster in step with your HRIS, nightly.const [hris, tracked] = await Promise.all([  fetchActiveStaffFromHris(),          // your system of record  call("/employees?tracked=true&pageSize=100"),]);const trackedByUrl = new Map(tracked.items.map((e) => [e.linkedinUrl, e]));// Starters: add in one batch, and read what was skipped.const starters = hris  .filter((p) => p.linkedinUrl && !trackedByUrl.has(p.linkedinUrl))  .map((p) => p.linkedinUrl);if (starters.length > 0) {  await call("/employees/bulk", {    method: "POST",    body: JSON.stringify({ linkedinUrls: starters.slice(0, 50), tracked: true }),  });}// Leavers: untrack rather than delete - the seat goes, the history stays.const active = new Set(hris.map((p) => p.linkedinUrl));for (const person of tracked.items) {  if (!active.has(person.linkedinUrl)) {    await call(`/employees/${person.id}`, {      method: "PATCH",      body: JSON.stringify({ tracked: false }),    });  }}

Check a key

GET/v1/me

Which company this key reaches, the competitor workspaces reachable through it, and what the key is allowed to do. Cheap, and the right first call in any integration - both to confirm the key works and to see whether it can write.

Returns

The organization, an array of competitor workspaces, and scopes.

curl https://app.teampredict.ai/api/v1/me \  -H "Authorization: Bearer tp_your_api_key"

Retrieve a company

GET/v1/organization

The company profile and its headcounts. Safe fields only - no billing state (that is /billing).

Query parameters

organizationIdinteger
A competitor workspace id to read instead of your own company.
curl https://app.teampredict.ai/api/v1/organization \  -H "Authorization: Bearer tp_your_api_key"

Update a company

Write key

PATCH/v1/organization

Edit the company profile and the score at which alert emails fire.

Body parameters

namestring
Display name.
websitestring | null
Company website.
companyInfostring | null
One-paragraph description used in AI risk context.
alertRiskThresholdnumber
0.5 to be alerted on anything at Watch or above, 0.75 for High only. Values in between snap to one of the two.
organizationIdinteger
A competitor workspace to edit instead.

The LinkedIn page is not editable here

It is what the daily scan points at, so re-pointing it would change which company you monitor. Change it in the dashboard, deliberately.

curl -X PATCH https://app.teampredict.ai/api/v1/organization \  -H "Authorization: Bearer tp_your_api_key" \  -H "Content-Type: application/json" \  -d '{ "alertRiskThreshold": 0.75 }'

Retrieve seats and cost

GET/v1/billing

What tracking costs today and how much more this key may add: billed seats across your company and its competitor workspaces, the plan and per-seat rate, an estimated recurring total, and what is left under both ceilings. Read this before a large batch.

Returns

Totals are estimates from tracked seats and the list price; Stripe's invoice is the source of truth and also reflects proration. seatSettlement tells you how many seats are unbilled and how many more until the next batch is charged - and whether a charge has failed, which blocks every add until it clears.

curl https://app.teampredict.ai/api/v1/billing \  -H "Authorization: Bearer tp_your_api_key"

List people

GET/v1/employees

The roster, newest first, each person carrying their latest visible change and risk level.

Query parameters

organizationIdinteger
A competitor workspace id (the poaching lens).
qstring
Search name, title and location.
trackedboolean
Filter to tracked (billed) or untracked people.
page, pageSizeinteger
Pagination. pageSize defaults to 25, max 100.

Returns

riskLevel maps the 0-1 score to the dashboard's tiers: high at 75%+, watch at 50-74%, low below. Departures carry no numeric score and read as high.

curl "https://app.teampredict.ai/api/v1/employees?tracked=true&pageSize=100" \  -H "Authorization: Bearer tp_your_api_key"

Add a person

Write keyAffects your bill

POST/v1/employees

Start tracking someone by their LinkedIn profile URL. The row is created immediately and the profile fetch (real name, photo, existence check) finishes in the background, so the response comes back with status: "importing" and a placeholder name - poll the person, or wait for the first daily scan.

A tracked add is one billed seat. See seats and billing.

Body parameters

linkedinUrlstringrequired
The public profile URL, e.g. https://www.linkedin.com/in/jordanlee. The internal /in/ACoAA… form is rejected - no provider can resolve it.
trackedboolean
Defaults to true. False adds them with no seat and no charge.
namestring
Display name to use until LinkedIn answers.
organizationIdinteger
Add to a competitor workspace instead of your own company.

Re-adding someone you removed

Adding a profile you previously archived revives that same row, so their history and tracking window come back with them. Adding a profile already on the roster answers 409 EMPLOYEE_EXISTS.

curl -X POST https://app.teampredict.ai/api/v1/employees \  -H "Authorization: Bearer tp_your_api_key" \  -H "Content-Type: application/json" \  -d '{    "linkedinUrl": "https://www.linkedin.com/in/jordanlee",    "tracked": true  }'

Add people in bulk

Write keyAffects your bill

POST/v1/employees/bulk

Up to 50 people in one call. Partial success is the normal outcome: everything that can be added is, and the rest comes back in skipped with a reason - a bad URL, a duplicate, or a seat ceiling.

The status code tells you which happened: 201 when the whole list landed, 207 when some of it did not.

Body parameters

linkedinUrlsstring[]required
1 to 50 public LinkedIn profile URLs.
trackedboolean
Defaults to true. Applies to the whole batch.
organizationIdinteger
Add into a competitor workspace instead.

Returns

added, skipped (each with reason and a readable message), and seatAllowance so you know what is left before the next batch.

curl -X POST https://app.teampredict.ai/api/v1/employees/bulk \  -H "Authorization: Bearer tp_your_api_key" \  -H "Content-Type: application/json" \  -d '{    "linkedinUrls": [      "https://www.linkedin.com/in/jordanlee",      "https://www.linkedin.com/in/samirpatel",      "https://www.linkedin.com/in/aliceweber"    ],    "tracked": true  }'

Retrieve a person

GET/v1/employees/{id}

One person with their latest visible change and risk level. Works for your own roster and for anyone inside a competitor workspace this key reaches.

curl https://app.teampredict.ai/api/v1/employees/123 \  -H "Authorization: Bearer tp_your_api_key"

Update a person

Write keyAffects your bill

PATCH/v1/employees/{id}

Turn tracking on or off, or archive and restore. Tracking is the billing switch: off releases the seat while keeping the person and their history on the roster.

Body parameters

trackedboolean
True starts scanning them and takes a seat; false stops both.
status"active" | "archived"
Archiving always untracks. Restoring sets tracked to false unless you also send tracked: true.

Names and URLs are not editable

A person's name and LinkedIn URL mirror LinkedIn through the daily sync, so the API will not let an integration overwrite them.

# Stop paying for someone without losing their historycurl -X PATCH https://app.teampredict.ai/api/v1/employees/481 \  -H "Authorization: Bearer tp_your_api_key" \  -H "Content-Type: application/json" \  -d '{ "tracked": false }'

Remove a person

Write keyAffects your bill

DELETE/v1/employees/{id}

Archives the person and releases their seat, keeping their change history. To erase the record for good, archive first and then call again with ?permanent=true - two deliberate steps, so one stray call can never destroy history.

Query parameters

permanentboolean
Erase an already-archived person and their history. Cannot be undone.
# 1. Archive: releases the seat, keeps the change historycurl -X DELETE https://app.teampredict.ai/api/v1/employees/481 \  -H "Authorization: Bearer tp_your_api_key"# 2. Only if you really want it gone: erase the archived row and its historycurl -X DELETE "https://app.teampredict.ai/api/v1/employees/481?permanent=true" \  -H "Authorization: Bearer tp_your_api_key"

List a person's changes

GET/v1/employees/{id}/changes

That person's detected profile changes with risk scores, newest first, windowed to what your company is allowed to see. Paginated.

Query parameters

page, pageSizeinteger
Pagination. pageSize defaults to 25, max 100.
curl "https://app.teampredict.ai/api/v1/employees/123/changes?pageSize=50" \  -H "Authorization: Bearer tp_your_api_key"

List risk changes

GET/v1/changes

The company-wide feed of detected profile changes with risk scores, newest first. This is the endpoint most integrations poll: filter by since and minRiskScore and you get exactly the signals worth acting on.

Query parameters

organizationIdinteger
A competitor workspace id (the poaching lens).
minRiskScorenumber
0 to 1. Only changes at or above this score. Departures are always included.
sinceISO 8601
Only changes detected at or after this time.
changeTypestring
One category: profile_update, headline_change, title_change, open_to_work, new_skills, location_change, photo_or_summary, edit_activity, experience_change, employee_departed.
page, pageSizeinteger
Pagination. pageSize defaults to 25, max 100.
curl "https://app.teampredict.ai/api/v1/changes?minRiskScore=0.5&since=2026-07-01T00:00:00Z" \  -H "Authorization: Bearer tp_your_api_key"

List competitors

GET/v1/competitors

The competitor workspaces your company tracks, each with its tracked-people count. Use an id as organizationId on /employees and /changes to read that competitor through the poaching lens - there, a high score means “may be open to a move”.

# 1. Find your competitor workspacescurl https://app.teampredict.ai/api/v1/competitors \  -H "Authorization: Bearer tp_your_api_key"# 2. Read that competitor's people, and their signalscurl "https://app.teampredict.ai/api/v1/employees?organizationId=9" \  -H "Authorization: Bearer tp_your_api_key"curl "https://app.teampredict.ai/api/v1/changes?organizationId=9&minRiskScore=0.75" \  -H "Authorization: Bearer tp_your_api_key"

Track a competitor

Write keyAffects your bill

POST/v1/competitors

Start tracking a competitor from their LinkedIn company page. The workspace is created immediately; its roster imports in the background, so people appear over the following minutes.

Creating the workspace costs nothing, but the people it finds are tracked profiles on your subscription. Pass roleFilters to keep the import to the roles you actually care about.

Body parameters

companyLinkedInUrlstringrequired
Their LinkedIn company page, e.g. https://www.linkedin.com/company/contoso/.
roleFiltersstring[]
Departments ("engineering", "sales", "product") or free text ("enterprise sales"). Empty means the whole roster.
namestring
The brand you meant. Checked against the page the URL resolves to.
acknowledgeIdentityMismatchboolean
Resend with true to track a page that did not match the name you gave.

Identity is checked, not assumed

If the page does not belong to the brand you named, the call is refused with 409 COMPETITOR_IDENTITY_MISMATCH and the company it really resolves to, so an automation cannot quietly start tracking the wrong business.

curl -X POST https://app.teampredict.ai/api/v1/competitors \  -H "Authorization: Bearer tp_your_api_key" \  -H "Content-Type: application/json" \  -d '{    "companyLinkedInUrl": "https://www.linkedin.com/company/contoso/",    "roleFilters": ["engineering", "product"]  }'

Retrieve a competitor

GET/v1/competitors/{id}

One competitor workspace: its role filters, whether it is paused, and how many of its people are tracked.

curl https://app.teampredict.ai/api/v1/competitors/9 \  -H "Authorization: Bearer tp_your_api_key"

Pause or retarget a competitor

Write keyAffects your bill

PATCH/v1/competitors/{id}

paused: true is the reversible way to stop paying for a competitor: everyone in the workspace is untracked, so the scan stops and the seats come off your subscription, while the people and their history stay. paused: false brings both back.

roleFilters changes who the import targets and re-runs it. People already imported stay - untrack or remove them like anyone else.

Body parameters

pausedboolean
Stop or resume monitoring and billing for this workspace.
roleFiltersstring[]
Replace the role targeting and re-run the roster import.
# Pause: keeps the people and history, releases every seatcurl -X PATCH https://app.teampredict.ai/api/v1/competitors/9 \  -H "Authorization: Bearer tp_your_api_key" \  -H "Content-Type: application/json" \  -d '{ "paused": true }'# Retarget: changes who the import looks for, and re-runs itcurl -X PATCH https://app.teampredict.ai/api/v1/competitors/9 \  -H "Authorization: Bearer tp_your_api_key" \  -H "Content-Type: application/json" \  -d '{ "roleFilters": ["engineering", "enterprise sales"] }'

Delete a competitor

Write keyAffects your bill

DELETE/v1/competitors/{id}

Erases the workspace, its people and their change history, then releases the seats. This is not “stop tracking” - that is PATCH with paused: true, which keeps the history. Deleting cannot be undone.

curl -X DELETE https://app.teampredict.ai/api/v1/competitors/9 \  -H "Authorization: Bearer tp_your_api_key"

List webhook endpoints

GET/v1/webhooks

Your registered receivers and their delivery health. Endpoints belong to your own company; competitor-workspace events arrive here too, flipped to the poaching lens. Signing secrets are never returned.

curl https://app.teampredict.ai/api/v1/webhooks \  -H "Authorization: Bearer tp_your_api_key"

Create a webhook endpoint

Write key

POST/v1/webhooks

Register an HTTPS receiver for employee.warning and poaching.opportunity events. The signing secret is returned once, here - every delivery is HMAC-SHA256 signed with it, so verify before you trust a payload.

Body parameters

urlstringrequired
HTTPS endpoint. Private and loopback addresses are refused.
descriptionstring
Label for your own records.
employeeWarningsboolean
Your own roster, retention lens. Defaults to true.
poachingOpportunitiesboolean
Competitor workspaces, poaching lens. Defaults to true.
employeeWarningSensitivity1-5
1 sends everything, 5 only the strongest signals. Defaults to 3.
poachingOpportunitySensitivity1-5
Same scale, for the poaching stream.

Payload shapes, signature verification and the retry schedule are on the webhooks page.

curl -X POST https://app.teampredict.ai/api/v1/webhooks \  -H "Authorization: Bearer tp_your_api_key" \  -H "Content-Type: application/json" \  -d '{    "url": "https://hooks.example.com/teampredict",    "description": "Retention alerts into Slack",    "employeeWarningSensitivity": 4,    "poachingOpportunities": false  }'

Update a webhook endpoint

Write key

PATCH/v1/webhooks/{id}

Change the URL, the event switches or their sensitivity, or flip the endpoint off without deleting it. Re-enabling clears the failure counter, so an endpoint we auto-disabled after repeated failures gets a clean slate.

Body parameters

enabledboolean
Turn delivery on or off.
urlstring
Move deliveries to a new HTTPS endpoint.
employeeWarnings, poachingOpportunitiesboolean
Which streams this endpoint receives.
employeeWarningSensitivity, poachingOpportunitySensitivity1-5
How strong a signal has to be to reach you.
# Turn an endpoint off without deleting it (and its delivery history)curl -X PATCH https://app.teampredict.ai/api/v1/webhooks/whe_2c9f1a \  -H "Authorization: Bearer tp_your_api_key" \  -H "Content-Type: application/json" \  -d '{ "enabled": false }'

Delete a webhook endpoint

Write key

DELETE/v1/webhooks/{id}

Removes the endpoint and its delivery history. To stop deliveries but keep the record, send PATCH { enabled: false } instead.

curl -X DELETE https://app.teampredict.ai/api/v1/webhooks/whe_2c9f1a \  -H "Authorization: Bearer tp_your_api_key"

Prefer events over polling?

Webhooks push employee warnings and poaching opportunities to you as they are detected - signed, retried, with a sensitivity slider per event. Most integrations use both: webhooks to hear about a signal, the API to act on it.