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). - At least one agent: its leads come in over the day, as its searches run.
Read the newest leads with curl
One request shows the shape of the answer: your newest leads, every agent's, 25 a page unless limit says otherwise (100 at most).
curl "https://trycoolie.ai/api/v1/leads?limit=5" \
-H "Authorization: Bearer $COOLIE_API_KEY"The key can also go in an x-api-key header. Add agentId=… to the query to read one agent's leads, or call GET /api/v1/agents/{agentId}/leads, which takes the same query.
Get every new lead in Python
The script keeps the newest processedAt it has seen in a small file. Each run asks for the leads found after it, follows nextCursor to the last page, handles the leads oldest first, and moves the mark forward. The first run reads every lead the account has.
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']}"
STATE = pathlib.Path("coolie-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 leads_since(since):
"""Every lead found after since (ISO 8601), newest first, all pages."""
params = {"limit": 100}
if since:
params["since"] = since
leads = []
while True:
page = get("/leads", params)
leads.extend(page["leads"])
if page["nextCursor"] is None:
return leads
params["cursor"] = page["nextCursor"]
def main():
state = json.loads(STATE.read_text()) if STATE.exists() else {}
leads = leads_since(state.get("since"))
for lead in reversed(leads): # oldest first
contact = lead["contact"] or {}
print(lead["aiScore"], lead["signal"]["name"], lead["company"]["name"],
contact.get("name"), contact.get("email"))
if leads:
state["since"] = leads[0]["processedAt"] # the newest lead's
STATE.write_text(json.dumps(state))
if __name__ == "__main__":
main()A lead is named by its runId and its prospectId: store the pair with the lead, and skip a pair you already have. Every call that acts on a lead takes it: finding its email, researching it, rating it.
Run it on a schedule
An agent's searches take turns through the day, so a run every 15 to 60 minutes is enough. With cron:
*/30 * * * * cd /opt/coolie && COOLIE_API_KEY=coolie_sk_your_key python3 coolie_leads.py >> coolie.log 2>&1Each run costs one request a page, against the 60 requests a minute an account gets. To act on each lead within minutes of it being found instead, use a webhook: receive Coolie webhooks and verify their signature.
The same in JavaScript
Node.js 18 or later has fetch built in. Save the script as coolie-leads.mjs and run node coolie-leads.mjs.
const API = "https://trycoolie.ai/api/v1";
async function get(path, params) {
for (;;) {
const response = await fetch(API + path + "?" + new URLSearchParams(params), {
headers: { Authorization: "Bearer " + process.env.COOLIE_API_KEY },
});
const data = await response.json();
if (response.status === 429 && data.error === "rate_limited") {
const seconds = Number(response.headers.get("Retry-After") ?? 1);
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
continue;
}
if (!response.ok) throw new Error(response.status + " " + data.error + ": " + data.message);
return data;
}
}
// Every lead found after since (ISO 8601), newest first, all pages.
async function leadsSince(since) {
const params = { limit: "100" };
if (since) params.since = since;
const leads = [];
for (;;) {
const page = await get("/leads", params);
leads.push(...page.leads);
if (page.nextCursor === null) return leads;
params.cursor = page.nextCursor;
}
}
const leads = await leadsSince("2026-10-01T00:00:00Z");
for (const lead of leads.reverse()) {
console.log(lead.aiScore, lead.signal.name, lead.company.name, lead.contact?.name, lead.contact?.email);
}What each lead holds
signal.typeis the id of the signal that surfaced the lead (hiring,funding,recent-job-change,tech-used,competitor-churn…, asGET /api/v1/signalslists them),signal.headlinethe job posting's title or the change,signal.postedAtits date.contactis null when nobody at the company matches the agent's job titles (statusno_contact);contact.emailandcontact.phonestay null until they are found.aiScoreranks the lead from 0 to 10;icpMatchsays whether the contact's title matches the agent's job titles.researchandsequencestay null until the lead is researched: see AI research and emails for a lead.statusis one of new, enriched, researched, written, disqualified and no_contact.
What GET /api/v1/leads answers
A page of leads, newest first, and the cursor of the next page (null on the last one). The values are examples.
{
"leads": [
{
"runId": "ui-5b9e2c41",
"prospectId": "p-41b7c2d9e0aa",
"agentId": "agent-3f2a91c0",
"status": "enriched",
"processedAt": "2026-10-19T09:02:13.511Z",
"icpMatch": true,
"aiScore": 8.4,
"feedback": null,
"signal": {
"type": "hiring",
"name": "Hiring",
"headline": "Operations Manager",
"detail": null,
"url": "https://jobs.example.com/12345",
"postedAt": "2026-10-17"
},
"company": {
"name": "Northwind Health",
"domain": "northwind.example",
"website": "https://northwind.example",
"linkedinUrl": "https://www.linkedin.com/company/example",
"industry": "Hospitals and Health Care",
"employees": 320,
"country": "France",
"locations": [
"Lyon, France"
]
},
"contact": {
"name": "Camille Martin",
"title": "COO",
"seniority": "c_suite",
"linkedinUrl": "https://www.linkedin.com/in/example",
"country": "France",
"email": "camille.martin@northwind.example",
"emailConfidence": 0.92,
"phone": null
},
"research": null,
"sequence": null
}
],
"nextCursor": "WyIyMDI2LTEwLTE5VDA5OjAyOjEzLjUxMVoiLCJwLTQxYjdjMmQ5ZTBhYSJd"
}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. |
| 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
Does reading leads through the API cost credits?
No. Reading leads costs nothing; credits pay for the work you ask for on a lead: 0.1 credit for an email found, 1 for a phone, 1 for researching it.
How often should I poll for new leads?
Every 15 to 60 minutes is plenty: an agent's searches take turns through the day. To get each lead as it is found, subscribe a webhook to lead.created instead.
How do I get one agent's leads only?
Add agentId=… to the query, or call GET /api/v1/agents/{agentId}/leads. GET /api/v1/agents lists your agents with their ids.
Why is contact.email null?
The agent did not look it up (its findEmail is off) or found none. You can ask for it lead by lead: see find a lead's email.
Can I download the leads as a CSV file?
Yes: GET /api/v1/agents/{agentId}/export answers with the agent's leads as the CSV file the app downloads.
Every endpoint, field and error is in the API reference, and more guides are in the developer hub.
