Updated
Before you start
- An API key: in Coolie, open Settings → API keys, name the key and click Create key. It is shown once, so keep it in an environment variable (
COOLIE_API_KEYin these examples). It acts as you, with your plan, your credits and your agents. - A Coolie account with its 7-day free trial started: the API answers 402 trial_required before that. A call costs what the same action costs in the app, and nothing more.
- Python 3.8 or later with the requests package (
pip install requests).
Create an agent that watches job changes
The signal looks for people who match your agent's ICP (job titles, industries, locations, company sizes) and started their current role in the last few months: a new leader often reviews the tools and vendors they inherit. An agent watches three to five signals, so pair it with signals that fit the same buyers. The body below watches Hiring, Funding and Recent Job Change:
curl https://trycoolie.ai/api/v1/agents \
-H "Authorization: Bearer $COOLIE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Ops leaders at hospitals in France",
"icp": {
"jobTitles": [
"COO",
"Head of Operations"
],
"industries": [
"Hospitals and Health Care"
],
"locations": [
"France"
],
"companySizes": [
"201-500 employees",
"501-1000 employees"
],
"keywordsInclude": [],
"keywordsExclude": [],
"mode": "broad"
},
"signalEventIds": [
"hiring",
"funding",
"recent-job-change"
],
"signalConfigs": {
"hiring": {
"jobTitles": [
"Operations Manager"
]
}
},
"findEmail": true,
"findPhone": false
}'Keep the agentId it answers with. More on the body: how to create a Coolie agent with the API.
Read the job changes in Python
The script reads the agent's leads found since its last run, keeps the job changes, prints who moved, to which role and since when, and moves its mark forward. Run it on a schedule, every 15 to 60 minutes.
import json
import os
import pathlib
import time
import requests
API = "https://trycoolie.ai/api/v1"
SESSION = requests.Session()
SESSION.headers["Authorization"] = f"Bearer {os.environ['COOLIE_API_KEY']}"
AGENT_ID = os.environ["COOLIE_AGENT_ID"] # an agent that watches recent-job-change
STATE = pathlib.Path("job-changes-state.json")
def get(path, params):
"""One GET. Waits and tries again when the minute's 60 requests are used up."""
while True:
response = SESSION.get(f"{API}{path}", params=params, timeout=60)
if response.status_code == 429 and response.json().get("error") == "rate_limited":
time.sleep(int(response.headers.get("Retry-After", "1")))
continue
if response.status_code >= 400:
raise RuntimeError(f"{response.status_code} {response.text}")
return response.json()
def job_changes_since(since):
"""The agent's job-change leads found after since, and the newest processedAt seen."""
params = {"limit": 100}
if since:
params["since"] = since
found, newest = [], since
while True:
page = get(f"/agents/{AGENT_ID}/leads", params)
for lead in page["leads"]:
newest = max(newest or "", lead["processedAt"])
if lead["signal"]["type"] == "recent-job-change":
found.append(lead)
if page["nextCursor"] is None:
return found, newest
params["cursor"] = page["nextCursor"]
state = json.loads(STATE.read_text()) if STATE.exists() else {}
changes, newest = job_changes_since(state.get("since"))
for lead in reversed(changes): # oldest first
person = lead["contact"] or {}
started = lead["signal"]["postedAt"] or "recently"
print(f"{person.get('name', 'Someone')}: {lead['signal']['headline']} (started {started})")
print(f" {person.get('email') or 'no email yet'} {lead['signal']['url'] or ''}")
if newest:
STATE.write_text(json.dumps({"since": newest}))Find their email
A job-change lead comes without an email when the agent does not look emails up (findEmail false) or found none. One call per lead finds it: 0.1 credit when an email is found, nothing otherwise. Appended to the script above:
for lead in changes:
if lead["contact"] and not lead["contact"]["email"]:
response = SESSION.post(
f"{API}/agents/{AGENT_ID}/leads/contact",
json={"runId": lead["runId"], "prospectId": lead["prospectId"], "field": "email"},
timeout=120,
)
result = response.json()
if response.status_code == 402:
print("Out of credits:", result["message"])
break
print(lead["contact"]["name"], result.get("value") if response.ok else result["error"])See how to find a lead's email with the Coolie API for the answers and errors.
Turn the move into a first message
Research the lead with POST /api/v1/agents/{agentId}/leads/actions (action research, 1 credit a lead): Coolie writes a brief and an email and LinkedIn sequence that start from the new role. See AI research and emails for a lead.
A job-change lead
One lead of GET /api/v1/agents/{agentId}/leads whose signal is Recent Job Change. The values are examples.
{
"runId": "ui-2d6f81e3",
"prospectId": "p-0b5a7e93d2c1",
"agentId": "agent-3f2a91c0",
"status": "enriched",
"processedAt": "2026-10-19T07:15:02.880Z",
"icpMatch": true,
"aiScore": 7.6,
"feedback": null,
"signal": {
"type": "recent-job-change",
"name": "Recent Job Change",
"headline": "Head of Operations at Brightline Clinics, since Sep 2026",
"detail": null,
"url": "https://www.linkedin.com/in/example",
"postedAt": "2026-09-01"
},
"company": {
"name": "Brightline Clinics",
"domain": "brightline.example",
"website": "https://brightline.example",
"linkedinUrl": "https://www.linkedin.com/company/example",
"industry": "Hospitals and Health Care",
"employees": 410,
"country": "FR",
"locations": []
},
"contact": {
"name": "Priya Shah",
"title": "Head of Operations",
"seniority": null,
"linkedinUrl": "https://www.linkedin.com/in/example",
"country": "France",
"email": null,
"emailConfidence": null,
"phone": null
},
"research": null,
"sequence": null
}Errors
An error answers with its HTTP status and a body { "error": code, "message": sentence }: branch on the code, show or log the message.
| Status | Code | What to do |
|---|---|---|
| 401 | missing_api_key | No key in the request: send Authorization: Bearer coolie_sk_… or an x-api-key header. |
| 401 | invalid_api_key | Not a Coolie key, or a key that is unknown or revoked: copy it again from Settings → API keys. |
| 400 | invalid_query | since is not a date, cursor is not one the API gave, or limit is not a whole number from 1 to 100. |
| 404 | agent_not_found | No agent with this id in your account. |
| 402 | no_credits | Not enough credits to find the email: buy credits in Settings → Credits. |
| 429 | rate_limited | More than 60 requests in the minute for your account: wait the seconds Retry-After gives, then try again. |
Frequently asked questions
Who is the contact on a job-change lead?
The person who changed roles, kept only when their new title matches the agent's job titles.
How recent are the job changes?
The signal keeps people who started their current role in the last few months; signal.postedAt gives the start date when it is known.
Can Coolie follow job changes of my own contacts or customers?
Not with this signal: it looks for people who match your agent's ICP, not for a list of contacts you upload.
Can I get each job change as it is found?
Yes: a webhook subscribed to lead.created receives each lead as it is found; keep those whose data.lead.signal.type is recent-job-change. See Coolie webhooks in Node.js.
What does it cost?
The leads count toward your plan's daily leads; an email found costs 0.1 credit, a phone 1, and researching a lead 1 credit.
Every endpoint, field and error is in the API reference, and more guides are in the developer hub.
