Developer guide

How to detect job changes with the Coolie API in Python

Add the Recent Job Change signal (recent-job-change) to a Coolie agent: it finds people who match the agent's ICP and recently took on a new role, and each one is the lead's contact. Then read the agent's leads in Python and keep those whose signal.type is recent-job-change: signal.headline describes the new role, signal.postedAt is when it started, when known, and signal.url links to where the move was found.

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_KEY in 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
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.

Python
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:

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

JSON
{
  "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.

StatusCodeWhat to do
401missing_api_keyNo key in the request: send Authorization: Bearer coolie_sk_… or an x-api-key header.
401invalid_api_keyNot a Coolie key, or a key that is unknown or revoked: copy it again from Settings → API keys.
400invalid_querysince is not a date, cursor is not one the API gave, or limit is not a whole number from 1 to 100.
404agent_not_foundNo agent with this id in your account.
402no_creditsNot enough credits to find the email: buy credits in Settings → Credits.
429rate_limitedMore 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.

Your next conversation is already happening somewhere.

Set up an agent in five minutes. The first leads arrive with their reason to write.