Developer guide

How to get new Coolie leads in Python

Call GET /api/v1/leads on https://trycoolie.ai with your Coolie API key as a Bearer token. Pass since, the processedAt of the newest lead you already have, to get only the leads found after it, up to 100 a page, newest first; while nextCursor is not null, send it back as cursor for the next page. Each lead carries its company, the buying signal that surfaced it, the contact (email and phone once found), an AI score from 0 to 10, and its research and messages once written.

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).
  • 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
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.

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']}"
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:

crontab
*/30 * * * * cd /opt/coolie && COOLIE_API_KEY=coolie_sk_your_key python3 coolie_leads.py >> coolie.log 2>&1

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

JavaScript (Node.js 18+)
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.type is the id of the signal that surfaced the lead (hiring, funding, recent-job-change, tech-used, competitor-churn…, as GET /api/v1/signals lists them), signal.headline the job posting's title or the change, signal.postedAt its date.
  • contact is null when nobody at the company matches the agent's job titles (status no_contact); contact.email and contact.phone stay null until they are found.
  • aiScore ranks the lead from 0 to 10; icpMatch says whether the contact's title matches the agent's job titles.
  • research and sequence stay null until the lead is researched: see AI research and emails for a lead.
  • status is 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.

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

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.
429rate_limitedMore 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.

Your next conversation is already happening somewhere.

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